| 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 |