| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library server.operation; |
| |
| import 'dart:async'; |
| |
| import 'package:analysis_server/plugin/protocol/protocol.dart'; |
| import 'package:logging/logging.dart'; |
| |
| import 'driver.dart'; |
| import 'input_converter.dart'; |
| |
| /** |
| * A [CompletionRequestOperation] tracks response time along with |
| * the first and last completion notifications. |
| */ |
| class CompletionRequestOperation extends RequestOperation { |
| Driver driver; |
| StreamSubscription<CompletionResultsParams> subscription; |
| String notificationId; |
| Stopwatch stopwatch; |
| bool firstNotification = true; |
| |
| CompletionRequestOperation( |
| CommonInputConverter converter, Map<String, dynamic> json) |
| : super(converter, json); |
| |
| @override |
| Future perform(Driver driver) { |
| this.driver = driver; |
| subscription = driver.onCompletionResults.listen(processNotification); |
| return super.perform(driver); |
| } |
| |
| void processNotification(CompletionResultsParams event) { |
| if (event.id == notificationId) { |
| Duration elapsed = stopwatch.elapsed; |
| if (firstNotification) { |
| firstNotification = false; |
| driver.results.record('completion notification first', elapsed, |
| notification: true); |
| } |
| if (event.isLast) { |
| subscription.cancel(); |
| driver.results.record('completion notification last', elapsed, |
| notification: true); |
| } |
| } |
| } |
| |
| @override |
| void processResult( |
| String id, Map<String, dynamic> result, Stopwatch stopwatch) { |
| notificationId = result['id']; |
| this.stopwatch = stopwatch; |
| super.processResult(id, result, stopwatch); |
| } |
| } |
| |
| /** |
| * An [Operation] represents an action such as sending a request to the server. |
| */ |
| abstract class Operation { |
| Future perform(Driver driver); |
| } |
| |
| /** |
| * A [RequestOperation] sends a [JSON] request to the server. |
| */ |
| class RequestOperation extends Operation { |
| final CommonInputConverter converter; |
| final Map<String, dynamic> json; |
| |
| RequestOperation(this.converter, this.json); |
| |
| @override |
| Future perform(Driver driver) { |
| Stopwatch stopwatch = new Stopwatch(); |
| String originalId = json['id']; |
| String method = json['method']; |
| json['clientRequestTime'] = new DateTime.now().millisecondsSinceEpoch; |
| driver.logger.log(Level.FINE, 'Sending request: $method\n $json'); |
| stopwatch.start(); |
| |
| void recordResult(bool success, result) { |
| Duration elapsed = stopwatch.elapsed; |
| driver.results.record(method, elapsed, success: success); |
| driver.logger |
| .log(Level.FINE, 'Response received: $method : $elapsed\n $result'); |
| } |
| |
| driver |
| .send(method, converter.asMap(json['params'])) |
| .then((Map<String, dynamic> result) { |
| recordResult(true, result); |
| processResult(originalId, result, stopwatch); |
| }).catchError((exception) { |
| recordResult(false, exception); |
| converter.processErrorResponse(originalId, exception); |
| }); |
| return null; |
| } |
| |
| void processResult( |
| String id, Map<String, dynamic> result, Stopwatch stopwatch) { |
| converter.processResponseResult(id, result); |
| } |
| } |
| |
| /** |
| * A [ResponseOperation] waits for a [JSON] response from the server. |
| */ |
| class ResponseOperation extends Operation { |
| static final Duration responseTimeout = new Duration(seconds: 60); |
| final CommonInputConverter converter; |
| final Map<String, dynamic> requestJson; |
| final Map<String, dynamic> responseJson; |
| final Completer completer = new Completer(); |
| Driver driver; |
| |
| ResponseOperation(this.converter, this.requestJson, this.responseJson) { |
| completer.future.then(_processResult).timeout(responseTimeout); |
| } |
| |
| @override |
| Future perform(Driver driver) { |
| this.driver = driver; |
| return converter.processExpectedResponse(responseJson['id'], completer); |
| } |
| |
| bool _equal(expectedResult, actualResult) { |
| if (expectedResult is Map && actualResult is Map) { |
| if (expectedResult.length == actualResult.length) { |
| return expectedResult.keys.every((String key) { |
| return key == |
| 'fileStamp' || // fileStamp values will not be the same across runs |
| _equal(expectedResult[key], actualResult[key]); |
| }); |
| } |
| } else if (expectedResult is List && actualResult is List) { |
| if (expectedResult.length == actualResult.length) { |
| for (int i = 0; i < expectedResult.length; ++i) { |
| if (!_equal(expectedResult[i], actualResult[i])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| return expectedResult == actualResult; |
| } |
| |
| /** |
| * Compare the expected and actual server response result. |
| */ |
| void _processResult(actualResult) { |
| var expectedResult = responseJson['result']; |
| if (!_equal(expectedResult, actualResult)) { |
| var expectedError = responseJson['error']; |
| String format(value) { |
| String text = '\n$value'; |
| if (text.endsWith('\n')) { |
| text = text.substring(0, text.length - 1); |
| } |
| return text.replaceAll('\n', '\n '); |
| } |
| |
| String message = 'Request:${format(requestJson)}\n' |
| 'expected result:${format(expectedResult)}\n' |
| 'expected error:${format(expectedError)}\n' |
| 'but received:${format(actualResult)}'; |
| driver.results.recordUnexpectedResults(requestJson['method']); |
| converter.logOverlayContent(); |
| if (expectedError == null) { |
| converter.logger.log(Level.SEVERE, message); |
| } else { |
| throw message; |
| } |
| } |
| } |
| } |
| |
| class StartServerOperation extends Operation { |
| @override |
| Future perform(Driver driver) { |
| return driver.startServer(); |
| } |
| } |
| |
| class WaitForAnalysisCompleteOperation extends Operation { |
| @override |
| Future perform(Driver driver) { |
| DateTime start = new DateTime.now(); |
| driver.logger.log(Level.FINE, 'waiting for analysis to complete'); |
| StreamSubscription<ServerStatusParams> subscription; |
| Timer timer; |
| Completer completer = new Completer(); |
| bool isAnalyzing = false; |
| subscription = driver.onServerStatus.listen((ServerStatusParams params) { |
| if (params.analysis != null) { |
| if (params.analysis.isAnalyzing) { |
| isAnalyzing = true; |
| } else { |
| subscription.cancel(); |
| timer.cancel(); |
| DateTime end = new DateTime.now(); |
| Duration delta = end.difference(start); |
| driver.logger.log(Level.FINE, 'analysis complete after $delta'); |
| completer.complete(); |
| driver.results.record('analysis complete', delta, notification: true); |
| } |
| } |
| }); |
| timer = new Timer.periodic(new Duration(milliseconds: 20), (_) { |
| if (!isAnalyzing) { |
| // TODO (danrubel) revisit this once source change requests are implemented |
| subscription.cancel(); |
| timer.cancel(); |
| driver.logger.log(Level.INFO, 'analysis never started'); |
| completer.complete(); |
| return; |
| } |
| // Timeout if no communcation received within the last 60 seconds. |
| double currentTime = driver.server.currentElapseTime; |
| double lastTime = driver.server.lastCommunicationTime; |
| if (currentTime - lastTime > 60) { |
| subscription.cancel(); |
| timer.cancel(); |
| String message = 'gave up waiting for analysis to complete'; |
| driver.logger.log(Level.WARNING, message); |
| completer.completeError(message); |
| } |
| }); |
| return completer.future; |
| } |
| } |