OLD | NEW |
| (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 } | |
OLD | NEW |