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

Commit f81b17f

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 2fc1ab2 commit f81b17f

13 files changed

+268
-287
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
2121
- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
2222
23+
- Improved the output of `Matcher` instances that fail due to type errors.
24+
2325
## 0.12.2+1
2426
2527
- 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

@@ -122,10 +125,10 @@ class isInstanceOf<T> extends TypeMatcher<T> {
122125
/// a wrapper will have to be created.
123126
const Matcher returnsNormally = const _ReturnsNormally();
124127

125-
class _ReturnsNormally extends Matcher {
128+
class _ReturnsNormally extends FeatureMatcher<Function> {
126129
const _ReturnsNormally();
127130

128-
bool matches(f, Map matchState) {
131+
bool typedMatches(Function f, Map matchState) {
129132
try {
130133
f();
131134
return true;
@@ -138,8 +141,8 @@ class _ReturnsNormally extends Matcher {
138141
Description describe(Description description) =>
139142
description.add("return normally");
140143

141-
Description describeMismatch(
142-
item, Description mismatchDescription, Map matchState, bool verbose) {
144+
Description describeTypedMismatch(Function item,
145+
Description mismatchDescription, Map matchState, bool verbose) {
143146
mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
144147
if (verbose) {
145148
mismatchDescription.add(' at ').add(matchState['stack'].toString());
@@ -210,11 +213,12 @@ class _Contains extends Matcher {
210213
const _Contains(this._expected);
211214

212215
bool matches(item, Map matchState) {
216+
var expected = _expected;
213217
if (item is String) {
214-
return item.contains((_expected as Pattern));
218+
return expected is Pattern && item.contains(expected);
215219
} else if (item is Iterable) {
216-
if (_expected is Matcher) {
217-
return item.any((e) => (_expected as Matcher).matches(e, matchState));
220+
if (expected is Matcher) {
221+
return item.any((e) => expected.matches(e, matchState));
218222
} else {
219223
return item.contains(_expected);
220224
}
@@ -240,27 +244,29 @@ class _Contains extends Matcher {
240244

241245
/// Returns a matcher that matches if the match argument is in
242246
/// the expected value. This is the converse of [contains].
243-
Matcher isIn(expected) => new _In(expected);
247+
Matcher isIn(expected) {
248+
if (expected is Iterable) {
249+
return new _In(expected, expected.contains);
250+
} else if (expected is String) {
251+
return new _In<Pattern>(expected, expected.contains);
252+
} else if (expected is Map) {
253+
return new _In(expected, expected.containsKey);
254+
}
244255

245-
class _In extends Matcher {
246-
final Object _expected;
256+
throw new ArgumentError.value(
257+
expected, 'expected', 'Only Iterable, Map, and String are supported.');
258+
}
247259

248-
const _In(this._expected);
260+
class _In<T> extends FeatureMatcher<T> {
261+
final Object _source;
262+
final bool Function(T) _containsFunction;
249263

250-
bool matches(item, Map matchState) {
251-
var expected = _expected;
252-
if (expected is String) {
253-
return expected.contains(item as Pattern);
254-
} else if (expected is Iterable) {
255-
return expected.contains(item);
256-
} else if (expected is Map) {
257-
return expected.containsKey(item);
258-
}
259-
return false;
260-
}
264+
const _In(this._source, this._containsFunction);
265+
266+
bool typedMatches(T item, Map matchState) => _containsFunction(item);
261267

262268
Description describe(Description description) =>
263-
description.add('is in ').addDescriptionOf(_expected);
269+
description.add('is in ').addDescriptionOf(_source);
264270
}
265271

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

276282
typedef bool _PredicateFunction<T>(T value);
277283

278-
class _Predicate<T> extends Matcher {
284+
class _Predicate<T> extends FeatureMatcher<T> {
279285
final _PredicateFunction<T> _matcher;
280286
final String _description;
281287

282288
_Predicate(this._matcher, this._description);
283289

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

286292
Description describe(Description description) =>
287293
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)