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

Side by Side Diff: pkg/analysis_server/test/integration/integration_tests.dart

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

Powered by Google App Engine
This is Rietveld 408576698