Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.

Commit 09a7cb1

Browse files
committed
fix: Improve the output of many matchers that expect specific types
- Add a new TypedMatcher class to generalize `matches` type checking - Use TypeMatcher across many of the existing Matcher implementations - Update tests validate new, more consistent failure messages - Add a few new tests for `isIn`
1 parent e4c7d12 commit 09a7cb1

13 files changed

+267
-287
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
99

10+
- Improved the output of `Matcher` instances that fail due to type errors.
11+
1012
## 0.12.2+1
1113

1214
- Updated SDK version to 2.0.0-dev.17.0

lib/src/core_matchers.dart

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'feature_matcher.dart';
56
import 'interfaces.dart';
67
import 'util.dart';
78

@@ -69,15 +70,17 @@ const Matcher isNaN = const _IsNaN();
6970
/// A matcher that matches any non-NaN value.
7071
const Matcher isNotNaN = const _IsNotNaN();
7172

72-
class _IsNaN extends Matcher {
73+
class _IsNaN extends FeatureMatcher<num> {
7374
const _IsNaN();
74-
bool matches(item, Map matchState) => double.nan.compareTo(item) == 0;
75+
bool typedMatches(num item, Map matchState) =>
76+
double.nan.compareTo(item) == 0;
7577
Description describe(Description description) => description.add('NaN');
7678
}
7779

78-
class _IsNotNaN extends Matcher {
80+
class _IsNotNaN extends FeatureMatcher<num> {
7981
const _IsNotNaN();
80-
bool matches(item, Map matchState) => double.nan.compareTo(item) != 0;
82+
bool typedMatches(num item, Map matchState) =>
83+
double.nan.compareTo(item) != 0;
8184
Description describe(Description description) => description.add('not NaN');
8285
}
8386

@@ -127,10 +130,10 @@ class isInstanceOf<T> extends TypeMatcher<T> {
127130
/// a wrapper will have to be created.
128131
const Matcher returnsNormally = const _ReturnsNormally();
129132

130-
class _ReturnsNormally extends Matcher {
133+
class _ReturnsNormally extends FeatureMatcher<Function> {
131134
const _ReturnsNormally();
132135

133-
bool matches(f, Map matchState) {
136+
bool typedMatches(Function f, Map matchState) {
134137
try {
135138
f();
136139
return true;
@@ -143,8 +146,8 @@ class _ReturnsNormally extends Matcher {
143146
Description describe(Description description) =>
144147
description.add("return normally");
145148

146-
Description describeMismatch(
147-
item, Description mismatchDescription, Map matchState, bool verbose) {
149+
Description describeTypedMismatch(Function item,
150+
Description mismatchDescription, Map matchState, bool verbose) {
148151
mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
149152
if (verbose) {
150153
mismatchDescription.add(' at ').add(matchState['stack'].toString());
@@ -257,11 +260,12 @@ class _Contains extends Matcher {
257260
const _Contains(this._expected);
258261

259262
bool matches(item, Map matchState) {
263+
var expected = _expected;
260264
if (item is String) {
261-
return item.contains((_expected as Pattern));
265+
return expected is Pattern && item.contains(expected);
262266
} else if (item is Iterable) {
263-
if (_expected is Matcher) {
264-
return item.any((e) => (_expected as Matcher).matches(e, matchState));
267+
if (expected is Matcher) {
268+
return item.any((e) => expected.matches(e, matchState));
265269
} else {
266270
return item.contains(_expected);
267271
}
@@ -287,27 +291,29 @@ class _Contains extends Matcher {
287291

288292
/// Returns a matcher that matches if the match argument is in
289293
/// the expected value. This is the converse of [contains].
290-
Matcher isIn(expected) => new _In(expected);
294+
Matcher isIn(expected) {
295+
if (expected is Iterable) {
296+
return new _In(expected, expected.contains);
297+
} else if (expected is String) {
298+
return new _In<Pattern>(expected, expected.contains);
299+
} else if (expected is Map) {
300+
return new _In(expected, expected.containsKey);
301+
}
291302

292-
class _In extends Matcher {
293-
final Object _expected;
303+
throw new ArgumentError.value(
304+
expected, 'expected', 'Only Iterable, Map, and String are supported.');
305+
}
294306

295-
const _In(this._expected);
307+
class _In<T> extends FeatureMatcher<T> {
308+
final Object _source;
309+
final bool Function(T) _containsFunction;
296310

297-
bool matches(item, Map matchState) {
298-
var expected = _expected;
299-
if (expected is String) {
300-
return expected.contains(item as Pattern);
301-
} else if (expected is Iterable) {
302-
return expected.contains(item);
303-
} else if (expected is Map) {
304-
return expected.containsKey(item);
305-
}
306-
return false;
307-
}
311+
const _In(this._source, this._containsFunction);
312+
313+
bool typedMatches(T item, Map matchState) => _containsFunction(item);
308314

309315
Description describe(Description description) =>
310-
description.add('is in ').addDescriptionOf(_expected);
316+
description.add('is in ').addDescriptionOf(_source);
311317
}
312318

313319
/// Returns a matcher that uses an arbitrary function that returns
@@ -322,13 +328,13 @@ Matcher predicate<T>(bool f(T value),
322328

323329
typedef bool _PredicateFunction<T>(T value);
324330

325-
class _Predicate<T> extends Matcher {
331+
class _Predicate<T> extends FeatureMatcher<T> {
326332
final _PredicateFunction<T> _matcher;
327333
final String _description;
328334

329335
_Predicate(this._matcher, this._description);
330336

331-
bool matches(item, Map matchState) => _matcher(item as T);
337+
bool typedMatches(T item, Map matchState) => _matcher(item);
332338

333339
Description describe(Description description) =>
334340
description.add(_description);

lib/src/equals_matcher.dart

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'description.dart';
6+
import 'feature_matcher.dart';
67
import 'interfaces.dart';
78
import 'util.dart';
89

@@ -16,67 +17,61 @@ import 'util.dart';
1617
/// handle cyclic structures a recursion depth [limit] can be provided. The
1718
/// default limit is 100. [Set]s will be compared order-independently.
1819
Matcher equals(expected, [int limit = 100]) => expected is String
19-
? new _StringEqualsMatcher(expected) as Matcher
20+
? new _StringEqualsMatcher(expected)
2021
: new _DeepMatcher(expected, limit);
2122

2223
typedef _RecursiveMatcher = List<String> Function(
2324
dynamic, dynamic, String, int);
2425

2526
/// A special equality matcher for strings.
26-
class _StringEqualsMatcher extends Matcher {
27+
class _StringEqualsMatcher extends FeatureMatcher<String> {
2728
final String _value;
2829

2930
_StringEqualsMatcher(this._value);
3031

31-
bool get showActualValue => true;
32-
33-
bool matches(item, Map matchState) => _value == item;
32+
bool typedMatches(String item, Map matchState) => _value == item;
3433

3534
Description describe(Description description) =>
3635
description.addDescriptionOf(_value);
3736

38-
Description describeMismatch(
39-
item, Description mismatchDescription, Map matchState, bool verbose) {
40-
if (item is! String) {
41-
return mismatchDescription.addDescriptionOf(item).add('is not a string');
42-
} else {
43-
var buff = new StringBuffer();
44-
buff.write('is different.');
45-
var escapedItem = escape(item);
46-
var escapedValue = escape(_value);
47-
var minLength = escapedItem.length < escapedValue.length
48-
? escapedItem.length
49-
: escapedValue.length;
50-
var start = 0;
51-
for (; start < minLength; start++) {
52-
if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
53-
break;
54-
}
37+
Description describeTypedMismatch(String item,
38+
Description mismatchDescription, Map matchState, bool verbose) {
39+
var buff = new StringBuffer();
40+
buff.write('is different.');
41+
var escapedItem = escape(item);
42+
var escapedValue = escape(_value);
43+
var minLength = escapedItem.length < escapedValue.length
44+
? escapedItem.length
45+
: escapedValue.length;
46+
var start = 0;
47+
for (; start < minLength; start++) {
48+
if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
49+
break;
5550
}
56-
if (start == minLength) {
57-
if (escapedValue.length < escapedItem.length) {
58-
buff.write(' Both strings start the same, but the actual value also'
59-
' has the following trailing characters: ');
60-
_writeTrailing(buff, escapedItem, escapedValue.length);
61-
} else {
62-
buff.write(' Both strings start the same, but the actual value is'
63-
' missing the following trailing characters: ');
64-
_writeTrailing(buff, escapedValue, escapedItem.length);
65-
}
51+
}
52+
if (start == minLength) {
53+
if (escapedValue.length < escapedItem.length) {
54+
buff.write(' Both strings start the same, but the actual value also'
55+
' has the following trailing characters: ');
56+
_writeTrailing(buff, escapedItem, escapedValue.length);
6657
} else {
67-
buff.write('\nExpected: ');
68-
_writeLeading(buff, escapedValue, start);
69-
_writeTrailing(buff, escapedValue, start);
70-
buff.write('\n Actual: ');
71-
_writeLeading(buff, escapedItem, start);
72-
_writeTrailing(buff, escapedItem, start);
73-
buff.write('\n ');
74-
for (var i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' ');
75-
buff.write('^\n Differ at offset $start');
58+
buff.write(' Both strings start the same, but the actual value is'
59+
' missing the following trailing characters: ');
60+
_writeTrailing(buff, escapedValue, escapedItem.length);
7661
}
77-
78-
return mismatchDescription.add(buff.toString());
62+
} else {
63+
buff.write('\nExpected: ');
64+
_writeLeading(buff, escapedValue, start);
65+
_writeTrailing(buff, escapedValue, start);
66+
buff.write('\n Actual: ');
67+
_writeLeading(buff, escapedItem, start);
68+
_writeTrailing(buff, escapedItem, start);
69+
buff.write('\n ');
70+
for (var i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' ');
71+
buff.write('^\n Differ at offset $start');
7972
}
73+
74+
return mismatchDescription.add(buff.toString());
8075
}
8176

8277
static void _writeLeading(StringBuffer buff, String s, int start) {

lib/src/feature_matcher.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'core_matchers.dart';
6+
import 'interfaces.dart';
7+
8+
// TODO: export this from the root 'matcher.dart' library
9+
/// A [Matcher] implementation that validates the type of `item` passed to
10+
/// [matches].
11+
abstract class FeatureMatcher<T> extends TypeMatcher<T> {
12+
const FeatureMatcher();
13+
14+
bool matches(item, Map matchState) =>
15+
super.matches(item, matchState) && typedMatches(item, matchState);
16+
17+
bool typedMatches(T item, Map matchState);
18+
19+
Description describeMismatch(
20+
item, Description mismatchDescription, Map matchState, bool verbose) {
21+
if (item is T) {
22+
return describeTypedMismatch(
23+
item, mismatchDescription, matchState, verbose);
24+
}
25+
26+
return super.describe(mismatchDescription.add('not an '));
27+
}
28+
29+
Description describeTypedMismatch(T item, Description mismatchDescription,
30+
Map matchState, bool verbose) =>
31+
mismatchDescription;
32+
}

0 commit comments

Comments
 (0)