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

Commit d1bcd54

Browse files
committed
fix: Improve the output of many matchers that expect specific types
- Add a package-private FeatureMatcher class to generalize type checking - Use it across many of the existing Matcher implementations - Update tests validate new, more consistent failure messages - Add a few new tests for `isIn`
1 parent 4ff649f commit d1bcd54

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 'type_matcher.dart';
78
import 'util.dart';
@@ -70,15 +71,17 @@ const Matcher isNaN = const _IsNaN();
7071
/// A matcher that matches any non-NaN value.
7172
const Matcher isNotNaN = const _IsNotNaN();
7273

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

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

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

131-
class _ReturnsNormally extends Matcher {
134+
class _ReturnsNormally extends FeatureMatcher<Function> {
132135
const _ReturnsNormally();
133136

134-
bool matches(f, Map matchState) {
137+
bool typedMatches(Function f, Map matchState) {
135138
try {
136139
f();
137140
return true;
@@ -144,8 +147,8 @@ class _ReturnsNormally extends Matcher {
144147
Description describe(Description description) =>
145148
description.add("return normally");
146149

147-
Description describeMismatch(
148-
item, Description mismatchDescription, Map matchState, bool verbose) {
150+
Description describeTypedMismatch(Function item,
151+
Description mismatchDescription, Map matchState, bool verbose) {
149152
mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
150153
if (verbose) {
151154
mismatchDescription.add(' at ').add(matchState['stack'].toString());
@@ -216,11 +219,12 @@ class _Contains extends Matcher {
216219
const _Contains(this._expected);
217220

218221
bool matches(item, Map matchState) {
222+
var expected = _expected;
219223
if (item is String) {
220-
return item.contains((_expected as Pattern));
224+
return expected is Pattern && item.contains(expected);
221225
} else if (item is Iterable) {
222-
if (_expected is Matcher) {
223-
return item.any((e) => (_expected as Matcher).matches(e, matchState));
226+
if (expected is Matcher) {
227+
return item.any((e) => expected.matches(e, matchState));
224228
} else {
225229
return item.contains(_expected);
226230
}
@@ -246,27 +250,29 @@ class _Contains extends Matcher {
246250

247251
/// Returns a matcher that matches if the match argument is in
248252
/// the expected value. This is the converse of [contains].
249-
Matcher isIn(expected) => new _In(expected);
253+
Matcher isIn(expected) {
254+
if (expected is Iterable) {
255+
return new _In(expected, expected.contains);
256+
} else if (expected is String) {
257+
return new _In<Pattern>(expected, expected.contains);
258+
} else if (expected is Map) {
259+
return new _In(expected, expected.containsKey);
260+
}
250261

251-
class _In extends Matcher {
252-
final Object _expected;
262+
throw new ArgumentError.value(
263+
expected, 'expected', 'Only Iterable, Map, and String are supported.');
264+
}
253265

254-
const _In(this._expected);
266+
class _In<T> extends FeatureMatcher<T> {
267+
final Object _source;
268+
final bool Function(T) _containsFunction;
255269

256-
bool matches(item, Map matchState) {
257-
var expected = _expected;
258-
if (expected is String) {
259-
return expected.contains(item as Pattern);
260-
} else if (expected is Iterable) {
261-
return expected.contains(item);
262-
} else if (expected is Map) {
263-
return expected.containsKey(item);
264-
}
265-
return false;
266-
}
270+
const _In(this._source, this._containsFunction);
271+
272+
bool typedMatches(T item, Map matchState) => _containsFunction(item);
267273

268274
Description describe(Description description) =>
269-
description.add('is in ').addDescriptionOf(_expected);
275+
description.add('is in ').addDescriptionOf(_source);
270276
}
271277

272278
/// Returns a matcher that uses an arbitrary function that returns
@@ -281,13 +287,13 @@ Matcher predicate<T>(bool f(T value),
281287

282288
typedef bool _PredicateFunction<T>(T value);
283289

284-
class _Predicate<T> extends Matcher {
290+
class _Predicate<T> extends FeatureMatcher<T> {
285291
final _PredicateFunction<T> _matcher;
286292
final String _description;
287293

288294
_Predicate(this._matcher, this._description);
289295

290-
bool matches(item, Map matchState) => _matcher(item as T);
296+
bool typedMatches(T item, Map matchState) => _matcher(item);
291297

292298
Description describe(Description description) =>
293299
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 'interfaces.dart';
6+
import 'type_matcher.dart';
7+
8+
/// A package-private [TypeMatcher] implementation that makes it easy for
9+
/// subclasses to validate aspects of specific types while providing consistent
10+
/// type checking.
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)