kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 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 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 5 | library matcher.core_matchers; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 6 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 7 | import 'description.dart'; |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 8 | import 'interfaces.dart'; |
kevmoo@google.com | e0d6dfd | 2014-07-31 22:50:50 +0000 | [diff] [blame] | 9 | import 'util.dart'; |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 10 | |
Andreas Kirsch | 755ee44 | 2015-01-12 16:00:27 -0800 | [diff] [blame] | 11 | /// Returns a matcher that matches the isEmpty property. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 12 | const Matcher isEmpty = const _Empty(); |
| 13 | |
| 14 | class _Empty extends Matcher { |
| 15 | const _Empty(); |
Andreas Kirsch | 755ee44 | 2015-01-12 16:00:27 -0800 | [diff] [blame] | 16 | |
| 17 | bool matches(item, Map matchState) => item.isEmpty; |
| 18 | |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 19 | Description describe(Description description) => description.add('empty'); |
| 20 | } |
| 21 | |
Andreas Kirsch | 755ee44 | 2015-01-12 16:00:27 -0800 | [diff] [blame] | 22 | /// Returns a matcher that matches the isNotEmpty property. |
nweiz@google.com | 831097d | 2014-12-04 21:28:04 +0000 | [diff] [blame] | 23 | const Matcher isNotEmpty = const _NotEmpty(); |
| 24 | |
| 25 | class _NotEmpty extends Matcher { |
| 26 | const _NotEmpty(); |
| 27 | |
Andreas Kirsch | 755ee44 | 2015-01-12 16:00:27 -0800 | [diff] [blame] | 28 | bool matches(item, Map matchState) => item.isNotEmpty; |
nweiz@google.com | 831097d | 2014-12-04 21:28:04 +0000 | [diff] [blame] | 29 | |
| 30 | Description describe(Description description) => description.add('non-empty'); |
| 31 | } |
| 32 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 33 | /// A matcher that matches any null value. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 34 | const Matcher isNull = const _IsNull(); |
| 35 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 36 | /// A matcher that matches any non-null value. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 37 | const Matcher isNotNull = const _IsNotNull(); |
| 38 | |
| 39 | class _IsNull extends Matcher { |
| 40 | const _IsNull(); |
| 41 | bool matches(item, Map matchState) => item == null; |
| 42 | Description describe(Description description) => description.add('null'); |
| 43 | } |
| 44 | |
| 45 | class _IsNotNull extends Matcher { |
| 46 | const _IsNotNull(); |
| 47 | bool matches(item, Map matchState) => item != null; |
| 48 | Description describe(Description description) => description.add('not null'); |
| 49 | } |
| 50 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 51 | /// A matcher that matches the Boolean value true. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 52 | const Matcher isTrue = const _IsTrue(); |
| 53 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 54 | /// A matcher that matches anything except the Boolean value true. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 55 | const Matcher isFalse = const _IsFalse(); |
| 56 | |
| 57 | class _IsTrue extends Matcher { |
| 58 | const _IsTrue(); |
| 59 | bool matches(item, Map matchState) => item == true; |
| 60 | Description describe(Description description) => description.add('true'); |
| 61 | } |
| 62 | |
| 63 | class _IsFalse extends Matcher { |
| 64 | const _IsFalse(); |
| 65 | bool matches(item, Map matchState) => item == false; |
| 66 | Description describe(Description description) => description.add('false'); |
| 67 | } |
| 68 | |
kevmoo@google.com | a77c143 | 2014-07-30 19:12:16 +0000 | [diff] [blame] | 69 | /// A matcher that matches the numeric value NaN. |
| 70 | const Matcher isNaN = const _IsNaN(); |
| 71 | |
| 72 | /// A matcher that matches any non-NaN value. |
| 73 | const Matcher isNotNaN = const _IsNotNaN(); |
| 74 | |
| 75 | class _IsNaN extends Matcher { |
| 76 | const _IsNaN(); |
| 77 | bool matches(item, Map matchState) => double.NAN.compareTo(item) == 0; |
| 78 | Description describe(Description description) => description.add('NaN'); |
| 79 | } |
| 80 | |
| 81 | class _IsNotNaN extends Matcher { |
| 82 | const _IsNotNaN(); |
| 83 | bool matches(item, Map matchState) => double.NAN.compareTo(item) != 0; |
| 84 | Description describe(Description description) => description.add('not NaN'); |
| 85 | } |
| 86 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 87 | /// Returns a matches that matches if the value is the same instance |
| 88 | /// as [expected], using [identical]. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 89 | Matcher same(expected) => new _IsSameAs(expected); |
| 90 | |
| 91 | class _IsSameAs extends Matcher { |
| 92 | final _expected; |
| 93 | const _IsSameAs(this._expected); |
| 94 | bool matches(item, Map matchState) => identical(item, _expected); |
| 95 | // If all types were hashable we could show a hash here. |
| 96 | Description describe(Description description) => |
| 97 | description.add('same instance as ').addDescriptionOf(_expected); |
| 98 | } |
| 99 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 100 | /// Returns a matcher that matches if the value is structurally equal to |
| 101 | /// [expected]. |
| 102 | /// |
| 103 | /// If [expected] is a [Matcher], then it matches using that. Otherwise it tests |
| 104 | /// for equality using `==` on the expected value. |
| 105 | /// |
| 106 | /// For [Iterable]s and [Map]s, this will recursively match the elements. To |
| 107 | /// handle cyclic structures a recursion depth [limit] can be provided. The |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 108 | /// default limit is 100. [Set]s will be compared order-independently. |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 109 | Matcher equals(expected, [int limit = 100]) => expected is String |
| 110 | ? new _StringEqualsMatcher(expected) |
| 111 | : new _DeepMatcher(expected, limit); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 112 | |
| 113 | class _DeepMatcher extends Matcher { |
| 114 | final _expected; |
| 115 | final int _limit; |
| 116 | var count; |
| 117 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 118 | _DeepMatcher(this._expected, [int limit = 1000]) : this._limit = limit; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 119 | |
| 120 | // Returns a pair (reason, location) |
| 121 | List _compareIterables(expected, actual, matcher, depth, location) { |
| 122 | if (actual is! Iterable) return ['is not Iterable', location]; |
| 123 | |
| 124 | var expectedIterator = expected.iterator; |
| 125 | var actualIterator = actual.iterator; |
| 126 | for (var index = 0; ; index++) { |
| 127 | // Advance in lockstep. |
| 128 | var expectedNext = expectedIterator.moveNext(); |
| 129 | var actualNext = actualIterator.moveNext(); |
| 130 | |
| 131 | // If we reached the end of both, we succeeded. |
| 132 | if (!expectedNext && !actualNext) return null; |
| 133 | |
| 134 | // Fail if their lengths are different. |
| 135 | var newLocation = '${location}[${index}]'; |
| 136 | if (!expectedNext) return ['longer than expected', newLocation]; |
| 137 | if (!actualNext) return ['shorter than expected', newLocation]; |
| 138 | |
| 139 | // Match the elements. |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 140 | var rp = matcher( |
| 141 | expectedIterator.current, actualIterator.current, newLocation, depth); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 142 | if (rp != null) return rp; |
| 143 | } |
| 144 | } |
| 145 | |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 146 | List _compareSets(Set expected, actual, matcher, depth, location) { |
| 147 | if (actual is! Iterable) return ['is not Iterable', location]; |
| 148 | actual = actual.toSet(); |
| 149 | |
| 150 | for (var expectedElement in expected) { |
| 151 | if (actual.every((actualElement) => |
| 152 | matcher(expectedElement, actualElement, location, depth) != null)) { |
| 153 | return ['does not contain $expectedElement', location]; |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | if (actual.length > expected.length) { |
| 158 | return ['larger than expected', location]; |
| 159 | } else if (actual.length < expected.length) { |
| 160 | return ['smaller than expected', location]; |
| 161 | } else { |
| 162 | return null; |
| 163 | } |
| 164 | } |
| 165 | |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 166 | List _recursiveMatch(expected, actual, String location, int depth) { |
| 167 | // If the expected value is a matcher, try to match it. |
| 168 | if (expected is Matcher) { |
| 169 | var matchState = {}; |
| 170 | if (expected.matches(actual, matchState)) return null; |
| 171 | |
| 172 | var description = new StringDescription(); |
| 173 | expected.describe(description); |
| 174 | return ['does not match $description', location]; |
| 175 | } else { |
| 176 | // Otherwise, test for equality. |
| 177 | try { |
| 178 | if (expected == actual) return null; |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 179 | } catch (e) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 180 | // TODO(gram): Add a test for this case. |
| 181 | return ['== threw "$e"', location]; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | if (depth > _limit) return ['recursion depth limit exceeded', location]; |
| 186 | |
| 187 | // If _limit is 1 we can only recurse one level into object. |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 188 | if (depth == 0 || _limit > 1) { |
| 189 | if (expected is Set) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 190 | return _compareSets( |
| 191 | expected, actual, _recursiveMatch, depth + 1, location); |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 192 | } else if (expected is Iterable) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 193 | return _compareIterables( |
| 194 | expected, actual, _recursiveMatch, depth + 1, location); |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 195 | } else if (expected is Map) { |
| 196 | if (actual is! Map) return ['expected a map', location]; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 197 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 198 | var err = (expected.length == actual.length) |
| 199 | ? '' |
| 200 | : 'has different length and '; |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 201 | for (var key in expected.keys) { |
| 202 | if (!actual.containsKey(key)) { |
| 203 | return ["${err}is missing map key '$key'", location]; |
| 204 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 205 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 206 | |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 207 | for (var key in actual.keys) { |
| 208 | if (!expected.containsKey(key)) { |
| 209 | return ["${err}has extra map key '$key'", location]; |
| 210 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 211 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 212 | |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 213 | for (var key in expected.keys) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 214 | var rp = _recursiveMatch( |
| 215 | expected[key], actual[key], "${location}['${key}']", depth + 1); |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 216 | if (rp != null) return rp; |
| 217 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 218 | |
nweiz@google.com | 3137e4f | 2014-06-12 20:14:50 +0000 | [diff] [blame] | 219 | return null; |
| 220 | } |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 221 | } |
| 222 | |
| 223 | var description = new StringDescription(); |
| 224 | |
| 225 | // If we have recursed, show the expected value too; if not, expect() will |
| 226 | // show it for us. |
| 227 | if (depth > 0) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 228 | description |
| 229 | .add('was ') |
| 230 | .addDescriptionOf(actual) |
| 231 | .add(' instead of ') |
| 232 | .addDescriptionOf(expected); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 233 | return [description.toString(), location]; |
| 234 | } |
| 235 | |
| 236 | // We're not adding any value to the actual value. |
| 237 | return ["", location]; |
| 238 | } |
| 239 | |
| 240 | String _match(expected, actual, Map matchState) { |
| 241 | var rp = _recursiveMatch(expected, actual, '', 0); |
| 242 | if (rp == null) return null; |
| 243 | var reason; |
| 244 | if (rp[0].length > 0) { |
| 245 | if (rp[1].length > 0) { |
| 246 | reason = "${rp[0]} at location ${rp[1]}"; |
| 247 | } else { |
| 248 | reason = rp[0]; |
| 249 | } |
| 250 | } else { |
| 251 | reason = ''; |
| 252 | } |
| 253 | // Cache the failure reason in the matchState. |
| 254 | addStateInfo(matchState, {'reason': reason}); |
| 255 | return reason; |
| 256 | } |
| 257 | |
| 258 | bool matches(item, Map matchState) => |
| 259 | _match(_expected, item, matchState) == null; |
| 260 | |
| 261 | Description describe(Description description) => |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 262 | description.addDescriptionOf(_expected); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 263 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 264 | Description describeMismatch( |
| 265 | item, Description mismatchDescription, Map matchState, bool verbose) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 266 | var reason = matchState['reason']; |
| 267 | // If we didn't get a good reason, that would normally be a |
| 268 | // simple 'is <value>' message. We only add that if the mismatch |
| 269 | // description is non empty (so we are supplementing the mismatch |
| 270 | // description). |
| 271 | if (reason.length == 0 && mismatchDescription.length > 0) { |
| 272 | mismatchDescription.add('is ').addDescriptionOf(item); |
| 273 | } else { |
| 274 | mismatchDescription.add(reason); |
| 275 | } |
| 276 | return mismatchDescription; |
| 277 | } |
| 278 | } |
| 279 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 280 | /// A special equality matcher for strings. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 281 | class _StringEqualsMatcher extends Matcher { |
| 282 | final String _value; |
| 283 | |
| 284 | _StringEqualsMatcher(this._value); |
| 285 | |
| 286 | bool get showActualValue => true; |
| 287 | |
| 288 | bool matches(item, Map matchState) => _value == item; |
| 289 | |
| 290 | Description describe(Description description) => |
| 291 | description.addDescriptionOf(_value); |
| 292 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 293 | Description describeMismatch( |
| 294 | item, Description mismatchDescription, Map matchState, bool verbose) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 295 | if (item is! String) { |
| 296 | return mismatchDescription.addDescriptionOf(item).add('is not a string'); |
| 297 | } else { |
| 298 | var buff = new StringBuffer(); |
| 299 | buff.write('is different.'); |
Kevin Moore | 3a1ab8c | 2015-02-10 12:45:22 -0800 | [diff] [blame] | 300 | var escapedItem = escape(item); |
| 301 | var escapedValue = escape(_value); |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 302 | int minLength = escapedItem.length < escapedValue.length |
| 303 | ? escapedItem.length |
| 304 | : escapedValue.length; |
Kevin Moore | 4198fe7 | 2015-02-23 19:55:30 -0800 | [diff] [blame^] | 305 | var start = 0; |
| 306 | for (; start < minLength; start++) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 307 | if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) { |
| 308 | break; |
| 309 | } |
| 310 | } |
| 311 | if (start == minLength) { |
| 312 | if (escapedValue.length < escapedItem.length) { |
| 313 | buff.write(' Both strings start the same, but the given value also' |
| 314 | ' has the following trailing characters: '); |
| 315 | _writeTrailing(buff, escapedItem, escapedValue.length); |
| 316 | } else { |
| 317 | buff.write(' Both strings start the same, but the given value is' |
| 318 | ' missing the following trailing characters: '); |
| 319 | _writeTrailing(buff, escapedValue, escapedItem.length); |
| 320 | } |
| 321 | } else { |
| 322 | buff.write('\nExpected: '); |
| 323 | _writeLeading(buff, escapedValue, start); |
| 324 | _writeTrailing(buff, escapedValue, start); |
| 325 | buff.write('\n Actual: '); |
| 326 | _writeLeading(buff, escapedItem, start); |
| 327 | _writeTrailing(buff, escapedItem, start); |
| 328 | buff.write('\n '); |
| 329 | for (int i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' '); |
| 330 | buff.write('^\n Differ at offset $start'); |
| 331 | } |
| 332 | |
| 333 | return mismatchDescription.replace(buff.toString()); |
| 334 | } |
| 335 | } |
| 336 | |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 337 | static void _writeLeading(StringBuffer buff, String s, int start) { |
| 338 | if (start > 10) { |
| 339 | buff.write('... '); |
| 340 | buff.write(s.substring(start - 10, start)); |
| 341 | } else { |
| 342 | buff.write(s.substring(0, start)); |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | static void _writeTrailing(StringBuffer buff, String s, int start) { |
| 347 | if (start + 10 > s.length) { |
| 348 | buff.write(s.substring(start)); |
| 349 | } else { |
| 350 | buff.write(s.substring(start, start + 10)); |
| 351 | buff.write(' ...'); |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 356 | /// A matcher that matches any value. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 357 | const Matcher anything = const _IsAnything(); |
| 358 | |
| 359 | class _IsAnything extends Matcher { |
| 360 | const _IsAnything(); |
| 361 | bool matches(item, Map matchState) => true; |
| 362 | Description describe(Description description) => description.add('anything'); |
| 363 | } |
| 364 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 365 | /// Returns a matcher that matches if an object is an instance |
| 366 | /// of [type] (or a subtype). |
| 367 | /// |
| 368 | /// As types are not first class objects in Dart we can only |
| 369 | /// approximate this test by using a generic wrapper class. |
| 370 | /// |
| 371 | /// For example, to test whether 'bar' is an instance of type |
| 372 | /// 'Foo', we would write: |
| 373 | /// |
| 374 | /// expect(bar, new isInstanceOf<Foo>()); |
| 375 | /// |
| 376 | /// To get better error message, supply a name when creating the |
| 377 | /// Type wrapper; e.g.: |
| 378 | /// |
| 379 | /// expect(bar, new isInstanceOf<Foo>('Foo')); |
| 380 | /// |
| 381 | /// Note that this does not currently work in dart2js; it will |
| 382 | /// match any type, and isNot(new isInstanceof<T>()) will always |
| 383 | /// fail. This is because dart2js currently ignores template type |
| 384 | /// parameters. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 385 | class isInstanceOf<T> extends Matcher { |
| 386 | final String _name; |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 387 | const isInstanceOf([name = 'specified type']) : this._name = name; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 388 | bool matches(obj, Map matchState) => obj is T; |
| 389 | // The description here is lame :-( |
| 390 | Description describe(Description description) => |
| 391 | description.add('an instance of ${_name}'); |
| 392 | } |
| 393 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 394 | /// A matcher that matches a function call against no exception. |
srawlins@google.com | 3bec76d | 2014-10-06 16:23:04 +0000 | [diff] [blame] | 395 | /// |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 396 | /// The function will be called once. Any exceptions will be silently swallowed. |
| 397 | /// The value passed to expect() should be a reference to the function. |
| 398 | /// Note that the function cannot take arguments; to handle this |
| 399 | /// a wrapper will have to be created. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 400 | const Matcher returnsNormally = const _ReturnsNormally(); |
| 401 | |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 402 | class _ReturnsNormally extends Matcher { |
| 403 | const _ReturnsNormally(); |
| 404 | |
| 405 | bool matches(f, Map matchState) { |
| 406 | try { |
| 407 | f(); |
| 408 | return true; |
| 409 | } catch (e, s) { |
| 410 | addStateInfo(matchState, {'exception': e, 'stack': s}); |
| 411 | return false; |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | Description describe(Description description) => |
| 416 | description.add("return normally"); |
| 417 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 418 | Description describeMismatch( |
| 419 | item, Description mismatchDescription, Map matchState, bool verbose) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 420 | mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']); |
| 421 | if (verbose) { |
| 422 | mismatchDescription.add(' at ').add(matchState['stack'].toString()); |
| 423 | } |
| 424 | return mismatchDescription; |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | /* |
| 429 | * Matchers for different exception types. Ideally we should just be able to |
| 430 | * use something like: |
| 431 | * |
| 432 | * final Matcher throwsException = |
| 433 | * const _Throws(const isInstanceOf<Exception>()); |
| 434 | * |
| 435 | * Unfortunately instanceOf is not working with dart2js. |
| 436 | * |
| 437 | * Alternatively, if static functions could be used in const expressions, |
| 438 | * we could use: |
| 439 | * |
| 440 | * bool _isException(x) => x is Exception; |
| 441 | * final Matcher isException = const _Predicate(_isException, "Exception"); |
| 442 | * final Matcher throwsException = const _Throws(isException); |
| 443 | * |
| 444 | * But currently using static functions in const expressions is not supported. |
| 445 | * For now the only solution for all platforms seems to be separate classes |
| 446 | * for each exception type. |
| 447 | */ |
| 448 | |
| 449 | abstract class TypeMatcher extends Matcher { |
| 450 | final String _name; |
| 451 | const TypeMatcher(this._name); |
| 452 | Description describe(Description description) => description.add(_name); |
| 453 | } |
| 454 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 455 | /// A matcher for Map types. |
| 456 | const Matcher isMap = const _IsMap(); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 457 | |
| 458 | class _IsMap extends TypeMatcher { |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 459 | const _IsMap() : super("Map"); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 460 | bool matches(item, Map matchState) => item is Map; |
| 461 | } |
| 462 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 463 | /// A matcher for List types. |
| 464 | const Matcher isList = const _IsList(); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 465 | |
| 466 | class _IsList extends TypeMatcher { |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 467 | const _IsList() : super("List"); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 468 | bool matches(item, Map matchState) => item is List; |
| 469 | } |
| 470 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 471 | /// Returns a matcher that matches if an object has a length property |
| 472 | /// that matches [matcher]. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 473 | Matcher hasLength(matcher) => new _HasLength(wrapMatcher(matcher)); |
| 474 | |
| 475 | class _HasLength extends Matcher { |
| 476 | final Matcher _matcher; |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 477 | const _HasLength([Matcher matcher = null]) : this._matcher = matcher; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 478 | |
| 479 | bool matches(item, Map matchState) { |
| 480 | try { |
| 481 | // This is harmless code that will throw if no length property |
| 482 | // but subtle enough that an optimizer shouldn't strip it out. |
| 483 | if (item.length * item.length >= 0) { |
| 484 | return _matcher.matches(item.length, matchState); |
| 485 | } |
| 486 | } catch (e) {} |
| 487 | return false; |
| 488 | } |
| 489 | |
| 490 | Description describe(Description description) => |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 491 | description.add('an object with length of ').addDescriptionOf(_matcher); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 492 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 493 | Description describeMismatch( |
| 494 | item, Description mismatchDescription, Map matchState, bool verbose) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 495 | try { |
| 496 | // We want to generate a different description if there is no length |
| 497 | // property; we use the same trick as in matches(). |
| 498 | if (item.length * item.length >= 0) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 499 | return mismatchDescription |
| 500 | .add('has length of ') |
| 501 | .addDescriptionOf(item.length); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 502 | } |
| 503 | } catch (e) {} |
| 504 | return mismatchDescription.add('has no length property'); |
| 505 | } |
| 506 | } |
| 507 | |
srawlins@google.com | 3bec76d | 2014-10-06 16:23:04 +0000 | [diff] [blame] | 508 | /// Returns a matcher that matches if the match argument contains the expected |
| 509 | /// value. |
| 510 | /// |
| 511 | /// For [String]s this means substring matching; |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 512 | /// for [Map]s it means the map has the key, and for [Iterable]s |
srawlins@google.com | 3bec76d | 2014-10-06 16:23:04 +0000 | [diff] [blame] | 513 | /// it means the iterable has a matching element. In the case of iterables, |
| 514 | /// [expected] can itself be a matcher. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 515 | Matcher contains(expected) => new _Contains(expected); |
| 516 | |
| 517 | class _Contains extends Matcher { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 518 | final _expected; |
| 519 | |
| 520 | const _Contains(this._expected); |
| 521 | |
| 522 | bool matches(item, Map matchState) { |
| 523 | if (item is String) { |
| 524 | return item.indexOf(_expected) >= 0; |
| 525 | } else if (item is Iterable) { |
| 526 | if (_expected is Matcher) { |
| 527 | return item.any((e) => _expected.matches(e, matchState)); |
| 528 | } else { |
| 529 | return item.contains(_expected); |
| 530 | } |
| 531 | } else if (item is Map) { |
| 532 | return item.containsKey(_expected); |
| 533 | } |
| 534 | return false; |
| 535 | } |
| 536 | |
| 537 | Description describe(Description description) => |
| 538 | description.add('contains ').addDescriptionOf(_expected); |
| 539 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 540 | Description describeMismatch( |
| 541 | item, Description mismatchDescription, Map matchState, bool verbose) { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 542 | if (item is String || item is Iterable || item is Map) { |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 543 | return super.describeMismatch( |
| 544 | item, mismatchDescription, matchState, verbose); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 545 | } else { |
| 546 | return mismatchDescription.add('is not a string, map or iterable'); |
| 547 | } |
| 548 | } |
| 549 | } |
| 550 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 551 | /// Returns a matcher that matches if the match argument is in |
| 552 | /// the expected value. This is the converse of [contains]. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 553 | Matcher isIn(expected) => new _In(expected); |
| 554 | |
| 555 | class _In extends Matcher { |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 556 | final _expected; |
| 557 | |
| 558 | const _In(this._expected); |
| 559 | |
| 560 | bool matches(item, Map matchState) { |
| 561 | if (_expected is String) { |
| 562 | return _expected.indexOf(item) >= 0; |
| 563 | } else if (_expected is Iterable) { |
| 564 | return _expected.any((e) => e == item); |
| 565 | } else if (_expected is Map) { |
| 566 | return _expected.containsKey(item); |
| 567 | } |
| 568 | return false; |
| 569 | } |
| 570 | |
| 571 | Description describe(Description description) => |
| 572 | description.add('is in ').addDescriptionOf(_expected); |
| 573 | } |
| 574 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 575 | /// Returns a matcher that uses an arbitrary function that returns |
srawlins@google.com | 3bec76d | 2014-10-06 16:23:04 +0000 | [diff] [blame] | 576 | /// true or false for the actual value. |
| 577 | /// |
| 578 | /// For example: |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 579 | /// |
| 580 | /// expect(v, predicate((x) => ((x % 2) == 0), "is even")) |
| 581 | Matcher predicate(bool f(value), [String description = 'satisfies function']) => |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 582 | new _Predicate(f, description); |
| 583 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 584 | typedef bool _PredicateFunction(value); |
kevmoo@google.com | 1495cd1 | 2014-06-03 07:10:01 +0000 | [diff] [blame] | 585 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 586 | class _Predicate extends Matcher { |
| 587 | final _PredicateFunction _matcher; |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 588 | final String _description; |
| 589 | |
| 590 | const _Predicate(this._matcher, this._description); |
| 591 | |
| 592 | bool matches(item, Map matchState) => _matcher(item); |
| 593 | |
| 594 | Description describe(Description description) => |
| 595 | description.add(_description); |
| 596 | } |
| 597 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 598 | /// A useful utility class for implementing other matchers through inheritance. |
| 599 | /// Derived classes should call the base constructor with a feature name and |
| 600 | /// description, and an instance matcher, and should implement the |
| 601 | /// [featureValueOf] abstract method. |
| 602 | /// |
| 603 | /// The feature description will typically describe the item and the feature, |
| 604 | /// while the feature name will just name the feature. For example, we may |
| 605 | /// have a Widget class where each Widget has a price; we could make a |
| 606 | /// [CustomMatcher] that can make assertions about prices with: |
| 607 | /// |
| 608 | /// class HasPrice extends CustomMatcher { |
| 609 | /// const HasPrice(matcher) : |
| 610 | /// super("Widget with price that is", "price", matcher); |
| 611 | /// featureValueOf(actual) => actual.price; |
| 612 | /// } |
| 613 | /// |
| 614 | /// and then use this for example like: |
| 615 | /// |
| 616 | /// expect(inventoryItem, new HasPrice(greaterThan(0))); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 617 | class CustomMatcher extends Matcher { |
| 618 | final String _featureDescription; |
| 619 | final String _featureName; |
| 620 | final Matcher _matcher; |
| 621 | |
| 622 | CustomMatcher(this._featureDescription, this._featureName, matcher) |
| 623 | : this._matcher = wrapMatcher(matcher); |
| 624 | |
kevmoo@google.com | 94a9bcf | 2014-06-05 17:11:03 +0000 | [diff] [blame] | 625 | /// Override this to extract the interesting feature. |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 626 | featureValueOf(actual) => actual; |
| 627 | |
| 628 | bool matches(item, Map matchState) { |
| 629 | var f = featureValueOf(item); |
| 630 | if (_matcher.matches(f, matchState)) return true; |
| 631 | addStateInfo(matchState, {'feature': f}); |
| 632 | return false; |
| 633 | } |
| 634 | |
| 635 | Description describe(Description description) => |
| 636 | description.add(_featureDescription).add(' ').addDescriptionOf(_matcher); |
| 637 | |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 638 | Description describeMismatch( |
| 639 | item, Description mismatchDescription, Map matchState, bool verbose) { |
| 640 | mismatchDescription |
| 641 | .add('has ') |
| 642 | .add(_featureName) |
| 643 | .add(' with value ') |
| 644 | .addDescriptionOf(matchState['feature']); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 645 | var innerDescription = new StringDescription(); |
Kevin Moore | 0d23f15 | 2015-01-14 14:28:53 -0800 | [diff] [blame] | 646 | _matcher.describeMismatch( |
| 647 | matchState['feature'], innerDescription, matchState['state'], verbose); |
kevmoo@google.com | f9c7742 | 2014-03-21 23:50:50 +0000 | [diff] [blame] | 648 | if (innerDescription.length > 0) { |
| 649 | mismatchDescription.add(' which ').add(innerDescription.toString()); |
| 650 | } |
| 651 | return mismatchDescription; |
| 652 | } |
| 653 | } |