Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(174)

Side by Side Diff: pkg/analyzer_plugin/test/integration/support/integration_tests.dart

Issue 2664213003: Add the generator and the generated files (Closed)
Patch Set: add missed files Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2017, 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 'dart:async';
6 import 'dart:collection';
7 import 'dart:convert';
8 import 'dart:io';
9
10 import 'package:analyzer_plugin/protocol/generated_protocol.dart';
11 import 'package:path/path.dart';
12 import 'package:test/test.dart';
13
14 import 'integration_test_methods.dart';
15 import 'protocol_matchers.dart';
16
17 const Matcher isBool = const isInstanceOf<bool>();
18
19 const Matcher isInt = const isInstanceOf<int>();
20
21 const Matcher isNotification = const MatchesJsonObject(
22 'notification', const {'event': isString},
23 optionalFields: const {'params': isMap});
24
25 const Matcher isObject = isMap;
26
27 const Matcher isString = const isInstanceOf<String>();
28
29 final Matcher isResponse = new MatchesJsonObject('response', {'id': isString},
30 optionalFields: {'result': anything, 'error': isRequestError});
31
32 Matcher isListOf(Matcher elementMatcher) => new _ListOf(elementMatcher);
33
34 Matcher isMapOf(Matcher keyMatcher, Matcher valueMatcher) =>
35 new _MapOf(keyMatcher, valueMatcher);
36
37 Matcher isOneOf(List<Matcher> choiceMatchers) => new _OneOf(choiceMatchers);
38
39 /**
40 * Assert that [actual] matches [matcher].
41 */
42 void outOfTestExpect(actual, matcher,
43 {String reason, skip, bool verbose: false}) {
44 var matchState = {};
45 try {
46 if (matcher.matches(actual, matchState)) return;
47 } catch (e, trace) {
48 if (reason == null) {
49 reason = '${(e is String) ? e : e.toString()} at $trace';
50 }
51 }
52 fail(_defaultFailFormatter(actual, matcher, reason, matchState, verbose));
53 }
54
55 String _defaultFailFormatter(
56 actual, Matcher matcher, String reason, Map matchState, bool verbose) {
57 var description = new StringDescription();
58 description.add('Expected: ').addDescriptionOf(matcher).add('\n');
59 description.add(' Actual: ').addDescriptionOf(actual).add('\n');
60
61 var mismatchDescription = new StringDescription();
62 matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
63
64 if (mismatchDescription.length > 0) {
65 description.add(' Which: $mismatchDescription\n');
66 }
67 if (reason != null) description.add(reason).add('\n');
68 return description.toString();
69 }
70
71 /**
72 * Type of closures used by LazyMatcher.
73 */
74 typedef Matcher MatcherCreator();
75
76 /**
77 * Type of closures used by MatchesJsonObject to record field mismatches.
78 */
79 typedef Description MismatchDescriber(Description mismatchDescription);
80
81 /**
82 * Type of callbacks used to process notifications.
83 */
84 typedef void NotificationProcessor(String event, params);
85
86 /**
87 * Base class for analysis server integration tests.
88 */
89 abstract class AbstractAnalysisServerIntegrationTest
90 extends IntegrationTestMixin {
91 /**
92 * Amount of time to give the server to respond to a shutdown request before
93 * forcibly terminating it.
94 */
95 static const Duration SHUTDOWN_TIMEOUT = const Duration(seconds: 5);
96
97 /**
98 * Connection to the analysis server.
99 */
100 @override
101 final Server server = new Server();
102
103 /**
104 * Temporary directory in which source files can be stored.
105 */
106 Directory sourceDirectory;
107
108 /**
109 * Map from file path to the list of analysis errors which have most recently
110 * been received for the file.
111 */
112 HashMap<String, List<AnalysisError>> currentAnalysisErrors =
113 new HashMap<String, List<AnalysisError>>();
114
115 /**
116 * True if the teardown process should skip sending a "server.shutdown"
117 * request (e.g. because the server is known to have already shutdown).
118 */
119 bool skipShutdown = false;
120
121 AbstractAnalysisServerIntegrationTest() {
122 initializeInttestMixin();
123 }
124
125 // /**
126 // * Return a future which will complete when a 'server.status' notification i s
127 // * received from the server with 'analyzing' set to false.
128 // *
129 // * The future will only be completed by 'server.status' notifications that a re
130 // * received after this function call. So it is safe to use this getter
131 // * multiple times in one test; each time it is used it will wait afresh for
132 // * analysis to finish.
133 // */
134 // Future get analysisFinished {
135 // Completer completer = new Completer();
136 // StreamSubscription subscription;
137 // // This will only work if the caller has already subscribed to
138 // // SERVER_STATUS (e.g. using sendServerSetSubscriptions(['STATUS']))
139 // outOfTestExpect(_subscribedToServerStatus, isTrue);
140 // subscription = onServerStatus.listen((PluginStatusParams params) {
141 // if (params.analysis != null && !params.analysis.isAnalyzing) {
142 // completer.complete(params);
143 // subscription.cancel();
144 // }
145 // });
146 // return completer.future;
147 // }
148
149 /**
150 * Print out any messages exchanged with the server. If some messages have
151 * already been exchanged with the server, they are printed out immediately.
152 */
153 void debugStdio() {
154 server.debugStdio();
155 }
156
157 /**
158 * The server is automatically started before every test, and a temporary
159 * [sourceDirectory] is created.
160 */
161 Future setUp() {
162 sourceDirectory = Directory.systemTemp.createTempSync('analysisServer');
163
164 onAnalysisErrors.listen((AnalysisErrorsParams params) {
165 currentAnalysisErrors[params.file] = params.errors;
166 });
167 Completer serverConnected = new Completer();
168 // TODO(brianwilkerson) Implement this.
169 // onServerConnected.listen((_) {
170 // outOfTestExpect(serverConnected.isCompleted, isFalse);
171 // serverConnected.complete();
172 // });
173 onPluginError.listen((PluginErrorParams params) {
174 // A plugin error should never happen during an integration test.
175 fail('${params.message}\n${params.stackTrace}');
176 });
177 return startServer().then((_) {
178 server.listenToOutput(dispatchNotification);
179 server.exitCode.then((_) {
180 skipShutdown = true;
181 });
182 return serverConnected.future;
183 });
184 }
185
186 /**
187 * If [skipShutdown] is not set, shut down the server.
188 */
189 Future shutdownIfNeeded() {
190 if (skipShutdown) {
191 return new Future.value();
192 }
193 // Give the server a short time to comply with the shutdown request; if it
194 // doesn't exit, then forcibly terminate it.
195 sendPluginShutdown();
196 return server.exitCode.timeout(SHUTDOWN_TIMEOUT, onTimeout: () {
197 return server.kill('server failed to exit');
198 });
199 }
200
201 /**
202 * Convert the given [relativePath] to an absolute path, by interpreting it
203 * relative to [sourceDirectory]. On Windows any forward slashes in
204 * [relativePath] are converted to backslashes.
205 */
206 String sourcePath(String relativePath) {
207 return join(sourceDirectory.path, relativePath.replaceAll('/', separator));
208 }
209
210 /**
211 * Send the server an 'analysis.setAnalysisRoots' command directing it to
212 * analyze [sourceDirectory]. If [subscribeStatus] is true (the default),
213 * then also enable [SERVER_STATUS] notifications so that [analysisFinished]
214 * can be used.
215 */
216 Future standardAnalysisSetup({bool subscribeStatus: true}) {
217 List<Future> futures = <Future>[];
218 // TODO(brianwilkerson) Implement this.
219 // if (subscribeStatus) {
220 // futures.add(sendServerSetSubscriptions([ServerService.STATUS]));
221 // }
222 // futures.add(sendAnalysisSetAnalysisRoots([sourceDirectory.path], []));
223 return Future.wait(futures);
224 }
225
226 /**
227 * Start [server].
228 */
229 Future startServer(
230 {bool checked: true, int diagnosticPort, int servicesPort}) =>
231 server.start(
232 checked: checked,
233 diagnosticPort: diagnosticPort,
234 servicesPort: servicesPort);
235
236 /**
237 * After every test, the server is stopped and [sourceDirectory] is deleted.
238 */
239 Future tearDown() {
240 return shutdownIfNeeded().then((_) {
241 sourceDirectory.deleteSync(recursive: true);
242 });
243 }
244
245 /**
246 * Write a source file with the given absolute [pathname] and [contents].
247 *
248 * If the file didn't previously exist, it is created. If it did, it is
249 * overwritten.
250 *
251 * Parent directories are created as necessary.
252 *
253 * Return a normalized path to the file (with symbolic links resolved).
254 */
255 String writeFile(String pathname, String contents) {
256 new Directory(dirname(pathname)).createSync(recursive: true);
257 File file = new File(pathname);
258 file.writeAsStringSync(contents);
259 return file.resolveSymbolicLinksSync();
260 }
261 }
262
263 /**
264 * Wrapper class for Matcher which doesn't create the underlying Matcher object
265 * until it is needed. This is necessary in order to create matchers that can
266 * refer to themselves (so that recursive data structures can be represented).
267 */
268 class LazyMatcher implements Matcher {
269 /**
270 * Callback that will be used to create the matcher the first time it is
271 * needed.
272 */
273 final MatcherCreator _creator;
274
275 /**
276 * The matcher returned by [_creator], if it has already been called.
277 * Otherwise null.
278 */
279 Matcher _wrappedMatcher;
280
281 LazyMatcher(this._creator);
282
283 @override
284 Description describe(Description description) {
285 _createMatcher();
286 return _wrappedMatcher.describe(description);
287 }
288
289 @override
290 Description describeMismatch(
291 item, Description mismatchDescription, Map matchState, bool verbose) {
292 _createMatcher();
293 return _wrappedMatcher.describeMismatch(
294 item, mismatchDescription, matchState, verbose);
295 }
296
297 @override
298 bool matches(item, Map matchState) {
299 _createMatcher();
300 return _wrappedMatcher.matches(item, matchState);
301 }
302
303 /**
304 * Create the wrapped matcher object, if it hasn't been created already.
305 */
306 void _createMatcher() {
307 if (_wrappedMatcher == null) {
308 _wrappedMatcher = _creator();
309 }
310 }
311 }
312
313 /**
314 * Matcher that matches a String drawn from a limited set.
315 */
316 class MatchesEnum extends Matcher {
317 /**
318 * Short description of the expected type.
319 */
320 final String description;
321
322 /**
323 * The set of enum values that are allowed.
324 */
325 final List<String> allowedValues;
326
327 const MatchesEnum(this.description, this.allowedValues);
328
329 @override
330 Description describe(Description description) =>
331 description.add(this.description);
332
333 @override
334 bool matches(item, Map matchState) {
335 return allowedValues.contains(item);
336 }
337 }
338
339 /**
340 * Matcher that matches a JSON object, with a given set of required and
341 * optional fields, and their associated types (expressed as [Matcher]s).
342 */
343 class MatchesJsonObject extends _RecursiveMatcher {
344 /**
345 * Short description of the expected type.
346 */
347 final String description;
348
349 /**
350 * Fields that are required to be in the JSON object, and [Matcher]s describin g
351 * their expected types.
352 */
353 final Map<String, Matcher> requiredFields;
354
355 /**
356 * Fields that are optional in the JSON object, and [Matcher]s describing
357 * their expected types.
358 */
359 final Map<String, Matcher> optionalFields;
360
361 const MatchesJsonObject(this.description, this.requiredFields,
362 {this.optionalFields});
363
364 @override
365 Description describe(Description description) =>
366 description.add(this.description);
367
368 @override
369 void populateMismatches(item, List<MismatchDescriber> mismatches) {
370 if (item is! Map) {
371 mismatches.add(simpleDescription('is not a map'));
372 return;
373 }
374 if (requiredFields != null) {
375 requiredFields.forEach((String key, Matcher valueMatcher) {
376 if (!item.containsKey(key)) {
377 mismatches.add((Description mismatchDescription) =>
378 mismatchDescription
379 .add('is missing field ')
380 .addDescriptionOf(key)
381 .add(' (')
382 .addDescriptionOf(valueMatcher)
383 .add(')'));
384 } else {
385 _checkField(key, item[key], valueMatcher, mismatches);
386 }
387 });
388 }
389 item.forEach((key, value) {
390 if (requiredFields != null && requiredFields.containsKey(key)) {
391 // Already checked this field
392 } else if (optionalFields != null && optionalFields.containsKey(key)) {
393 _checkField(key, value, optionalFields[key], mismatches);
394 } else {
395 mismatches.add((Description mismatchDescription) => mismatchDescription
396 .add('has unexpected field ')
397 .addDescriptionOf(key));
398 }
399 });
400 }
401
402 /**
403 * Check the type of a field called [key], having value [value], using
404 * [valueMatcher]. If it doesn't match, record a closure in [mismatches]
405 * which can describe the mismatch.
406 */
407 void _checkField(String key, value, Matcher valueMatcher,
408 List<MismatchDescriber> mismatches) {
409 checkSubstructure(
410 value,
411 valueMatcher,
412 mismatches,
413 (Description description) =>
414 description.add('field ').addDescriptionOf(key));
415 }
416 }
417
418 /**
419 * Instances of the class [Server] manage a connection to a server process, and
420 * facilitate communication to and from the server.
421 */
422 class Server {
423 /**
424 * Server process object, or null if server hasn't been started yet.
425 */
426 Process _process = null;
427
428 /**
429 * Commands that have been sent to the server but not yet acknowledged, and
430 * the [Completer] objects which should be completed when acknowledgement is
431 * received.
432 */
433 final Map<String, Completer> _pendingCommands = <String, Completer>{};
434
435 /**
436 * Number which should be used to compute the 'id' to send in the next command
437 * sent to the server.
438 */
439 int _nextId = 0;
440
441 /**
442 * Messages which have been exchanged with the server; we buffer these
443 * up until the test finishes, so that they can be examined in the debugger
444 * or printed out in response to a call to [debugStdio].
445 */
446 final List<String> _recordedStdio = <String>[];
447
448 /**
449 * True if we are currently printing out messages exchanged with the server.
450 */
451 bool _debuggingStdio = false;
452
453 /**
454 * True if we've received bad data from the server, and we are aborting the
455 * test.
456 */
457 bool _receivedBadDataFromServer = false;
458
459 /**
460 * Stopwatch that we use to generate timing information for debug output.
461 */
462 Stopwatch _time = new Stopwatch();
463
464 /**
465 * The [currentElapseTime] at which the last communication was received from t he server
466 * or `null` if no communication has been received.
467 */
468 double lastCommunicationTime;
469
470 /**
471 * The current elapse time (seconds) since the server was started.
472 */
473 double get currentElapseTime => _time.elapsedTicks / _time.frequency;
474
475 /**
476 * Future that completes when the server process exits.
477 */
478 Future<int> get exitCode => _process.exitCode;
479
480 /**
481 * Print out any messages exchanged with the server. If some messages have
482 * already been exchanged with the server, they are printed out immediately.
483 */
484 void debugStdio() {
485 if (_debuggingStdio) {
486 return;
487 }
488 _debuggingStdio = true;
489 for (String line in _recordedStdio) {
490 print(line);
491 }
492 }
493
494 /**
495 * Find the root directory of the analysis_server package by proceeding
496 * upward to the 'test' dir, and then going up one more directory.
497 */
498 String findRoot(String pathname) {
499 while (!['benchmark', 'test'].contains(basename(pathname))) {
500 String parent = dirname(pathname);
501 if (parent.length >= pathname.length) {
502 throw new Exception("Can't find root directory");
503 }
504 pathname = parent;
505 }
506 return dirname(pathname);
507 }
508
509 /**
510 * Return a future that will complete when all commands that have been sent
511 * to the server so far have been flushed to the OS buffer.
512 */
513 Future flushCommands() {
514 return _process.stdin.flush();
515 }
516
517 /**
518 * Stop the server.
519 */
520 Future kill(String reason) {
521 debugStdio();
522 _recordStdio('FORCIBLY TERMINATING PROCESS: $reason');
523 _process.kill();
524 return _process.exitCode;
525 }
526
527 /**
528 * Start listening to output from the server, and deliver notifications to
529 * [notificationProcessor].
530 */
531 void listenToOutput(NotificationProcessor notificationProcessor) {
532 _process.stdout
533 .transform((new Utf8Codec()).decoder)
534 .transform(new LineSplitter())
535 .listen((String line) {
536 lastCommunicationTime = currentElapseTime;
537 String trimmedLine = line.trim();
538 if (trimmedLine.startsWith('Observatory listening on ')) {
539 return;
540 }
541 _recordStdio('RECV: $trimmedLine');
542 var message;
543 try {
544 message = JSON.decoder.convert(trimmedLine);
545 } catch (exception) {
546 _badDataFromServer('JSON decode failure: $exception');
547 return;
548 }
549 outOfTestExpect(message, isMap);
550 Map messageAsMap = message;
551 if (messageAsMap.containsKey('id')) {
552 outOfTestExpect(messageAsMap['id'], isString);
553 String id = message['id'];
554 Completer completer = _pendingCommands[id];
555 if (completer == null) {
556 fail('Unexpected response from server: id=$id');
557 } else {
558 _pendingCommands.remove(id);
559 }
560 if (messageAsMap.containsKey('error')) {
561 // TODO(paulberry): propagate the error info to the completer.
562 completer.completeError(new UnimplementedError(
563 'Server responded with an error: ${JSON.encode(message)}'));
564 } else {
565 completer.complete(messageAsMap['result']);
566 }
567 // Check that the message is well-formed. We do this after calling
568 // completer.complete() or completer.completeError() so that we don't
569 // stall the test in the event of an error.
570 outOfTestExpect(message, isResponse);
571 } else {
572 // Message is a notification. It should have an event and possibly
573 // params.
574 outOfTestExpect(messageAsMap, contains('event'));
575 outOfTestExpect(messageAsMap['event'], isString);
576 notificationProcessor(messageAsMap['event'], messageAsMap['params']);
577 // Check that the message is well-formed. We do this after calling
578 // notificationController.add() so that we don't stall the test in the
579 // event of an error.
580 outOfTestExpect(message, isNotification);
581 }
582 });
583 _process.stderr
584 .transform((new Utf8Codec()).decoder)
585 .transform(new LineSplitter())
586 .listen((String line) {
587 String trimmedLine = line.trim();
588 _recordStdio('ERR: $trimmedLine');
589 _badDataFromServer('Message received on stderr', silent: true);
590 });
591 }
592
593 /**
594 * Send a command to the server. An 'id' will be automatically assigned.
595 * The returned [Future] will be completed when the server acknowledges the
596 * command with a response. If the server acknowledges the command with a
597 * normal (non-error) response, the future will be completed with the 'result'
598 * field from the response. If the server acknowledges the command with an
599 * error response, the future will be completed with an error.
600 */
601 Future send(String method, Map<String, dynamic> params) {
602 String id = '${_nextId++}';
603 Map<String, dynamic> command = <String, dynamic>{
604 'id': id,
605 'method': method
606 };
607 if (params != null) {
608 command['params'] = params;
609 }
610 Completer completer = new Completer();
611 _pendingCommands[id] = completer;
612 String line = JSON.encode(command);
613 _recordStdio('SEND: $line');
614 _process.stdin.add(UTF8.encoder.convert("$line\n"));
615 return completer.future;
616 }
617
618 /**
619 * Start the server. If [debugServer] is `true`, the server will be started
620 * with "--debug", allowing a debugger to be attached. If [profileServer] is
621 * `true`, the server will be started with "--observe" and
622 * "--pause-isolates-on-exit", allowing the observatory to be used.
623 */
624 Future start(
625 {bool checked: true,
626 bool debugServer: false,
627 int diagnosticPort,
628 bool profileServer: false,
629 String sdkPath,
630 int servicesPort,
631 bool useAnalysisHighlight2: false}) {
632 if (_process != null) {
633 throw new Exception('Process already started');
634 }
635 _time.start();
636 String dartBinary = Platform.executable;
637 String rootDir =
638 findRoot(Platform.script.toFilePath(windows: Platform.isWindows));
639 String serverPath = normalize(join(rootDir, 'bin', 'server.dart'));
640 List<String> arguments = [];
641 //
642 // Add VM arguments.
643 //
644 if (debugServer) {
645 arguments.add('--debug');
646 }
647 if (profileServer) {
648 if (servicesPort == null) {
649 arguments.add('--observe');
650 } else {
651 arguments.add('--observe=$servicesPort');
652 }
653 arguments.add('--pause-isolates-on-exit');
654 } else if (servicesPort != null) {
655 arguments.add('--enable-vm-service=$servicesPort');
656 }
657 if (Platform.packageRoot != null) {
658 arguments.add('--package-root=${Platform.packageRoot}');
659 }
660 if (Platform.packageConfig != null) {
661 arguments.add('--packages=${Platform.packageConfig}');
662 }
663 if (checked) {
664 arguments.add('--checked');
665 }
666 //
667 // Add the server executable.
668 //
669 arguments.add(serverPath);
670 //
671 // Add server arguments.
672 //
673 if (diagnosticPort != null) {
674 arguments.add('--port');
675 arguments.add(diagnosticPort.toString());
676 }
677 if (sdkPath != null) {
678 arguments.add('--sdk=$sdkPath');
679 }
680 if (useAnalysisHighlight2) {
681 arguments.add('--useAnalysisHighlight2');
682 }
683 // print('Launching $serverPath');
684 // print('$dartBinary ${arguments.join(' ')}');
685 return Process.start(dartBinary, arguments).then((Process process) {
686 _process = process;
687 process.exitCode.then((int code) {
688 if (code != 0) {
689 _badDataFromServer('server terminated with exit code $code');
690 }
691 });
692 });
693 }
694
695 /**
696 * Deal with bad data received from the server.
697 */
698 void _badDataFromServer(String details, {bool silent: false}) {
699 if (!silent) {
700 _recordStdio('BAD DATA FROM SERVER: $details');
701 }
702 if (_receivedBadDataFromServer) {
703 // We're already dealing with it.
704 return;
705 }
706 _receivedBadDataFromServer = true;
707 debugStdio();
708 // Give the server 1 second to continue outputting bad data before we kill
709 // the test. This is helpful if the server has had an unhandled exception
710 // and is outputting a stacktrace, because it ensures that we see the
711 // entire stacktrace. Use expectAsync() to prevent the test from
712 // ending during this 1 second.
713 new Future.delayed(new Duration(seconds: 1), expectAsync(() {
714 fail('Bad data received from server: $details');
715 }));
716 }
717
718 /**
719 * Record a message that was exchanged with the server, and print it out if
720 * [debugStdio] has been called.
721 */
722 void _recordStdio(String line) {
723 double elapsedTime = currentElapseTime;
724 line = "$elapsedTime: $line";
725 if (_debuggingStdio) {
726 print(line);
727 }
728 _recordedStdio.add(line);
729 }
730 }
731
732 /**
733 * Matcher that matches a list of objects, each of which satisfies the given
734 * matcher.
735 */
736 class _ListOf extends Matcher {
737 /**
738 * Matcher which every element of the list must satisfy.
739 */
740 final Matcher elementMatcher;
741
742 /**
743 * Iterable matcher which we use to test the contents of the list.
744 */
745 final Matcher iterableMatcher;
746
747 _ListOf(elementMatcher)
748 : elementMatcher = elementMatcher,
749 iterableMatcher = everyElement(elementMatcher);
750
751 @override
752 Description describe(Description description) =>
753 description.add('List of ').addDescriptionOf(elementMatcher);
754
755 @override
756 Description describeMismatch(
757 item, Description mismatchDescription, Map matchState, bool verbose) {
758 if (item is! List) {
759 return super
760 .describeMismatch(item, mismatchDescription, matchState, verbose);
761 } else {
762 return iterableMatcher.describeMismatch(
763 item, mismatchDescription, matchState, verbose);
764 }
765 }
766
767 @override
768 bool matches(item, Map matchState) {
769 if (item is! List) {
770 return false;
771 }
772 return iterableMatcher.matches(item, matchState);
773 }
774 }
775
776 /**
777 * Matcher that matches a map of objects, where each key/value pair in the
778 * map satisies the given key and value matchers.
779 */
780 class _MapOf extends _RecursiveMatcher {
781 /**
782 * Matcher which every key in the map must satisfy.
783 */
784 final Matcher keyMatcher;
785
786 /**
787 * Matcher which every value in the map must satisfy.
788 */
789 final Matcher valueMatcher;
790
791 _MapOf(this.keyMatcher, this.valueMatcher);
792
793 @override
794 Description describe(Description description) => description
795 .add('Map from ')
796 .addDescriptionOf(keyMatcher)
797 .add(' to ')
798 .addDescriptionOf(valueMatcher);
799
800 @override
801 void populateMismatches(item, List<MismatchDescriber> mismatches) {
802 if (item is! Map) {
803 mismatches.add(simpleDescription('is not a map'));
804 return;
805 }
806 item.forEach((key, value) {
807 checkSubstructure(
808 key,
809 keyMatcher,
810 mismatches,
811 (Description description) =>
812 description.add('key ').addDescriptionOf(key));
813 checkSubstructure(
814 value,
815 valueMatcher,
816 mismatches,
817 (Description description) =>
818 description.add('field ').addDescriptionOf(key));
819 });
820 }
821 }
822
823 /**
824 * Matcher that matches a union of different types, each of which is described
825 * by a matcher.
826 */
827 class _OneOf extends Matcher {
828 /**
829 * Matchers for the individual choices.
830 */
831 final List<Matcher> choiceMatchers;
832
833 _OneOf(this.choiceMatchers);
834
835 @override
836 Description describe(Description description) {
837 for (int i = 0; i < choiceMatchers.length; i++) {
838 if (i != 0) {
839 if (choiceMatchers.length == 2) {
840 description = description.add(' or ');
841 } else {
842 description = description.add(', ');
843 if (i == choiceMatchers.length - 1) {
844 description = description.add('or ');
845 }
846 }
847 }
848 description = description.addDescriptionOf(choiceMatchers[i]);
849 }
850 return description;
851 }
852
853 @override
854 bool matches(item, Map matchState) {
855 for (Matcher choiceMatcher in choiceMatchers) {
856 Map subState = {};
857 if (choiceMatcher.matches(item, subState)) {
858 return true;
859 }
860 }
861 return false;
862 }
863 }
864
865 /**
866 * Base class for matchers that operate by recursing through the contents of
867 * an object.
868 */
869 abstract class _RecursiveMatcher extends Matcher {
870 const _RecursiveMatcher();
871
872 /**
873 * Check the type of a substructure whose value is [item], using [matcher].
874 * If it doesn't match, record a closure in [mismatches] which can describe
875 * the mismatch. [describeSubstructure] is used to describe which
876 * substructure did not match.
877 */
878 checkSubstructure(item, Matcher matcher, List<MismatchDescriber> mismatches,
879 Description describeSubstructure(Description)) {
880 Map subState = {};
881 if (!matcher.matches(item, subState)) {
882 mismatches.add((Description mismatchDescription) {
883 mismatchDescription = mismatchDescription.add('contains malformed ');
884 mismatchDescription = describeSubstructure(mismatchDescription);
885 mismatchDescription =
886 mismatchDescription.add(' (should be ').addDescriptionOf(matcher);
887 String subDescription = matcher
888 .describeMismatch(item, new StringDescription(), subState, false)
889 .toString();
890 if (subDescription.isNotEmpty) {
891 mismatchDescription =
892 mismatchDescription.add('; ').add(subDescription);
893 }
894 return mismatchDescription.add(')');
895 });
896 }
897 }
898
899 @override
900 Description describeMismatch(
901 item, Description mismatchDescription, Map matchState, bool verbose) {
902 List<MismatchDescriber> mismatches =
903 matchState['mismatches'] as List<MismatchDescriber>;
904 if (mismatches != null) {
905 for (int i = 0; i < mismatches.length; i++) {
906 MismatchDescriber mismatch = mismatches[i];
907 if (i > 0) {
908 if (mismatches.length == 2) {
909 mismatchDescription = mismatchDescription.add(' and ');
910 } else if (i == mismatches.length - 1) {
911 mismatchDescription = mismatchDescription.add(', and ');
912 } else {
913 mismatchDescription = mismatchDescription.add(', ');
914 }
915 }
916 mismatchDescription = mismatch(mismatchDescription);
917 }
918 return mismatchDescription;
919 } else {
920 return super
921 .describeMismatch(item, mismatchDescription, matchState, verbose);
922 }
923 }
924
925 @override
926 bool matches(item, Map matchState) {
927 List<MismatchDescriber> mismatches = <MismatchDescriber>[];
928 populateMismatches(item, mismatches);
929 if (mismatches.isEmpty) {
930 return true;
931 } else {
932 addStateInfo(matchState, {'mismatches': mismatches});
933 return false;
934 }
935 }
936
937 /**
938 * Populate [mismatches] with descriptions of all the ways in which [item]
939 * does not match.
940 */
941 void populateMismatches(item, List<MismatchDescriber> mismatches);
942
943 /**
944 * Create a [MismatchDescriber] describing a mismatch with a simple string.
945 */
946 MismatchDescriber simpleDescription(String description) =>
947 (Description mismatchDescription) {
948 mismatchDescription.add(description);
949 };
950 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698