OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library test.integration.analysis; | 5 library test.integration.analysis; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection'; | 8 import 'dart:collection'; |
9 import 'dart:convert'; | 9 import 'dart:convert'; |
10 import 'dart:io'; | 10 import 'dart:io'; |
11 | 11 |
12 import 'package:analysis_server/src/constants.dart'; | 12 import 'package:analysis_server/src/constants.dart'; |
13 import 'package:path/path.dart'; | 13 import 'package:path/path.dart'; |
14 import 'package:unittest/unittest.dart'; | 14 import 'package:unittest/unittest.dart'; |
15 | 15 |
16 /** | 16 /** |
17 * Base class for analysis server integration tests. | 17 * Base class for analysis server integration tests. |
18 */ | 18 */ |
19 abstract class AbstractAnalysisServerIntegrationTest { | 19 abstract class AbstractAnalysisServerIntegrationTest { |
20 /** | 20 /** |
21 * Amount of time to give the server to respond to a shutdown request before | 21 * Amount of time to give the server to respond to a shutdown request before |
22 * forcibly terminating it. | 22 * forcibly terminating it. |
23 * | |
24 * TODO(paulberry): the extra-long timeout (20s) is because sometimes the | |
25 * buildbots are slow to spawn a new analysis server process. It would be | |
26 * better to wait for the initial "server.connected" message with a long | |
27 * timeout, and keep this timeout short. | |
28 * | |
29 */ | 23 */ |
30 static const Duration SHUTDOWN_TIMEOUT = const Duration(seconds: 20); | 24 static const Duration SHUTDOWN_TIMEOUT = const Duration(seconds: 5); |
31 | 25 |
32 /** | 26 /** |
33 * Connection to the analysis server. | 27 * Connection to the analysis server. |
34 */ | 28 */ |
35 Server server; | 29 final Server server = new Server(); |
36 | 30 |
37 /** | 31 /** |
38 * Temporary directory in which source files can be stored. | 32 * Temporary directory in which source files can be stored. |
39 */ | 33 */ |
40 Directory sourceDirectory; | 34 Directory sourceDirectory; |
41 | 35 |
42 /** | 36 /** |
43 * Map from file path to the list of analysis errors which have most recently | 37 * Map from file path to the list of analysis errors which have most recently |
44 * been received for the file. | 38 * been received for the file. |
45 */ | 39 */ |
46 HashMap<String, dynamic> currentAnalysisErrors = new HashMap<String, dynamic>( | 40 HashMap<String, dynamic> currentAnalysisErrors = new HashMap<String, dynamic>( |
47 ); | 41 ); |
48 | 42 |
49 /** | 43 /** |
50 * True if the teardown process should skip sending a "server.shutdown" | 44 * True if the teardown process should skip sending a "server.shutdown" |
51 * request (e.g. because the server is known to have already shutdown). | 45 * request (e.g. because the server is known to have already shutdown). |
52 */ | 46 */ |
53 bool skipShutdown = false; | 47 bool skipShutdown = false; |
54 | 48 |
55 /** | 49 /** |
| 50 * Data associated with the "server.connected" notification that was received |
| 51 * when the server started up. |
| 52 */ |
| 53 var serverConnectedParams; |
| 54 |
| 55 /** |
56 * Write a source file with the given contents. [relativePath] | 56 * Write a source file with the given contents. [relativePath] |
57 * is relative to [sourceDirectory]; on Windows any forward slashes it | 57 * is relative to [sourceDirectory]; on Windows any forward slashes it |
58 * contains are converted to backslashes. | 58 * contains are converted to backslashes. |
59 * | 59 * |
60 * If the file didn't previously exist, it is created. If it did, it is | 60 * If the file didn't previously exist, it is created. If it did, it is |
61 * overwritten. | 61 * overwritten. |
62 */ | 62 */ |
63 void writeFile(String relativePath, String contents) { | 63 void writeFile(String relativePath, String contents) { |
64 String absolutePath = normalizePath(relativePath); | 64 String absolutePath = normalizePath(relativePath); |
65 new Directory(dirname(absolutePath)).createSync(recursive: true); | 65 new Directory(dirname(absolutePath)).createSync(recursive: true); |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
130 void debugStdio() { | 130 void debugStdio() { |
131 server.debugStdio(); | 131 server.debugStdio(); |
132 } | 132 } |
133 | 133 |
134 /** | 134 /** |
135 * The server is automatically started before every test, and a temporary | 135 * The server is automatically started before every test, and a temporary |
136 * [sourceDirectory] is created. | 136 * [sourceDirectory] is created. |
137 */ | 137 */ |
138 Future setUp() { | 138 Future setUp() { |
139 sourceDirectory = Directory.systemTemp.createTempSync('analysisServer'); | 139 sourceDirectory = Directory.systemTemp.createTempSync('analysisServer'); |
140 return Server.start().then((Server server) { | 140 |
141 this.server = server; | 141 server.onNotification(ANALYSIS_ERRORS).listen((params) { |
142 server.onNotification(ANALYSIS_ERRORS).listen((params) { | 142 expect(params, isMap); |
143 expect(params, isMap); | 143 expect(params['file'], isString); |
144 expect(params['file'], isString); | 144 currentAnalysisErrors[params['file']] = params['errors']; |
145 currentAnalysisErrors[params['file']] = params['errors']; | 145 }); |
146 }); | 146 Completer serverConnected = new Completer(); |
| 147 server.onNotification(SERVER_CONNECTED).listen((_) { |
| 148 expect(serverConnected.isCompleted, isFalse); |
| 149 serverConnected.complete(); |
| 150 }); |
| 151 return server.start().then((params) { |
| 152 serverConnectedParams = params; |
147 server.exitCode.then((_) { skipShutdown = true; }); | 153 server.exitCode.then((_) { skipShutdown = true; }); |
| 154 return serverConnected.future; |
148 }); | 155 }); |
149 } | 156 } |
150 | 157 |
151 /** | 158 /** |
152 * After every test, the server is stopped and [sourceDirectory] is deleted. | 159 * After every test, the server is stopped and [sourceDirectory] is deleted. |
153 */ | 160 */ |
154 Future tearDown() { | 161 Future tearDown() { |
155 return _shutdownIfNeeded().then((_) { | 162 return _shutdownIfNeeded().then((_) { |
156 sourceDirectory.deleteSync(recursive: true); | 163 sourceDirectory.deleteSync(recursive: true); |
157 }); | 164 }); |
(...skipping 303 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
461 } | 468 } |
462 | 469 |
463 Matcher isListOf(Matcher elementMatcher) => new _ListOf(elementMatcher); | 470 Matcher isListOf(Matcher elementMatcher) => new _ListOf(elementMatcher); |
464 | 471 |
465 /** | 472 /** |
466 * Instances of the class [Server] manage a connection to a server process, and | 473 * Instances of the class [Server] manage a connection to a server process, and |
467 * facilitate communication to and from the server. | 474 * facilitate communication to and from the server. |
468 */ | 475 */ |
469 class Server { | 476 class Server { |
470 /** | 477 /** |
471 * Server process object. | 478 * Server process object, or null if server hasn't been started yet. |
472 */ | 479 */ |
473 Process _process; | 480 Process _process = null; |
474 | 481 |
475 /** | 482 /** |
476 * Commands that have been sent to the server but not yet acknowledged, and | 483 * Commands that have been sent to the server but not yet acknowledged, and |
477 * the [Completer] objects which should be completed when acknowledgement is | 484 * the [Completer] objects which should be completed when acknowledgement is |
478 * received. | 485 * received. |
479 */ | 486 */ |
480 final HashMap<String, Completer> _pendingCommands = <String, Completer> {}; | 487 final HashMap<String, Completer> _pendingCommands = <String, Completer> {}; |
481 | 488 |
482 /** | 489 /** |
483 * Number which should be used to compute the 'id' to send in the next command | 490 * Number which should be used to compute the 'id' to send in the next command |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
515 * True if we've received bad data from the server, and we are aborting the | 522 * True if we've received bad data from the server, and we are aborting the |
516 * test. | 523 * test. |
517 */ | 524 */ |
518 bool _receivedBadDataFromServer = false; | 525 bool _receivedBadDataFromServer = false; |
519 | 526 |
520 /** | 527 /** |
521 * Stopwatch that we use to generate timing information for debug output. | 528 * Stopwatch that we use to generate timing information for debug output. |
522 */ | 529 */ |
523 Stopwatch _time = new Stopwatch(); | 530 Stopwatch _time = new Stopwatch(); |
524 | 531 |
525 Server._(this._process) { | |
526 _time.start(); | |
527 } | |
528 | |
529 /** | 532 /** |
530 * Get a stream which will receive notifications of the given event type. | 533 * Get a stream which will receive notifications of the given event type. |
531 * The values delivered to the stream will be the contents of the 'params' | 534 * The values delivered to the stream will be the contents of the 'params' |
532 * field of the notification message. | 535 * field of the notification message. |
533 */ | 536 */ |
534 Stream onNotification(String event) { | 537 Stream onNotification(String event) { |
535 Stream notificationStream = _notificationStreams[event]; | 538 Stream notificationStream = _notificationStreams[event]; |
536 if (notificationStream == null) { | 539 if (notificationStream == null) { |
537 StreamController notificationController = new StreamController(); | 540 StreamController notificationController = new StreamController(); |
538 _notificationControllers[event] = notificationController; | 541 _notificationControllers[event] = notificationController; |
539 notificationStream = notificationController.stream.asBroadcastStream(); | 542 notificationStream = notificationController.stream.asBroadcastStream(); |
540 _notificationStreams[event] = notificationStream; | 543 _notificationStreams[event] = notificationStream; |
541 } | 544 } |
542 return notificationStream; | 545 return notificationStream; |
543 } | 546 } |
544 | 547 |
545 /** | 548 /** |
546 * Start the server. If [debugServer] is true, the server will be started | 549 * Start the server. If [debugServer] is true, the server will be started |
547 * with "--debug", allowing a debugger to be attached. | 550 * with "--debug", allowing a debugger to be attached. |
548 */ | 551 */ |
549 static Future<Server> start({bool debugServer: false}) { | 552 Future start({bool debugServer: false}) { |
| 553 if (_process != null) { |
| 554 throw new Exception('Process already started'); |
| 555 } |
| 556 _time.start(); |
550 // TODO(paulberry): move the logic for finding the script, the dart | 557 // TODO(paulberry): move the logic for finding the script, the dart |
551 // executable, and the package root into a shell script. | 558 // executable, and the package root into a shell script. |
552 String dartBinary = Platform.executable; | 559 String dartBinary = Platform.executable; |
553 String scriptDir = dirname(Platform.script.toFilePath(windows: | 560 String scriptDir = dirname(Platform.script.toFilePath(windows: |
554 Platform.isWindows)); | 561 Platform.isWindows)); |
555 String serverPath = normalize(join(scriptDir, '..', '..', 'bin', | 562 String serverPath = normalize(join(scriptDir, '..', '..', 'bin', |
556 'server.dart')); | 563 'server.dart')); |
557 List<String> arguments = []; | 564 List<String> arguments = []; |
558 if (debugServer) { | 565 if (debugServer) { |
559 arguments.add('--debug'); | 566 arguments.add('--debug'); |
560 } | 567 } |
561 if (Platform.packageRoot.isNotEmpty) { | 568 if (Platform.packageRoot.isNotEmpty) { |
562 arguments.add('--package-root=${Platform.packageRoot}'); | 569 arguments.add('--package-root=${Platform.packageRoot}'); |
563 } | 570 } |
564 arguments.add(serverPath); | 571 arguments.add(serverPath); |
565 return Process.start(dartBinary, arguments).then((Process process) { | 572 return Process.start(dartBinary, arguments).then((Process process) { |
566 Server server = new Server._(process); | 573 _process = process; |
567 process.stdout.transform((new Utf8Codec()).decoder).transform( | 574 process.stdout.transform((new Utf8Codec()).decoder).transform( |
568 new LineSplitter()).listen((String line) { | 575 new LineSplitter()).listen((String line) { |
569 String trimmedLine = line.trim(); | 576 String trimmedLine = line.trim(); |
570 server._recordStdio('RECV: $trimmedLine'); | 577 _recordStdio('RECV: $trimmedLine'); |
571 var message; | 578 var message; |
572 try { | 579 try { |
573 message = JSON.decoder.convert(trimmedLine); | 580 message = JSON.decoder.convert(trimmedLine); |
574 } catch (exception) { | 581 } catch (exception) { |
575 server._badDataFromServer(); | 582 _badDataFromServer(); |
576 return; | 583 return; |
577 } | 584 } |
578 expect(message, isMap); | 585 expect(message, isMap); |
579 Map messageAsMap = message; | 586 Map messageAsMap = message; |
580 if (messageAsMap.containsKey('id')) { | 587 if (messageAsMap.containsKey('id')) { |
581 expect(messageAsMap['id'], isString); | 588 expect(messageAsMap['id'], isString); |
582 String id = message['id']; | 589 String id = message['id']; |
583 Completer completer = server._pendingCommands[id]; | 590 Completer completer = _pendingCommands[id]; |
584 if (completer == null) { | 591 if (completer == null) { |
585 fail('Unexpected response from server: id=$id'); | 592 fail('Unexpected response from server: id=$id'); |
586 } else { | 593 } else { |
587 server._pendingCommands.remove(id); | 594 _pendingCommands.remove(id); |
588 } | 595 } |
589 if (messageAsMap.containsKey('error')) { | 596 if (messageAsMap.containsKey('error')) { |
590 // TODO(paulberry): propagate the error info to the completer. | 597 // TODO(paulberry): propagate the error info to the completer. |
591 completer.completeError(new UnimplementedError( | 598 completer.completeError(new UnimplementedError( |
592 'Server responded with an error')); | 599 'Server responded with an error')); |
593 } else { | 600 } else { |
594 completer.complete(messageAsMap['result']); | 601 completer.complete(messageAsMap['result']); |
595 } | 602 } |
596 // Check that the message is well-formed. We do this after calling | 603 // Check that the message is well-formed. We do this after calling |
597 // completer.complete() or completer.completeError() so that we don't | 604 // completer.complete() or completer.completeError() so that we don't |
598 // stall the test in the event of an error. | 605 // stall the test in the event of an error. |
599 expect(message, isResponse); | 606 expect(message, isResponse); |
600 } else { | 607 } else { |
601 // Message is a notification. It should have an event and possibly | 608 // Message is a notification. It should have an event and possibly |
602 // params. | 609 // params. |
603 expect(messageAsMap, contains('event')); | 610 expect(messageAsMap, contains('event')); |
604 expect(messageAsMap['event'], isString); | 611 expect(messageAsMap['event'], isString); |
605 String event = messageAsMap['event']; | 612 String event = messageAsMap['event']; |
606 StreamController notificationController = | 613 StreamController notificationController = |
607 server._notificationControllers[event]; | 614 _notificationControllers[event]; |
608 if (notificationController != null) { | 615 if (notificationController != null) { |
609 notificationController.add(messageAsMap['params']); | 616 notificationController.add(messageAsMap['params']); |
610 } | 617 } |
611 // Check that the message is well-formed. We do this after calling | 618 // Check that the message is well-formed. We do this after calling |
612 // notificationController.add() so that we don't stall the test in the | 619 // notificationController.add() so that we don't stall the test in the |
613 // event of an error. | 620 // event of an error. |
614 expect(message, isNotification); | 621 expect(message, isNotification); |
615 } | 622 } |
616 }); | 623 }); |
617 process.stderr.transform((new Utf8Codec()).decoder).transform( | 624 process.stderr.transform((new Utf8Codec()).decoder).transform( |
618 new LineSplitter()).listen((String line) { | 625 new LineSplitter()).listen((String line) { |
619 String trimmedLine = line.trim(); | 626 String trimmedLine = line.trim(); |
620 server._recordStdio('ERR: $trimmedLine'); | 627 _recordStdio('ERR: $trimmedLine'); |
621 server._badDataFromServer(); | 628 _badDataFromServer(); |
622 }); | 629 }); |
623 process.exitCode.then((int code) { | 630 process.exitCode.then((int code) { |
624 server._recordStdio('TERMINATED WITH EXIT CODE $code'); | 631 _recordStdio('TERMINATED WITH EXIT CODE $code'); |
625 if (code != 0) { | 632 if (code != 0) { |
626 server._badDataFromServer(); | 633 _badDataFromServer(); |
627 } | 634 } |
628 }); | 635 }); |
629 return server; | |
630 }); | 636 }); |
631 } | 637 } |
632 | 638 |
633 /** | 639 /** |
634 * Future that completes when the server process exits. | 640 * Future that completes when the server process exits. |
635 */ | 641 */ |
636 Future<int> get exitCode => _process.exitCode; | 642 Future<int> get exitCode => _process.exitCode; |
637 | 643 |
638 /** | 644 /** |
639 * Stop the server. | 645 * Stop the server. |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
710 */ | 716 */ |
711 void _recordStdio(String line) { | 717 void _recordStdio(String line) { |
712 double elapsedTime = _time.elapsedTicks / _time.frequency; | 718 double elapsedTime = _time.elapsedTicks / _time.frequency; |
713 line = "$elapsedTime: $line"; | 719 line = "$elapsedTime: $line"; |
714 if (_debuggingStdio) { | 720 if (_debuggingStdio) { |
715 print(line); | 721 print(line); |
716 } | 722 } |
717 _recordedStdio.add(line); | 723 _recordedStdio.add(line); |
718 } | 724 } |
719 } | 725 } |
OLD | NEW |