Eliminate MethodCall hashCode and equals (#12277)
Since MethodCall equality checks are limited to test scenarios, this patch replaces them with an equivalent test matcher. At present MethodCalls are always used in scenarios where indentity-based equality/hashing is appropriate. This change avoids an assertion failure when MethodCall args are Iterable (possible since args are of type dyanmic), and hashValue() from dart:ui asserts that its input is not an Iterable. The alternative of implementing support for deep equality in dart:ui was rejected on the basis that if we're to encourage performant code, expensive checks should be obviously-expensive to the author.
This commit is contained in:
parent
354ab0ce7b
commit
4e2e69797a
@ -3,7 +3,6 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui' show hashValues;
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@ -46,48 +45,6 @@ class MethodCall {
|
|||||||
/// Must be a valid value for the [MethodCodec] used.
|
/// Must be a valid value for the [MethodCodec] used.
|
||||||
final dynamic arguments;
|
final dynamic arguments;
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator == (dynamic other) {
|
|
||||||
if (identical(this, other))
|
|
||||||
return true;
|
|
||||||
if (runtimeType != other.runtimeType)
|
|
||||||
return false;
|
|
||||||
return method == other.method && _deepEquals(arguments, other.arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues(method, arguments);
|
|
||||||
|
|
||||||
bool _deepEquals(dynamic a, dynamic b) {
|
|
||||||
if (a == b)
|
|
||||||
return true;
|
|
||||||
if (a is List)
|
|
||||||
return b is List && _deepEqualsList(a, b);
|
|
||||||
if (a is Map)
|
|
||||||
return b is Map && _deepEqualsMap(a, b);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
|
|
||||||
if (a.length != b.length)
|
|
||||||
return false;
|
|
||||||
for (int i = 0; i < a.length; i++) {
|
|
||||||
if (!_deepEquals(a[i], b[i]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
|
|
||||||
if (a.length != b.length)
|
|
||||||
return false;
|
|
||||||
for (dynamic key in a.keys) {
|
|
||||||
if (!b.containsKey(key) || !_deepEquals(a[key], b[key]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType($method, $arguments)';
|
String toString() => '$runtimeType($method, $arguments)';
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'message_codecs_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Haptic feedback control test', () async {
|
test('Haptic feedback control test', () async {
|
||||||
final List<MethodCall> log = <MethodCall>[];
|
final List<MethodCall> log = <MethodCall>[];
|
||||||
@ -15,6 +17,7 @@ void main() {
|
|||||||
|
|
||||||
await HapticFeedback.vibrate();
|
await HapticFeedback.vibrate();
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[const MethodCall('HapticFeedback.vibrate')]));
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall('HapticFeedback.vibrate', arguments: null));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
68
packages/flutter/test/services/message_codecs_utils.dart
Normal file
68
packages/flutter/test/services/message_codecs_utils.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart' show MethodCall;
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class _IsMethodCall extends Matcher {
|
||||||
|
const _IsMethodCall(this.name, this.arguments);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final dynamic arguments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||||
|
if (item is! MethodCall)
|
||||||
|
return false;
|
||||||
|
if (item.method != name)
|
||||||
|
return false;
|
||||||
|
return _deepEquals(item.arguments, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _deepEquals(dynamic a, dynamic b) {
|
||||||
|
if (a == b)
|
||||||
|
return true;
|
||||||
|
if (a is List)
|
||||||
|
return b is List && _deepEqualsList(a, b);
|
||||||
|
if (a is Map)
|
||||||
|
return b is Map && _deepEqualsMap(a, b);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
|
||||||
|
if (a.length != b.length)
|
||||||
|
return false;
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
if (!_deepEquals(a[i], b[i]))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
|
||||||
|
if (a.length != b.length)
|
||||||
|
return false;
|
||||||
|
for (dynamic key in a.keys) {
|
||||||
|
if (!b.containsKey(key) || !_deepEquals(a[key], b[key]))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) {
|
||||||
|
return description
|
||||||
|
.add('has method name: ').addDescriptionOf(name)
|
||||||
|
.add(' with arguments: ').addDescriptionOf(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a matcher that matches [MethodCall] instances with the specified
|
||||||
|
/// method name and arguments.
|
||||||
|
///
|
||||||
|
/// Arguments checking implements deep equality for [List] and [Map] types.
|
||||||
|
Matcher isMethodCall(String name, {@required dynamic arguments}) {
|
||||||
|
return new _IsMethodCall(name, arguments);
|
||||||
|
}
|
@ -7,6 +7,8 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'message_codecs_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('SystemChrome overlay style test', (WidgetTester tester) async {
|
testWidgets('SystemChrome overlay style test', (WidgetTester tester) async {
|
||||||
// The first call is a cache miss and will queue a microtask
|
// The first call is a cache miss and will queue a microtask
|
||||||
@ -33,10 +35,11 @@ void main() {
|
|||||||
DeviceOrientation.portraitUp,
|
DeviceOrientation.portraitUp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[new MethodCall(
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall(
|
||||||
'SystemChrome.setPreferredOrientations',
|
'SystemChrome.setPreferredOrientations',
|
||||||
<String>['DeviceOrientation.portraitUp'],
|
arguments: <String>['DeviceOrientation.portraitUp'],
|
||||||
)]));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setApplicationSwitcherDescription control test', () async {
|
test('setApplicationSwitcherDescription control test', () async {
|
||||||
@ -50,10 +53,11 @@ void main() {
|
|||||||
const ApplicationSwitcherDescription(label: 'Example label', primaryColor: 0xFF00FF00)
|
const ApplicationSwitcherDescription(label: 'Example label', primaryColor: 0xFF00FF00)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[new MethodCall(
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall(
|
||||||
'SystemChrome.setApplicationSwitcherDescription',
|
'SystemChrome.setApplicationSwitcherDescription',
|
||||||
<String, dynamic>{'label':'Example label','primaryColor':4278255360}
|
arguments: <String, dynamic>{'label': 'Example label', 'primaryColor': 4278255360},
|
||||||
)]));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setApplicationSwitcherDescription missing plugin', () async {
|
test('setApplicationSwitcherDescription missing plugin', () async {
|
||||||
@ -80,9 +84,10 @@ void main() {
|
|||||||
|
|
||||||
await SystemChrome.setEnabledSystemUIOverlays(<SystemUiOverlay>[SystemUiOverlay.top]);
|
await SystemChrome.setEnabledSystemUIOverlays(<SystemUiOverlay>[SystemUiOverlay.top]);
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[new MethodCall(
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall(
|
||||||
'SystemChrome.setEnabledSystemUIOverlays',
|
'SystemChrome.setEnabledSystemUIOverlays',
|
||||||
<String>['SystemUiOverlay.top'],
|
arguments: <String>['SystemUiOverlay.top'],
|
||||||
)]));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'message_codecs_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('System navigator control test', () async {
|
test('System navigator control test', () async {
|
||||||
final List<MethodCall> log = <MethodCall>[];
|
final List<MethodCall> log = <MethodCall>[];
|
||||||
@ -15,6 +17,7 @@ void main() {
|
|||||||
|
|
||||||
await SystemNavigator.pop();
|
await SystemNavigator.pop();
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[const MethodCall('SystemNavigator.pop')]));
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall('SystemNavigator.pop', arguments: null));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'message_codecs_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('System sound control test', () async {
|
test('System sound control test', () async {
|
||||||
final List<MethodCall> log = <MethodCall>[];
|
final List<MethodCall> log = <MethodCall>[];
|
||||||
@ -15,6 +17,7 @@ void main() {
|
|||||||
|
|
||||||
await SystemSound.play(SystemSoundType.click);
|
await SystemSound.play(SystemSoundType.click);
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[const MethodCall('SystemSound.play', 'SystemSoundType.click')]));
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall('SystemSound.play', arguments: 'SystemSoundType.click'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../services/message_codecs_utils.dart';
|
||||||
import 'semantics_tester.dart';
|
import 'semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -243,8 +244,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(log, <MethodCall>[
|
expect(log, hasLength(1));
|
||||||
const MethodCall('TextInput.setEditingState', const <String, dynamic>{
|
expect(log.single, isMethodCall(
|
||||||
|
'TextInput.setEditingState',
|
||||||
|
arguments: const <String, dynamic>{
|
||||||
'text': 'Wobble',
|
'text': 'Wobble',
|
||||||
'selectionBase': -1,
|
'selectionBase': -1,
|
||||||
'selectionExtent': -1,
|
'selectionExtent': -1,
|
||||||
@ -252,8 +255,8 @@ void main() {
|
|||||||
'selectionIsDirectional': false,
|
'selectionIsDirectional': false,
|
||||||
'composingBase': -1,
|
'composingBase': -1,
|
||||||
'composingExtent': -1,
|
'composingExtent': -1,
|
||||||
}),
|
},
|
||||||
]);
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
|
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
|
||||||
|
@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../services/message_codecs_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('toString control test', (WidgetTester tester) async {
|
testWidgets('toString control test', (WidgetTester tester) async {
|
||||||
final Widget widget = new Title(
|
final Widget widget = new Title(
|
||||||
@ -58,9 +60,10 @@ void main() {
|
|||||||
color: const Color(0xFF00FF00),
|
color: const Color(0xFF00FF00),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(log, equals(<MethodCall>[new MethodCall(
|
expect(log, hasLength(1));
|
||||||
|
expect(log.single, isMethodCall(
|
||||||
'SystemChrome.setApplicationSwitcherDescription',
|
'SystemChrome.setApplicationSwitcherDescription',
|
||||||
<String, dynamic>{'label': '', 'primaryColor': 4278255360},
|
arguments: <String, dynamic>{'label': '', 'primaryColor': 4278255360},
|
||||||
)]));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user