Line data Source code
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 'feature_matcher.dart';
6 : import 'interfaces.dart';
7 : import 'util.dart';
8 :
9 : /// Returns a matcher that matches if the value is structurally equal to
10 : /// [expected].
11 : ///
12 : /// If [expected] is a [Matcher], then it matches using that. Otherwise it tests
13 : /// for equality using `==` on the expected value.
14 : ///
15 : /// For [Iterable]s and [Map]s, this will recursively match the elements. To
16 : /// handle cyclic structures a recursion depth [limit] can be provided. The
17 : /// default limit is 100. [Set]s will be compared order-independently.
18 16 : Matcher equals(Object? expected, [int limit = 100]) => expected is String
19 7 : ? _StringEqualsMatcher(expected)
20 5 : : _DeepMatcher(expected, limit);
21 :
22 : typedef _RecursiveMatcher = _Mismatch? Function(Object?, Object?, String, int);
23 :
24 : /// A special equality matcher for strings.
25 : class _StringEqualsMatcher extends FeatureMatcher<String> {
26 : final String _value;
27 :
28 7 : _StringEqualsMatcher(this._value);
29 :
30 7 : @override
31 14 : bool typedMatches(String item, Map matchState) => _value == item;
32 :
33 0 : @override
34 : Description describe(Description description) =>
35 0 : description.addDescriptionOf(_value);
36 :
37 0 : @override
38 : Description describeTypedMismatch(String item,
39 : Description mismatchDescription, Map matchState, bool verbose) {
40 0 : var buff = StringBuffer();
41 0 : buff.write('is different.');
42 0 : var escapedItem = escape(item);
43 0 : var escapedValue = escape(_value);
44 0 : var minLength = escapedItem.length < escapedValue.length
45 0 : ? escapedItem.length
46 0 : : escapedValue.length;
47 : var start = 0;
48 0 : for (; start < minLength; start++) {
49 0 : if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
50 : break;
51 : }
52 : }
53 0 : if (start == minLength) {
54 0 : if (escapedValue.length < escapedItem.length) {
55 0 : buff.write(' Both strings start the same, but the actual value also'
56 : ' has the following trailing characters: ');
57 0 : _writeTrailing(buff, escapedItem, escapedValue.length);
58 : } else {
59 0 : buff.write(' Both strings start the same, but the actual value is'
60 : ' missing the following trailing characters: ');
61 0 : _writeTrailing(buff, escapedValue, escapedItem.length);
62 : }
63 : } else {
64 0 : buff.write('\nExpected: ');
65 0 : _writeLeading(buff, escapedValue, start);
66 0 : _writeTrailing(buff, escapedValue, start);
67 0 : buff.write('\n Actual: ');
68 0 : _writeLeading(buff, escapedItem, start);
69 0 : _writeTrailing(buff, escapedItem, start);
70 0 : buff.write('\n ');
71 0 : for (var i = start > 10 ? 14 : start; i > 0; i--) {
72 0 : buff.write(' ');
73 : }
74 0 : buff.write('^\n Differ at offset $start');
75 : }
76 :
77 0 : return mismatchDescription.add(buff.toString());
78 : }
79 :
80 0 : static void _writeLeading(StringBuffer buff, String s, int start) {
81 0 : if (start > 10) {
82 0 : buff.write('... ');
83 0 : buff.write(s.substring(start - 10, start));
84 : } else {
85 0 : buff.write(s.substring(0, start));
86 : }
87 : }
88 :
89 0 : static void _writeTrailing(StringBuffer buff, String s, int start) {
90 0 : if (start + 10 > s.length) {
91 0 : buff.write(s.substring(start));
92 : } else {
93 0 : buff.write(s.substring(start, start + 10));
94 0 : buff.write(' ...');
95 : }
96 : }
97 : }
98 :
99 : class _DeepMatcher extends Matcher {
100 : final Object? _expected;
101 : final int _limit;
102 :
103 5 : _DeepMatcher(this._expected, [int limit = 1000]) : _limit = limit;
104 :
105 0 : _Mismatch? _compareIterables(Iterable expected, Object? actual,
106 : _RecursiveMatcher matcher, int depth, String location) {
107 0 : if (actual is Iterable) {
108 0 : var expectedIterator = expected.iterator;
109 0 : var actualIterator = actual.iterator;
110 0 : for (var index = 0;; index++) {
111 : // Advance in lockstep.
112 0 : var expectedNext = expectedIterator.moveNext();
113 0 : var actualNext = actualIterator.moveNext();
114 :
115 : // If we reached the end of both, we succeeded.
116 : if (!expectedNext && !actualNext) return null;
117 :
118 : // Fail if their lengths are different.
119 0 : var newLocation = '$location[$index]';
120 : if (!expectedNext) {
121 0 : return _Mismatch.simple(newLocation, actual, 'longer than expected');
122 : }
123 : if (!actualNext) {
124 0 : return _Mismatch.simple(newLocation, actual, 'shorter than expected');
125 : }
126 :
127 : // Match the elements.
128 0 : var rp = matcher(expectedIterator.current, actualIterator.current,
129 : newLocation, depth);
130 : if (rp != null) return rp;
131 : }
132 : } else {
133 0 : return _Mismatch.simple(location, actual, 'is not Iterable');
134 : }
135 : }
136 :
137 0 : _Mismatch? _compareSets(Set expected, Object? actual,
138 : _RecursiveMatcher matcher, int depth, String location) {
139 0 : if (actual is Iterable) {
140 0 : var other = actual.toSet();
141 :
142 0 : for (var expectedElement in expected) {
143 0 : if (other.every((actualElement) =>
144 : matcher(expectedElement, actualElement, location, depth) != null)) {
145 0 : return _Mismatch(
146 : location,
147 : actual,
148 0 : (description, verbose) => description
149 0 : .add('does not contain ')
150 0 : .addDescriptionOf(expectedElement));
151 : }
152 : }
153 :
154 0 : if (other.length > expected.length) {
155 0 : return _Mismatch.simple(location, actual, 'larger than expected');
156 0 : } else if (other.length < expected.length) {
157 0 : return _Mismatch.simple(location, actual, 'smaller than expected');
158 : } else {
159 : return null;
160 : }
161 : } else {
162 0 : return _Mismatch.simple(location, actual, 'is not Iterable');
163 : }
164 : }
165 :
166 5 : _Mismatch? _recursiveMatch(
167 : Object? expected, Object? actual, String location, int depth) {
168 : // If the expected value is a matcher, try to match it.
169 5 : if (expected is Matcher) {
170 0 : var matchState = {};
171 0 : if (expected.matches(actual, matchState)) return null;
172 0 : return _Mismatch(location, actual, (description, verbose) {
173 0 : var oldLength = description.length;
174 0 : expected.describeMismatch(actual, description, matchState, verbose);
175 0 : if (depth > 0 && description.length == oldLength) {
176 0 : description.add('does not match ');
177 0 : expected.describe(description);
178 : }
179 : });
180 : } else {
181 : // Otherwise, test for equality.
182 : try {
183 5 : if (expected == actual) return null;
184 : } catch (e) {
185 : // TODO(gram): Add a test for this case.
186 0 : return _Mismatch(
187 : location,
188 : actual,
189 0 : (description, verbose) =>
190 0 : description.add('== threw ').addDescriptionOf(e));
191 : }
192 : }
193 :
194 10 : if (depth > _limit) {
195 0 : return _Mismatch.simple(
196 : location, actual, 'recursion depth limit exceeded');
197 : }
198 :
199 : // If _limit is 1 we can only recurse one level into object.
200 5 : if (depth == 0 || _limit > 1) {
201 5 : if (expected is Set) {
202 0 : return _compareSets(
203 0 : expected, actual, _recursiveMatch, depth + 1, location);
204 5 : } else if (expected is Iterable) {
205 0 : return _compareIterables(
206 0 : expected, actual, _recursiveMatch, depth + 1, location);
207 5 : } else if (expected is Map) {
208 0 : if (actual is! Map) {
209 0 : return _Mismatch.simple(location, actual, 'expected a map');
210 : }
211 0 : var err = (expected.length == actual.length)
212 : ? ''
213 : : 'has different length and ';
214 0 : for (var key in expected.keys) {
215 0 : if (!actual.containsKey(key)) {
216 0 : return _Mismatch(
217 : location,
218 : actual,
219 0 : (description, verbose) => description
220 0 : .add('${err}is missing map key ')
221 0 : .addDescriptionOf(key));
222 : }
223 : }
224 :
225 0 : for (var key in actual.keys) {
226 0 : if (!expected.containsKey(key)) {
227 0 : return _Mismatch(
228 : location,
229 : actual,
230 0 : (description, verbose) => description
231 0 : .add('${err}has extra map key ')
232 0 : .addDescriptionOf(key));
233 : }
234 : }
235 :
236 0 : for (var key in expected.keys) {
237 0 : var rp = _recursiveMatch(
238 0 : expected[key], actual[key], "$location['$key']", depth + 1);
239 : if (rp != null) return rp;
240 : }
241 :
242 : return null;
243 : }
244 : }
245 :
246 : // If we have recursed, show the expected value too; if not, expect() will
247 : // show it for us.
248 5 : if (depth > 0) {
249 0 : return _Mismatch(location, actual,
250 0 : (description, verbose) => description.addDescriptionOf(expected),
251 : instead: true);
252 : } else {
253 5 : return _Mismatch(location, actual, null);
254 : }
255 : }
256 :
257 5 : @override
258 : bool matches(Object? actual, Map matchState) {
259 10 : var mismatch = _recursiveMatch(_expected, actual, '', 0);
260 : if (mismatch == null) return true;
261 10 : addStateInfo(matchState, {'mismatch': mismatch});
262 : return false;
263 : }
264 :
265 0 : @override
266 : Description describe(Description description) =>
267 0 : description.addDescriptionOf(_expected);
268 :
269 0 : @override
270 : Description describeMismatch(Object? item, Description mismatchDescription,
271 : Map matchState, bool verbose) {
272 0 : var mismatch = matchState['mismatch'] as _Mismatch;
273 0 : var describeProblem = mismatch.describeProblem;
274 0 : if (mismatch.location.isNotEmpty) {
275 : mismatchDescription
276 0 : .add('at location ')
277 0 : .add(mismatch.location)
278 0 : .add(' is ')
279 0 : .addDescriptionOf(mismatch.actual);
280 : if (describeProblem != null) {
281 : mismatchDescription
282 0 : .add(' ${mismatch.instead ? 'instead of' : 'which'} ');
283 : describeProblem(mismatchDescription, verbose);
284 : }
285 : } else {
286 : // If we didn't get a good reason, that would normally be a
287 : // simple 'is <value>' message. We only add that if the mismatch
288 : // description is non empty (so we are supplementing the mismatch
289 : // description).
290 : if (describeProblem == null) {
291 0 : if (mismatchDescription.length > 0) {
292 0 : mismatchDescription.add('is ').addDescriptionOf(item);
293 : }
294 : } else {
295 : describeProblem(mismatchDescription, verbose);
296 : }
297 : }
298 : return mismatchDescription;
299 : }
300 : }
301 :
302 : class _Mismatch {
303 : /// A human-readable description of the location within the collection where
304 : /// the mismatch occurred.
305 : final String location;
306 :
307 : /// The actual value found at [location].
308 : final Object? actual;
309 :
310 : /// Callback that can create a detailed description of the problem.
311 : final void Function(Description, bool verbose)? describeProblem;
312 :
313 : /// If `true`, [describeProblem] describes the expected value, so when the
314 : /// final mismatch description is pieced together, it will be preceded by
315 : /// `instead of` (e.g. `at location [2] is <3> instead of <4>`). If `false`,
316 : /// [describeProblem] is a problem description from a sub-matcher, so when the
317 : /// final mismatch description is pieced together, it will be preceded by
318 : /// `which` (e.g. `at location [2] is <foo> which has length of 3`).
319 : final bool instead;
320 :
321 5 : _Mismatch(this.location, this.actual, this.describeProblem,
322 : {this.instead = false});
323 :
324 0 : _Mismatch.simple(this.location, this.actual, String problem,
325 : {this.instead = false})
326 0 : : describeProblem = ((description, verbose) => description.add(problem));
327 : }
|