OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, 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 | |
6 /** | |
7 * Returns a matcher that matches empty strings, maps or collections. | |
8 */ | |
9 final IMatcher isEmpty = const _Empty(); | |
10 | |
11 class _Empty extends Matcher { | |
12 const _Empty(); | |
13 bool matches(item) { | |
14 if (item is Map || item is Collection) | |
Bob Nystrom
2012/06/01 18:22:23
{} for multi-line ifs.
gram
2012/06/01 22:22:00
Done.
| |
15 return item.isEmpty(); | |
16 else if (item is String) | |
17 return item.length == 0; | |
18 else | |
19 return false; | |
20 } | |
21 IDescription describe(IDescription description) => | |
22 description.add('empty'); | |
23 } | |
24 | |
25 /** A matcher that matches any null value. */ | |
26 final IMatcher isNull = const _IsNull(); | |
27 | |
28 /** A matcher that matches any non-null value. */ | |
29 final IMatcher isNotNull = const _IsNotNull(); | |
30 | |
31 class _IsNull extends Matcher { | |
32 const _IsNull(); | |
33 bool matches(item) => item == null; | |
34 IDescription describe(IDescription description) => | |
35 description.add('null'); | |
36 } | |
37 | |
38 class _IsNotNull extends Matcher { | |
39 const _IsNotNull(); | |
40 bool matches(item) => item != null; | |
41 IDescription describe(IDescription description) => | |
42 description.add('not null'); | |
43 } | |
44 | |
45 /** A matcher that matches the Boolean value true. */ | |
46 final IMatcher isTrue = const _IsTrue(); | |
47 | |
48 /** A matcher that matches anything except the Boolean value true. */ | |
49 final IMatcher isFalse = const _IsFalse(); | |
50 | |
51 class _IsTrue extends Matcher { | |
52 const _IsTrue(); | |
53 bool matches(item) => item == true; | |
54 IDescription describe(IDescription description) => | |
55 description.add('true'); | |
56 } | |
57 | |
58 class _IsFalse extends Matcher { | |
59 const _IsFalse(); | |
60 bool matches(item) => item != true; | |
61 IDescription describe(IDescription description) => | |
62 description.add('false'); | |
63 } | |
64 | |
65 /** | |
66 * Returns a matches that matches if the value is the same instance | |
67 * as [object] (`===`). | |
68 */ | |
69 IMatcher same(expected) => new _IsSameAs(expected); | |
70 | |
71 class _IsSameAs extends Matcher { | |
72 final _expected; | |
73 const _IsSameAs(this._expected); | |
74 bool matches(item) => item === _expected; | |
75 // If all types were hashable we could show a hash here. | |
76 IDescription describe(IDescription description) => | |
77 description.add('same instance as ').addDescriptionOf(_expected); | |
78 } | |
79 | |
80 /** Returns a matcher that matches if two objects are equal (==). */ | |
81 IMatcher equals(expected) => new _IsEqual(expected); | |
82 | |
83 class _IsEqual extends Matcher { | |
84 final _expected; | |
85 const _IsEqual(this._expected); | |
86 bool matches(item) => item == _expected; | |
87 IDescription describe(IDescription description) => | |
88 description.addDescriptionOf(_expected); | |
89 } | |
90 | |
91 /** A matcher that matches any value. */ | |
92 final IMatcher anything = const _IsAnything(); | |
93 | |
94 class _IsAnything extends Matcher { | |
95 const _IsAnything(); | |
96 bool matches(item) => true; | |
97 IDescription describe(IDescription description) => | |
98 description.add('anything'); | |
99 } | |
100 | |
101 /** | |
102 * A matcher that matches functions that throw exceptions when called. | |
103 * The value passed to expect() should be a reference to the function. | |
104 * Note that the function cannot take arguments; to handle this | |
105 * a wrapper will have to be created. | |
106 * The function will be called once upon success, or twice upon failure | |
107 * (the second time to get the failure description). | |
108 */ | |
109 final IMatcher throws = const _Throws(); | |
110 | |
111 /** | |
112 * Returns a matcher that matches a function call against an exception, | |
113 * which is in turn constrained by a [matcher]. | |
114 * The value passed to expect() should be a reference to the function. | |
115 * Note that the function cannot take arguments; to handle this | |
116 * a wrapper will have to be created. | |
117 * The function will be called once upon success, or twice upon failure | |
118 * (the second time to get the failure description). | |
119 */ | |
120 IMatcher throwsA(IMatcher matcher) => new _Throws(matcher); | |
Bob Nystrom
2012/06/01 18:22:23
Nice name. :)
gram
2012/06/01 22:22:00
Done.
| |
121 | |
122 /** | |
123 * A matcher that matches a function call against no exception. | |
124 * The function will be called once. Any exceptions will be silently swallowed. | |
125 * The value passed to expect() should be a reference to the function. | |
126 * Note that the function cannot take arguments; to handle this | |
127 * a wrapper will have to be created. | |
128 */ | |
129 final IMatcher returnsNormally = const _ReturnsNormally(); | |
130 | |
131 class _Throws extends Matcher { | |
132 final IMatcher _matcher; | |
133 | |
134 const _Throws([IMatcher this._matcher = null]); | |
135 | |
136 bool matches(item) { | |
137 try { | |
138 item(); | |
139 return false; | |
140 } catch (final e) { | |
141 return _matcher == null || _matcher.matches(e); | |
142 } | |
143 } | |
144 | |
145 IDescription describe(IDescription description) { | |
146 if (_matcher == null) { | |
147 return description.add("throws an exception"); | |
148 } else { | |
149 return description.add('throws an exception which matches '). | |
150 addDescriptionOf(_matcher); | |
151 } | |
152 } | |
153 | |
154 IDescription describeMismatch(item, IDescription mismatchDescription) { | |
155 try { | |
156 item(); | |
157 return mismatchDescription.add(' no exception'); | |
158 } catch (final e) { | |
159 return mismatchDescription.add(' exception does not match '). | |
160 addDescriptionOf(_matcher); | |
161 } | |
162 } | |
163 } | |
164 | |
165 class _ReturnsNormally extends Matcher { | |
166 | |
167 const _ReturnsNormally(); | |
168 | |
169 bool matches(f) { | |
170 try { | |
171 f(); | |
172 return true; | |
173 } catch (final e) { | |
174 return false; | |
175 } | |
176 } | |
177 | |
178 IDescription describe(IDescription description) => | |
179 description.add("return normally"); | |
180 | |
181 IDescription describeMismatch(item, IDescription mismatchDescription) { | |
182 return mismatchDescription.add(' threw exception'); | |
183 } | |
184 } | |
185 | |
186 /** | |
187 * Returns a matcher that matches if an object is an instance | |
188 * of [type] (or a subtype). | |
189 * | |
190 * As types are not first class objects in Dart we can only | |
191 * approximate this test by using a generic wrapper class. | |
192 * | |
193 * For example, to test whether 'bar' is an instance of type | |
194 * 'Foo', we would write: | |
195 * | |
196 * expect(bar, new isInstanceOf<Foo>()); | |
197 * | |
198 * To get better error message, supply a name when creating the | |
199 * Type wrapper; e.g.: | |
200 * | |
201 * expect(bar, new isInstanceOf<Foo>('Foo')); | |
202 */ | |
203 class isInstanceOf<T> extends Matcher { | |
204 final String _name; | |
205 const isInstanceOf([this._name = 'specified type']); | |
206 bool matches(obj) => obj is T; | |
207 // The description here is lame :-( | |
208 IDescription describe(IDescription description) => | |
209 description.add('an instance of ${_name}'); | |
210 } | |
211 | |
212 /** | |
213 * Returns a matcher that matches if an object has a length property | |
214 * that matches [matcher]. | |
215 */ | |
216 IMatcher hasLength(IMatcher matcher) => | |
217 new _HasLength(wrapMatcher(matcher)); | |
218 | |
219 // TODO(gram) - this is intended to fail if the value | |
220 // does not have a length property but it doesn't. See if this | |
221 // can be fixed. | |
222 class _HasLength extends Matcher { | |
223 final IMatcher _matcher; | |
224 const _HasLength([IMatcher this._matcher = null]); | |
225 | |
226 bool matches(item) { | |
227 try { | |
228 // TODO(gram): this hack to throw if no length property is not | |
229 // working | |
230 if ((item.length * item.length) >= 0) { | |
231 return _matcher.matches(item.length); | |
232 } | |
233 } catch(var e) { | |
234 return false; | |
235 } | |
236 } | |
237 | |
238 IDescription describe(IDescription description) => | |
239 description.add('an object with length of '). | |
240 addDescriptionOf(_matcher); | |
241 | |
242 IDescription describeMismatch(item, IDescription mismatchDescription) { | |
243 super.describeMismatch(item, mismatchDescription); | |
244 try { | |
245 // TODO(gram): this hack to throw if no length property is not | |
246 // working | |
247 if ((item.length * item.length) >= 0) { | |
248 return mismatchDescription.add(' with length of '). | |
249 addDescriptionOf(item.length); | |
250 } | |
251 } catch (var e) { | |
252 return mismatchDescription.add(' has no length property'); | |
253 } | |
254 } | |
255 } | |
256 | |
257 /** | |
258 * Returns a matcher that does a deep recursive match. This only works | |
259 * with scalars, Maps and Iterables. To handle cyclic structures an | |
260 * item [limit] can be provided; if after [limit] items have been | |
261 * compared and the process is not complete this will be treated as | |
262 * a mismatch. The default limit is 1000. | |
263 */ | |
264 IMatcher recursivelyMatches(expected) => new _DeepMatcher(expected); | |
265 | |
266 class _DeepMatcher extends Matcher { | |
267 final _expected; | |
268 final int _limit; | |
269 var count; | |
270 | |
271 _DeepMatcher(this._expected, [this._limit = 1000]); | |
272 | |
273 String _recursiveMatch(expected, actual, String location) { | |
274 String reason = null; | |
275 if (++count >= _limit) { | |
276 return 'item comparison limit exceeded ${location}'; | |
277 } else if (expected is Iterable) { | |
278 if (actual is !Iterable) { | |
279 reason = 'expected an Iterable'; | |
280 } else { | |
281 var expectedIterator = expected.iterator(); | |
282 var actualIterator = actual.iterator(); | |
283 var position = 0; | |
284 while (reason == null) { | |
285 if (expectedIterator.hasNext()) { | |
Bob Nystrom
2012/06/01 18:22:23
collection_matchers has a near identical chunk of
gram
2012/06/01 22:22:00
Done.
| |
286 if (actualIterator.hasNext()) { | |
287 reason = _recursiveMatch(expectedIterator.next(), | |
288 actualIterator.next(), | |
289 'at position ${position} ${location}'); | |
290 if (reason != null) { | |
291 break; | |
292 } | |
293 ++position; | |
294 } else { | |
295 reason = 'shorter than expected ${location}'; | |
296 } | |
297 } else if (actualIterator.hasNext()) { | |
298 reason = 'longer than expected ${location}'; | |
299 } else { | |
300 break; | |
301 } | |
302 } | |
303 } | |
304 } else if (expected is Map) { | |
305 if (actual is !Map) { | |
306 reason = 'expected a map'; | |
307 } else if (expected.length != actual.length) { | |
308 reason = 'different map lengths'; | |
309 } else { | |
310 for (var key in expected.getKeys()) { | |
311 if (!actual.containsKey(key)) { | |
312 reason = 'missing map key ${key}'; | |
313 break; | |
314 } | |
315 reason = _recursiveMatch(expected[key], actual[key], | |
316 'with key ${key} ${location}'); | |
317 if (reason != null) { | |
318 break; | |
319 } | |
320 } | |
321 } | |
322 } else if (expected != actual) { | |
323 reason = 'expected ${expected} but got ${actual}'; | |
324 } | |
325 if (reason == null) { | |
326 return null; | |
327 } else { | |
328 return '${reason} ${location}'; | |
329 } | |
330 } | |
331 | |
332 String _match(expected, actual) { | |
333 count = 0; | |
334 return _recursiveMatch(expected, actual, ''); | |
335 } | |
336 | |
337 bool matches(item) => _match(_expected, item) == null; | |
338 | |
339 IDescription describe(IDescription description) => | |
340 description.add('recursively matches ').addDescriptionOf(_expected); | |
341 | |
342 IDescription describeMismatch(item, IDescription mismatchDescription) => | |
343 mismatchDescription.add(_match(_expected, item)); | |
344 } | |
345 | |
346 /** | |
347 * Returns a matcher that matches if the match argument contains | |
348 * the expected value. For [String]s this means substring matching; | |
349 * for [Map]s is means the map has the key, and for [Collection]s it | |
350 * means the collection has a matching element. | |
351 */ | |
352 IMatcher contains(expected) => new _Contains(expected); | |
Bob Nystrom
2012/06/01 18:22:23
For the non-string case, can expected be a matcher
gram
2012/06/01 22:22:00
Good idea!
| |
353 | |
354 class _Contains extends Matcher { | |
355 | |
356 final _expected; | |
357 | |
358 const _Contains(this._expected); | |
359 | |
360 bool matches(item) { | |
361 if (item is String) { | |
362 return item.indexOf(_expected) >= 0; | |
363 } else if (item is Collection) { | |
364 return item.some((e) => e == _expected); | |
365 } else if (item is Map) { | |
366 return item.containsKey(_expected); | |
367 } | |
368 return false; | |
369 } | |
370 | |
371 IDescription describe(IDescription description) => | |
372 description.add('contains ').addDescriptionOf(_expected); | |
373 } | |
OLD | NEW |