blob: db0bfcfdb86f07f166c5e1ef0a4666da92181c6e [file] [log] [blame]
// 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.
import 'dart:async';
import 'package:analysis_server/protocol/protocol_generated.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 {
late Driver driver;
late StreamSubscription<CompletionResultsParams> subscription;
late String notificationId;
late Stopwatch stopwatch;
bool firstNotification = true;
CompletionRequestOperation(
CommonInputConverter converter, Map<String, dynamic> json)
: super(converter, json);
@override
Future<void>? perform(Driver driver) {
this.driver = driver;
subscription = driver.onCompletionResults.listen(processNotification);
return super.perform(driver);
}
void processNotification(CompletionResultsParams event) {
if (event.id == notificationId) {
var 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, Object?> result, Stopwatch stopwatch) {
notificationId = result['id'] as String;
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<void>? 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<void>? perform(Driver driver) {
var stopwatch = Stopwatch();
var originalId = json['id'] as String;
var method = json['method'] as String;
json['clientRequestTime'] = DateTime.now().millisecondsSinceEpoch;
driver.logger.log(Level.FINE, 'Sending request: $method\n $json');
stopwatch.start();
void recordResult(bool success, result) {
var 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((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, Object?> 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 = Duration(seconds: 60);
final CommonInputConverter converter;
final Map<String, Object?> requestJson;
final Map<String, Object?> responseJson;
final Completer completer = Completer();
late Driver driver;
ResponseOperation(this.converter, this.requestJson, this.responseJson) {
completer.future.then(_processResult).timeout(responseTimeout);
}
@override
Future<void>? perform(Driver driver) {
this.driver = driver;
var id = responseJson['id'] as String;
return converter.processExpectedResponse(id, completer);
}
bool _equal(expectedResult, actualResult) {
if (expectedResult is Map && actualResult is Map) {
if (expectedResult.length == actualResult.length) {
return expectedResult.keys.every((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 (var 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) {
var text = '\n$value';
if (text.endsWith('\n')) {
text = text.substring(0, text.length - 1);
}
return text.replaceAll('\n', '\n ');
}
var message = 'Request:${format(requestJson)}\n'
'expected result:${format(expectedResult)}\n'
'expected error:${format(expectedError)}\n'
'but received:${format(actualResult)}';
var method = requestJson['method'] as String;
driver.results.recordUnexpectedResults(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) {
var start = DateTime.now();
driver.logger.log(Level.FINE, 'waiting for analysis to complete');
late StreamSubscription<ServerStatusParams> subscription;
late Timer timer;
var completer = Completer();
var isAnalyzing = false;
subscription = driver.onServerStatus.listen((ServerStatusParams params) {
var analysisStatus = params.analysis;
if (analysisStatus != null) {
if (analysisStatus.isAnalyzing) {
isAnalyzing = true;
} else {
subscription.cancel();
timer.cancel();
var end = DateTime.now();
var delta = end.difference(start);
driver.logger.log(Level.FINE, 'analysis complete after $delta');
completer.complete();
driver.results.record('analysis complete', delta, notification: true);
}
}
});
timer = Timer.periodic(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 communication received within the last 60 seconds.
var currentTime = driver.server.currentElapseTime;
var lastTime = driver.server.lastCommunicationTime!;
if (currentTime - lastTime > 60) {
subscription.cancel();
timer.cancel();
var message = 'gave up waiting for analysis to complete';
driver.logger.log(Level.WARNING, message);
completer.completeError(message);
}
});
return completer.future;
}
}