b0d4ab8ef9
This was mostly a rename/move of the folder (and the analysis server updated all references), but I also had to:
- add `integration_test/analysis_options.yaml` to import from `../test` to get the same lint ignores
- update paths of exclusions in `verify_sorted_test.dart`
By moving all of the tests that start the server out-of-process out of test, we can:
1. Use "dart test" to just run the faster tests ("dart test test") and get functionality of the pkg:test runner (for example running tests concurrently and JSON output)
2. Allow VS Code to spawn different debug sessions for the "test" and "integration_test" folder, which means we can use a `preLaunchTask` to trigger compilation of the analysis server from source whenever running integration tests (avoiding having to compile manually, or run from source in a way that compiles a new server for each test suite)
Change-Id: I37cc03dc32d08c5b51a2eab79f6338bb079b32ac
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/434801
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
1060 lines
34 KiB
Dart
1060 lines
34 KiB
Dart
// Copyright (c) 2014, 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 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:analysis_server/protocol/protocol_constants.dart';
|
|
import 'package:analysis_server/src/protocol_server.dart';
|
|
import 'package:analysis_server/src/services/pub/pub_command.dart';
|
|
import 'package:analyzer/file_system/physical_file_system.dart';
|
|
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
|
|
import 'package:analyzer_testing/mock_packages/mock_packages.dart';
|
|
import 'package:analyzer_testing/utilities/utilities.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:test/test.dart';
|
|
|
|
import '../../test/constants.dart';
|
|
import '../../test/support/configuration_files.dart';
|
|
import '../../test/support/sdk_paths.dart';
|
|
import 'integration_test_methods.dart';
|
|
import 'protocol_matchers.dart';
|
|
|
|
const Matcher isBool = TypeMatcher<bool>();
|
|
|
|
const Matcher isDouble = TypeMatcher<double>();
|
|
|
|
const Matcher isInt = TypeMatcher<int>();
|
|
|
|
const Matcher isNotification = MatchesJsonObject(
|
|
'notification',
|
|
{'event': isString},
|
|
optionalFields: {'params': isMap},
|
|
);
|
|
|
|
const Matcher isObject = TypeMatcher<Object>();
|
|
|
|
const Matcher isString = TypeMatcher<String>();
|
|
|
|
final Matcher isResponse = MatchesJsonObject(
|
|
'response',
|
|
{'id': isString},
|
|
optionalFields: {'result': anything, 'error': isRequestError},
|
|
);
|
|
|
|
Matcher isListOf(Matcher elementMatcher) => _ListOf(elementMatcher);
|
|
|
|
Matcher isMapOf(Matcher keyMatcher, Matcher valueMatcher) =>
|
|
_MapOf(keyMatcher, valueMatcher);
|
|
|
|
Matcher isOneOf(List<Matcher> choiceMatchers) => _OneOf(choiceMatchers);
|
|
|
|
/// Assert that [actual] matches [matcher].
|
|
void outOfTestExpect(
|
|
Object? actual,
|
|
Matcher matcher, {
|
|
String? reason,
|
|
skip,
|
|
bool verbose = false,
|
|
}) {
|
|
var matchState = {};
|
|
try {
|
|
if (matcher.matches(actual, matchState)) return;
|
|
} catch (e, trace) {
|
|
reason ??= '${(e is String) ? e : e.toString()} at $trace';
|
|
}
|
|
fail(_defaultFailFormatter(actual, matcher, reason, matchState, verbose));
|
|
}
|
|
|
|
String _defaultFailFormatter(
|
|
dynamic actual,
|
|
Matcher matcher,
|
|
String? reason,
|
|
Map<Object?, Object?> matchState,
|
|
bool verbose,
|
|
) {
|
|
var description = StringDescription();
|
|
description.add('Expected: ').addDescriptionOf(matcher).add('\n');
|
|
description.add(' Actual: ').addDescriptionOf(actual).add('\n');
|
|
|
|
var mismatchDescription = StringDescription();
|
|
matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
|
|
|
|
if (mismatchDescription.length > 0) {
|
|
description.add(' Which: $mismatchDescription\n');
|
|
}
|
|
if (reason != null) description.add(reason).add('\n');
|
|
return description.toString();
|
|
}
|
|
|
|
/// Type of closures used by LazyMatcher.
|
|
typedef MatcherCreator = Matcher Function();
|
|
|
|
/// Type of closures used by MatchesJsonObject to record field mismatches.
|
|
typedef MismatchDescriber =
|
|
Description Function(Description mismatchDescription);
|
|
|
|
/// Type of callbacks used to process notifications.
|
|
typedef NotificationProcessor =
|
|
void Function(String event, Map<Object?, Object?> params);
|
|
|
|
/// Type of callbacks used to process reverse-requests.
|
|
typedef ReverseRequestProcessor = void Function(Request request);
|
|
|
|
/// Base class for analysis server integration tests.
|
|
abstract class AbstractAnalysisServerIntegrationTest extends IntegrationTest
|
|
with MockPackagesMixin, ConfigurationFilesMixin {
|
|
/// Amount of time to give the server to respond to a shutdown request before
|
|
/// forcibly terminating it.
|
|
static const Duration SHUTDOWN_TIMEOUT = Duration(seconds: 60);
|
|
|
|
/// Connection to the analysis server.
|
|
@override
|
|
final Server server = Server();
|
|
|
|
/// Temporary directory in which source files can be stored.
|
|
late Directory sourceDirectory;
|
|
|
|
/// Temporary directory in which additional packages can be stored.
|
|
late Directory packagesDirectory;
|
|
|
|
@override
|
|
final resourceProvider = PhysicalResourceProvider.INSTANCE;
|
|
|
|
/// Map from file path to the list of analysis errors which have most recently
|
|
/// been received for the file.
|
|
///
|
|
/// If the server requests a files errors are flushed, they will be removed
|
|
/// from the map.
|
|
Map<String, List<AnalysisError>> currentAnalysisErrors =
|
|
HashMap<String, List<AnalysisError>>();
|
|
|
|
/// The last list of analyzed files received.
|
|
late List<String> lastAnalyzedFiles;
|
|
|
|
/// True if the teardown process should skip sending a "server.shutdown"
|
|
/// request (e.g. because the server is known to have already shutdown).
|
|
bool skipShutdown = false;
|
|
|
|
/// True if we are currently subscribed to [SERVER_NOTIFICATION_STATUS]
|
|
/// updates.
|
|
bool _subscribedToServerStatus = false;
|
|
|
|
String dartSdkPath = path.dirname(path.dirname(Platform.resolvedExecutable));
|
|
|
|
StreamController<Request> serverToClientRequestsController =
|
|
StreamController<Request>.broadcast();
|
|
|
|
/// Return a future which will complete when a 'server.status' notification is
|
|
/// received from the server with 'analyzing' set to false.
|
|
///
|
|
/// The future will only be completed by 'server.status' notifications that
|
|
/// are received after this function call. So it is safe to use this getter
|
|
/// multiple times in one test; each time it is used it will wait afresh for
|
|
/// analysis to finish.
|
|
Future<ServerStatusParams> get analysisFinished {
|
|
var completer = Completer<ServerStatusParams>();
|
|
late StreamSubscription<ServerStatusParams> subscription;
|
|
// This will only work if the caller has already subscribed to
|
|
// SERVER_STATUS (e.g. using sendServerSetSubscriptions(['STATUS']))
|
|
outOfTestExpect(_subscribedToServerStatus, isTrue);
|
|
subscription = onServerStatus.listen((ServerStatusParams params) {
|
|
var analysisStatus = params.analysis;
|
|
if (analysisStatus != null && !analysisStatus.isAnalyzing) {
|
|
completer.complete(params);
|
|
subscription.cancel();
|
|
}
|
|
});
|
|
return completer.future;
|
|
}
|
|
|
|
@override
|
|
String get packagesRootPath => packagesDirectory.path;
|
|
|
|
Stream<Request> get serverToClientRequests =>
|
|
serverToClientRequestsController.stream;
|
|
|
|
@override
|
|
String get testPackageRootPath => sourceDirectory.path;
|
|
|
|
/// Print out any messages exchanged with the server. If some messages have
|
|
/// already been exchanged with the server, they are printed out immediately.
|
|
void debugStdio() {
|
|
server.debugStdio();
|
|
}
|
|
|
|
/// Deletes the folder at [pathname], recursively deleting all children.
|
|
void deleteFolder(String pathname) {
|
|
Directory(pathname).deleteSync(recursive: true);
|
|
}
|
|
|
|
void dispatchReverseRequest(Request request) {
|
|
serverToClientRequestsController.add(request);
|
|
}
|
|
|
|
/// If there was a set of errors (might be empty) received for the file
|
|
/// with the given [path], return it. If no errors have been received (or they
|
|
/// have since been flushed) - fail.
|
|
List<AnalysisError> existingErrorsForFile(String path) {
|
|
var errors = currentAnalysisErrors[path];
|
|
if (errors == null) {
|
|
fail('Expected errors for: $path');
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
List<AnalysisError>? getErrors(String pathname) =>
|
|
currentAnalysisErrors[pathname];
|
|
|
|
/// Read a source file with the given absolute [pathname].
|
|
String readFile(String pathname) => File(pathname).readAsStringSync();
|
|
|
|
@override
|
|
Future<void> sendServerSetSubscriptions(List<ServerService> subscriptions) {
|
|
_subscribedToServerStatus = subscriptions.contains(ServerService.STATUS);
|
|
return super.sendServerSetSubscriptions(subscriptions);
|
|
}
|
|
|
|
/// The server is automatically started before every test, and a temporary
|
|
/// [sourceDirectory] is created.
|
|
Future<void> setUp() async {
|
|
var pathContext = resourceProvider.pathContext;
|
|
var tempDirectoryPath =
|
|
Directory.systemTemp
|
|
.createTempSync('analysisServer')
|
|
.resolveSymbolicLinksSync();
|
|
sourceDirectory = Directory(pathContext.join(tempDirectoryPath, 'app'))
|
|
..createSync();
|
|
packagesDirectory = Directory(
|
|
pathContext.join(tempDirectoryPath, 'packages'),
|
|
)..createSync();
|
|
writeTestPackageConfig();
|
|
|
|
writeTestPackageAnalysisOptionsFile(
|
|
analysisOptionsContent(experiments: ['macros']),
|
|
);
|
|
|
|
onAnalysisErrors.listen((AnalysisErrorsParams params) {
|
|
currentAnalysisErrors[params.file] = params.errors;
|
|
});
|
|
onAnalysisFlushResults.listen((AnalysisFlushResultsParams params) {
|
|
for (var file in params.files) {
|
|
currentAnalysisErrors.remove(file);
|
|
}
|
|
});
|
|
onAnalysisAnalyzedFiles.listen((AnalysisAnalyzedFilesParams params) {
|
|
lastAnalyzedFiles = params.directories;
|
|
});
|
|
var serverConnected = Completer();
|
|
onServerConnected.listen((_) {
|
|
outOfTestExpect(serverConnected.isCompleted, isFalse);
|
|
serverConnected.complete();
|
|
});
|
|
onServerError.listen((ServerErrorParams params) {
|
|
// A server error should never happen during an integration test.
|
|
fail('${params.message}\n${params.stackTrace}');
|
|
});
|
|
await startServer();
|
|
server.listenToOutput(dispatchNotification, dispatchReverseRequest);
|
|
unawaited(
|
|
server.exitCode.then((_) {
|
|
skipShutdown = true;
|
|
}),
|
|
);
|
|
return serverConnected.future;
|
|
}
|
|
|
|
/// If [skipShutdown] is not set, shut down the server.
|
|
Future<void> shutdownIfNeeded() {
|
|
if (skipShutdown) {
|
|
return Future.value();
|
|
}
|
|
// Give the server a short time to comply with the shutdown request; if it
|
|
// doesn't exit, then forcibly terminate it.
|
|
sendServerShutdown();
|
|
return server.exitCode.timeout(
|
|
SHUTDOWN_TIMEOUT,
|
|
onTimeout: () {
|
|
// The integer value of the exit code isn't used, but we have to return
|
|
// an integer to keep the typing correct.
|
|
return server.kill('server failed to exit').then((_) => -1);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Convert the given [relativePath] to an absolute path, by interpreting it
|
|
/// relative to [sourceDirectory]. On Windows any forward slashes in
|
|
/// [relativePath] are converted to backslashes.
|
|
String sourcePath(String relativePath) {
|
|
return path.join(
|
|
sourceDirectory.path,
|
|
relativePath.replaceAll('/', path.separator),
|
|
);
|
|
}
|
|
|
|
/// Send the server an 'analysis.setAnalysisRoots' command directing it to
|
|
/// analyze [sourceDirectory]. If [subscribeStatus] is true (the default),
|
|
/// then also enable [SERVER_NOTIFICATION_STATUS] notifications so that
|
|
/// [analysisFinished] can be used.
|
|
Future<void> standardAnalysisSetup({bool subscribeStatus = true}) {
|
|
var futures = <Future<void>>[];
|
|
if (subscribeStatus) {
|
|
futures.add(sendServerSetSubscriptions([ServerService.STATUS]));
|
|
}
|
|
futures.add(sendAnalysisSetAnalysisRoots([sourceDirectory.path], []));
|
|
return Future.wait(futures);
|
|
}
|
|
|
|
/// Start [server].
|
|
Future<void> startServer({int? diagnosticPort, int? servicePort}) {
|
|
return server.start(
|
|
dartSdkPath: dartSdkPath,
|
|
diagnosticPort: diagnosticPort,
|
|
servicePort: servicePort,
|
|
);
|
|
}
|
|
|
|
/// After every test, the server is stopped and [sourceDirectory] and
|
|
/// [packagesDirectory] are deleted.
|
|
@mustCallSuper
|
|
Future<void> tearDown() {
|
|
return shutdownIfNeeded().then((_) {
|
|
sourceDirectory.deleteSync(recursive: true);
|
|
packagesDirectory.deleteSync(recursive: true);
|
|
});
|
|
}
|
|
|
|
/// Returns the latest set of errors for the file.
|
|
///
|
|
/// If there are no errors received from the server yet, waits for them.
|
|
/// Removes the set from the storage, so will wait again next time.
|
|
Future<List<AnalysisError>> waitForFileErrors(String path) async {
|
|
while (true) {
|
|
var result = currentAnalysisErrors.remove(path);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
await pumpEventQueue();
|
|
}
|
|
}
|
|
|
|
/// Write a source file with the given absolute [pathname] and [contents].
|
|
///
|
|
/// If the file didn't previously exist, it is created. If it did, it is
|
|
/// overwritten.
|
|
///
|
|
/// Parent directories are created as necessary.
|
|
///
|
|
/// Return a normalized path to the file (with symbolic links resolved).
|
|
String writeFile(String pathname, String contents) {
|
|
Directory(path.dirname(pathname)).createSync(recursive: true);
|
|
var file = File(pathname);
|
|
file.writeAsStringSync(contents);
|
|
return file.resolveSymbolicLinksSync();
|
|
}
|
|
|
|
void writeTestPackageAnalysisOptionsFile(String content) {
|
|
String filePath = path.join(
|
|
testPackageRootPath,
|
|
file_paths.analysisOptionsYaml,
|
|
);
|
|
writeFile(filePath, content);
|
|
}
|
|
}
|
|
|
|
/// Wrapper class for Matcher which doesn't create the underlying Matcher object
|
|
/// until it is needed. This is necessary in order to create matchers that can
|
|
/// refer to themselves (so that recursive data structures can be represented).
|
|
class LazyMatcher implements Matcher {
|
|
/// Callback that will be used to create the matcher the first time it is
|
|
/// needed.
|
|
final MatcherCreator _creator;
|
|
|
|
/// The matcher returned by [_creator], if it has already been called.
|
|
/// Otherwise null.
|
|
Matcher? _wrappedMatcher;
|
|
|
|
LazyMatcher(this._creator);
|
|
|
|
/// Create the wrapped matcher object, if it hasn't been created already.
|
|
Matcher get _matcher {
|
|
return _wrappedMatcher ??= _creator();
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return _matcher.describe(description);
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
Object? item,
|
|
Description mismatchDescription,
|
|
Map<Object?, Object?> matchState,
|
|
bool verbose,
|
|
) {
|
|
return _matcher.describeMismatch(
|
|
item,
|
|
mismatchDescription,
|
|
matchState,
|
|
verbose,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool matches(dynamic item, Map<Object?, Object?> matchState) {
|
|
return _matcher.matches(item, matchState);
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches a String drawn from a limited set.
|
|
class MatchesEnum extends Matcher {
|
|
/// Short description of the expected type.
|
|
final String description;
|
|
|
|
/// The set of enum values that are allowed.
|
|
final List<String> allowedValues;
|
|
|
|
const MatchesEnum(this.description, this.allowedValues);
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add(this.description);
|
|
|
|
@override
|
|
bool matches(item, Map<Object?, Object?> matchState) {
|
|
return allowedValues.contains(item);
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches a JSON object, with a given set of required and
|
|
/// optional fields, and their associated types (expressed as [Matcher]s).
|
|
class MatchesJsonObject extends _RecursiveMatcher {
|
|
/// Short description of the expected type.
|
|
final String description;
|
|
|
|
/// Fields that are required to be in the JSON object, and [Matcher]s
|
|
/// describing their expected types.
|
|
final Map<String, Matcher>? requiredFields;
|
|
|
|
/// Fields that are optional in the JSON object, and [Matcher]s describing
|
|
/// their expected types.
|
|
final Map<String, Matcher>? optionalFields;
|
|
|
|
const MatchesJsonObject(
|
|
this.description,
|
|
this.requiredFields, {
|
|
this.optionalFields,
|
|
});
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add(this.description);
|
|
|
|
@override
|
|
void populateMismatches(Object? item, List<MismatchDescriber> mismatches) {
|
|
if (item is! Map<String, Object?>) {
|
|
mismatches.add(simpleDescription('is not a map'));
|
|
return;
|
|
}
|
|
var requiredFields = this.requiredFields;
|
|
var optionalFields = this.optionalFields;
|
|
if (requiredFields != null) {
|
|
requiredFields.forEach((String key, Matcher valueMatcher) {
|
|
if (!item.containsKey(key)) {
|
|
mismatches.add(
|
|
(Description mismatchDescription) => mismatchDescription
|
|
.add('is missing field ')
|
|
.addDescriptionOf(key)
|
|
.add(' (')
|
|
.addDescriptionOf(valueMatcher)
|
|
.add(')'),
|
|
);
|
|
} else {
|
|
_checkField(key, item[key], valueMatcher, mismatches);
|
|
}
|
|
});
|
|
}
|
|
item.forEach((key, value) {
|
|
if (requiredFields != null && requiredFields.containsKey(key)) {
|
|
// Already checked this field
|
|
return;
|
|
}
|
|
if (optionalFields != null) {
|
|
var optionalValue = optionalFields[key];
|
|
if (optionalValue != null) {
|
|
_checkField(key, value, optionalValue, mismatches);
|
|
return;
|
|
}
|
|
}
|
|
mismatches.add(
|
|
(Description mismatchDescription) => mismatchDescription
|
|
.add('has unexpected field ')
|
|
.addDescriptionOf(key),
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Check the type of a field called [key], having value [value], using
|
|
/// [valueMatcher]. If it doesn't match, record a closure in [mismatches]
|
|
/// which can describe the mismatch.
|
|
void _checkField(
|
|
String key,
|
|
value,
|
|
Matcher valueMatcher,
|
|
List<MismatchDescriber> mismatches,
|
|
) {
|
|
checkSubstructure(
|
|
value,
|
|
valueMatcher,
|
|
mismatches,
|
|
(Description description) =>
|
|
description.add('field ').addDescriptionOf(key),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Instances of the class [Server] manage a connection to a server process, and
|
|
/// facilitate communication to and from the server.
|
|
class Server {
|
|
/// Server process object, or null if server hasn't been started yet.
|
|
late final Process _process;
|
|
|
|
/// Commands that have been sent to the server but not yet acknowledged, and
|
|
/// the [Completer] objects which should be completed when acknowledgement is
|
|
/// received.
|
|
final Map<String, Completer<Map<String, Object?>?>> _pendingCommands = {};
|
|
|
|
/// Number which should be used to compute the 'id' to send in the next
|
|
/// command sent to the server.
|
|
int _nextId = 0;
|
|
|
|
/// Messages which have been exchanged with the server; we buffer these
|
|
/// up until the test finishes, so that they can be examined in the debugger
|
|
/// or printed out in response to a call to [debugStdio].
|
|
final List<String> _recordedStdio = <String>[];
|
|
|
|
/// True if we are currently printing out messages exchanged with the server.
|
|
bool _debuggingStdio = debugPrintCommunication;
|
|
|
|
/// True if we've received bad data from the server, and we are aborting the
|
|
/// test.
|
|
bool _receivedBadDataFromServer = false;
|
|
|
|
/// Stopwatch that we use to generate timing information for debug output.
|
|
final Stopwatch _time = Stopwatch();
|
|
|
|
/// The [currentElapseTime] at which the last communication was received from
|
|
/// the server or `null` if no communication has been received.
|
|
double? lastCommunicationTime;
|
|
|
|
/// The current elapse time (seconds) since the server was started.
|
|
double get currentElapseTime => _time.elapsedTicks / _time.frequency;
|
|
|
|
/// Future that completes when the server process exits.
|
|
Future<int> get exitCode => _process.exitCode;
|
|
|
|
/// Print out any messages exchanged with the server. If some messages have
|
|
/// already been exchanged with the server, they are printed out immediately.
|
|
void debugStdio() {
|
|
if (_debuggingStdio) {
|
|
return;
|
|
}
|
|
_debuggingStdio = true;
|
|
for (var line in _recordedStdio) {
|
|
print(line);
|
|
}
|
|
}
|
|
|
|
/// Return a future that will complete when all commands that have been sent
|
|
/// to the server so far have been flushed to the OS buffer.
|
|
Future<void> flushCommands() {
|
|
return _process.stdin.flush();
|
|
}
|
|
|
|
/// Stop the server.
|
|
Future<int> kill(String reason) {
|
|
debugStdio();
|
|
_recordStdio('FORCIBLY TERMINATING PROCESS: $reason');
|
|
_process.kill();
|
|
return _process.exitCode;
|
|
}
|
|
|
|
/// Start listening to output from the server, and deliver notifications to
|
|
/// [notificationProcessor] and reverse requests to [reverseRequestProcessor].
|
|
void listenToOutput(
|
|
NotificationProcessor notificationProcessor, [
|
|
ReverseRequestProcessor? reverseRequestProcessor,
|
|
]) {
|
|
// Provide a default implementation of the reverse request processor that
|
|
// just throws because there are many tests that don't use reverse-requests.
|
|
reverseRequestProcessor ??=
|
|
(_) =>
|
|
throw UnimplementedError(
|
|
"A reverse request was received but the test did not provide 'reverseRequestProcessor'",
|
|
);
|
|
|
|
_process.stdout.transform(utf8.decoder).transform(LineSplitter()).listen((
|
|
String line,
|
|
) {
|
|
lastCommunicationTime = currentElapseTime;
|
|
var trimmedLine = line.trim();
|
|
|
|
// Guard against lines like:
|
|
// {"event":"server.connected","params":{...}}The Dart VM service is listening on ...
|
|
const dartVMServiceMessage = 'The Dart VM service is listening on ';
|
|
if (trimmedLine.contains(dartVMServiceMessage)) {
|
|
trimmedLine =
|
|
trimmedLine
|
|
.substring(0, trimmedLine.indexOf(dartVMServiceMessage))
|
|
.trim();
|
|
}
|
|
const devtoolsMessage =
|
|
'The Dart DevTools debugger and profiler is available at:';
|
|
if (trimmedLine.contains(devtoolsMessage)) {
|
|
trimmedLine =
|
|
trimmedLine
|
|
.substring(0, trimmedLine.indexOf(devtoolsMessage))
|
|
.trim();
|
|
}
|
|
if (trimmedLine.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
_recordStdio('<== $trimmedLine');
|
|
Map<Object?, Object?> message;
|
|
try {
|
|
message = json.decoder.convert(trimmedLine) as Map<Object?, Object?>;
|
|
} catch (exception) {
|
|
_badDataFromServer('JSON decode failure: $exception');
|
|
return;
|
|
}
|
|
outOfTestExpect(message, isMap);
|
|
if (message.containsKey('id') && message.containsKey('method')) {
|
|
// Message is a reverse-request from the server.
|
|
outOfTestExpect(message['id'], isString);
|
|
var id = message['id'] as String;
|
|
outOfTestExpect(message['method'], isString);
|
|
var method = message['method'] as String;
|
|
reverseRequestProcessor!(
|
|
Request(id, method, message['params'] as Map<String, Object?>?),
|
|
);
|
|
} else if (message.containsKey('id')) {
|
|
// Message is a response to an outstanding request.
|
|
outOfTestExpect(message['id'], isString);
|
|
var id = message['id'] as String;
|
|
var completer = _pendingCommands[id];
|
|
if (completer == null) {
|
|
fail('Unexpected response from server: id=$id');
|
|
} else {
|
|
_pendingCommands.remove(id);
|
|
}
|
|
if (message.containsKey('error')) {
|
|
completer.completeError(ServerErrorMessage(message));
|
|
} else {
|
|
completer.complete(message['result'] as Map<String, Object?>?);
|
|
}
|
|
// Check that the message is well-formed. We do this after calling
|
|
// completer.complete() or completer.completeError() so that we don't
|
|
// stall the test in the event of an error.
|
|
outOfTestExpect(message, isResponse);
|
|
} else {
|
|
// Message is a notification. It should have an event and possibly
|
|
// params.
|
|
outOfTestExpect(message, contains('event'));
|
|
outOfTestExpect(message['event'], isString);
|
|
notificationProcessor(
|
|
message['event'] as String,
|
|
message['params'] as Map<Object?, Object?>,
|
|
);
|
|
// Check that the message is well-formed. We do this after calling
|
|
// notificationController.add() so that we don't stall the test in the
|
|
// event of an error.
|
|
outOfTestExpect(message, isNotification);
|
|
}
|
|
});
|
|
_process.stderr
|
|
.transform(Utf8Codec().decoder)
|
|
.transform(LineSplitter())
|
|
.listen((String line) {
|
|
var trimmedLine = line.trim();
|
|
_recordStdio('ERR: $trimmedLine');
|
|
_badDataFromServer('Message received on stderr', silent: true);
|
|
});
|
|
}
|
|
|
|
/// Send a command to the server. An 'id' will be automatically assigned.
|
|
/// The returned [Future] will be completed when the server acknowledges the
|
|
/// command with a response. If the server acknowledges the command with a
|
|
/// normal (non-error) response, the future will be completed with the
|
|
/// 'result' field from the response. If the server acknowledges the command
|
|
/// with an error response, the future will be completed with an error.
|
|
Future<Map<String, Object?>?> send(
|
|
String method,
|
|
Map<String, Object?>? params,
|
|
) {
|
|
var id = '${_nextId++}';
|
|
var command = <String, Object?>{'id': id, 'method': method};
|
|
if (params != null) {
|
|
command['params'] = params;
|
|
}
|
|
var completer = Completer<Map<String, Object?>?>();
|
|
_pendingCommands[id] = completer;
|
|
sendRaw(command);
|
|
return completer.future;
|
|
}
|
|
|
|
/// Send a raw object to the server.
|
|
void sendRaw(Map<String, Object?> data) {
|
|
var line = json.encode(data);
|
|
_recordStdio('==> $line');
|
|
_process.stdin.add(utf8.encode('$line\n'));
|
|
}
|
|
|
|
/// Start the server. If [profileServer] is `true`, the server will be started
|
|
/// with "--observe" and "--pause-isolates-on-exit", allowing Dart DevTools
|
|
/// to be used.
|
|
Future<void> start({
|
|
required String dartSdkPath,
|
|
int? diagnosticPort,
|
|
String? instrumentationLogFile,
|
|
String? packagesFile,
|
|
bool profileServer = false,
|
|
int? servicePort,
|
|
bool useAnalysisHighlight2 = false,
|
|
}) async {
|
|
_time.start();
|
|
|
|
var dartBinary = path.join(dartSdkPath, 'bin', 'dart');
|
|
var serverPath = await getAnalysisServerPath(dartSdkPath);
|
|
|
|
var arguments = <String>['--disable-dart-dev', '--no-dds'];
|
|
//
|
|
// Add VM arguments.
|
|
//
|
|
if (profileServer) {
|
|
if (servicePort == null) {
|
|
arguments.add('--observe');
|
|
} else {
|
|
arguments.add('--observe=$servicePort');
|
|
}
|
|
arguments.add('--pause-isolates-on-exit');
|
|
} else if (servicePort != null) {
|
|
arguments.add('--enable-vm-service=$servicePort');
|
|
}
|
|
if (Platform.packageConfig != null) {
|
|
arguments.add('--packages=${Platform.packageConfig}');
|
|
}
|
|
arguments.add('--disable-service-auth-codes');
|
|
//
|
|
// Add the server executable.
|
|
//
|
|
arguments.add(serverPath);
|
|
//
|
|
// Add server arguments.
|
|
//
|
|
arguments.add('--suppress-analytics');
|
|
arguments.add('--sdk=$dartSdkPath');
|
|
if (diagnosticPort != null) {
|
|
arguments.add('--port');
|
|
arguments.add(diagnosticPort.toString());
|
|
}
|
|
if (instrumentationLogFile != null) {
|
|
arguments.add('--instrumentation-log-file=$instrumentationLogFile');
|
|
}
|
|
if (packagesFile != null) {
|
|
arguments.add('--packages=$packagesFile');
|
|
}
|
|
if (useAnalysisHighlight2) {
|
|
arguments.add('--useAnalysisHighlight2');
|
|
}
|
|
_process = await Process.start(
|
|
dartBinary,
|
|
arguments,
|
|
environment: {PubCommand.disablePubCommandEnvironmentKey: 'true'},
|
|
);
|
|
unawaited(
|
|
_process.exitCode.then((int code) {
|
|
if (code != 0) {
|
|
_badDataFromServer('server terminated with exit code $code');
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// Deal with bad data received from the server.
|
|
void _badDataFromServer(String details, {bool silent = false}) {
|
|
if (!silent) {
|
|
_recordStdio('BAD DATA FROM SERVER: $details');
|
|
}
|
|
if (_receivedBadDataFromServer) {
|
|
// We're already dealing with it.
|
|
return;
|
|
}
|
|
_receivedBadDataFromServer = true;
|
|
debugStdio();
|
|
// Give the server 1 second to continue outputting bad data before we kill
|
|
// the test. This is helpful if the server has had an unhandled exception
|
|
// and is outputting a stacktrace, because it ensures that we see the
|
|
// entire stacktrace. Use expectAsync() to prevent the test from
|
|
// ending during this 1 second.
|
|
Future.delayed(
|
|
Duration(seconds: 1),
|
|
expectAsync0(() {
|
|
fail('Bad data received from server: $details');
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// Record a message that was exchanged with the server, and print it out if
|
|
/// [debugStdio] has been called.
|
|
void _recordStdio(String line) {
|
|
var elapsedTime = currentElapseTime;
|
|
line = '$elapsedTime: $line';
|
|
if (_debuggingStdio) {
|
|
print(line);
|
|
}
|
|
_recordedStdio.add(line);
|
|
}
|
|
}
|
|
|
|
/// An error result from a server request.
|
|
class ServerErrorMessage {
|
|
final Map<Object?, Object?> message;
|
|
|
|
ServerErrorMessage(this.message);
|
|
|
|
dynamic get error => message['error'];
|
|
|
|
@override
|
|
String toString() => message.toString();
|
|
}
|
|
|
|
/// Matcher that matches a list of objects, each of which satisfies the given
|
|
/// matcher.
|
|
class _ListOf extends Matcher {
|
|
/// Matcher which every element of the list must satisfy.
|
|
final Matcher elementMatcher;
|
|
|
|
/// Iterable matcher which we use to test the contents of the list.
|
|
final Matcher iterableMatcher;
|
|
|
|
_ListOf(this.elementMatcher) : iterableMatcher = everyElement(elementMatcher);
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add('List of ').addDescriptionOf(elementMatcher);
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
item,
|
|
Description mismatchDescription,
|
|
Map<Object?, Object?> matchState,
|
|
bool verbose,
|
|
) {
|
|
if (item is! List) {
|
|
return super.describeMismatch(
|
|
item,
|
|
mismatchDescription,
|
|
matchState,
|
|
verbose,
|
|
);
|
|
} else {
|
|
return iterableMatcher.describeMismatch(
|
|
item,
|
|
mismatchDescription,
|
|
matchState,
|
|
verbose,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool matches(item, Map<Object?, Object?> matchState) {
|
|
if (item is! List) {
|
|
return false;
|
|
}
|
|
return iterableMatcher.matches(item, matchState);
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches a map of objects, where each key/value pair in the
|
|
/// map satisfies the given key and value matchers.
|
|
class _MapOf extends _RecursiveMatcher {
|
|
/// Matcher which every key in the map must satisfy.
|
|
final Matcher keyMatcher;
|
|
|
|
/// Matcher which every value in the map must satisfy.
|
|
final Matcher valueMatcher;
|
|
|
|
_MapOf(this.keyMatcher, this.valueMatcher);
|
|
|
|
@override
|
|
Description describe(Description description) => description
|
|
.add('Map from ')
|
|
.addDescriptionOf(keyMatcher)
|
|
.add(' to ')
|
|
.addDescriptionOf(valueMatcher);
|
|
|
|
@override
|
|
void populateMismatches(item, List<MismatchDescriber> mismatches) {
|
|
if (item is! Map) {
|
|
mismatches.add(simpleDescription('is not a map'));
|
|
return;
|
|
}
|
|
item.forEach((key, value) {
|
|
checkSubstructure(
|
|
key,
|
|
keyMatcher,
|
|
mismatches,
|
|
(Description description) =>
|
|
description.add('key ').addDescriptionOf(key),
|
|
);
|
|
checkSubstructure(
|
|
value,
|
|
valueMatcher,
|
|
mismatches,
|
|
(Description description) =>
|
|
description.add('field ').addDescriptionOf(key),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches a union of different types, each of which is described
|
|
/// by a matcher.
|
|
class _OneOf extends Matcher {
|
|
/// Matchers for the individual choices.
|
|
final List<Matcher> choiceMatchers;
|
|
|
|
_OneOf(this.choiceMatchers);
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
for (var i = 0; i < choiceMatchers.length; i++) {
|
|
if (i != 0) {
|
|
if (choiceMatchers.length == 2) {
|
|
description = description.add(' or ');
|
|
} else {
|
|
description = description.add(', ');
|
|
if (i == choiceMatchers.length - 1) {
|
|
description = description.add('or ');
|
|
}
|
|
}
|
|
}
|
|
description = description.addDescriptionOf(choiceMatchers[i]);
|
|
}
|
|
return description;
|
|
}
|
|
|
|
@override
|
|
bool matches(item, Map<Object?, Object?> matchState) {
|
|
for (var choiceMatcher in choiceMatchers) {
|
|
var subState = {};
|
|
if (choiceMatcher.matches(item, subState)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Base class for matchers that operate by recursing through the contents of
|
|
/// an object.
|
|
abstract class _RecursiveMatcher extends Matcher {
|
|
const _RecursiveMatcher();
|
|
|
|
/// Check the type of a substructure whose value is [item], using [matcher].
|
|
/// If it doesn't match, record a closure in [mismatches] which can describe
|
|
/// the mismatch. [describeSubstructure] is used to describe which
|
|
/// substructure did not match.
|
|
void checkSubstructure(
|
|
Object? item,
|
|
Matcher matcher,
|
|
List<MismatchDescriber> mismatches,
|
|
Description Function(Description) describeSubstructure,
|
|
) {
|
|
var subState = {};
|
|
if (!matcher.matches(item, subState)) {
|
|
mismatches.add((Description mismatchDescription) {
|
|
mismatchDescription = mismatchDescription.add('contains malformed ');
|
|
mismatchDescription = describeSubstructure(mismatchDescription);
|
|
mismatchDescription = mismatchDescription
|
|
.add(' (should be ')
|
|
.addDescriptionOf(matcher);
|
|
var subDescription =
|
|
matcher
|
|
.describeMismatch(item, StringDescription(), subState, false)
|
|
.toString();
|
|
if (subDescription.isNotEmpty) {
|
|
mismatchDescription = mismatchDescription
|
|
.add('; ')
|
|
.add(subDescription);
|
|
}
|
|
return mismatchDescription.add(')');
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
Object? item,
|
|
Description mismatchDescription,
|
|
Map<Object?, Object?> matchState,
|
|
bool verbose,
|
|
) {
|
|
var mismatches = matchState['mismatches'] as List<MismatchDescriber>?;
|
|
if (mismatches != null) {
|
|
for (var i = 0; i < mismatches.length; i++) {
|
|
var mismatch = mismatches[i];
|
|
if (i > 0) {
|
|
if (mismatches.length == 2) {
|
|
mismatchDescription = mismatchDescription.add(' and ');
|
|
} else if (i == mismatches.length - 1) {
|
|
mismatchDescription = mismatchDescription.add(', and ');
|
|
} else {
|
|
mismatchDescription = mismatchDescription.add(', ');
|
|
}
|
|
}
|
|
mismatchDescription = mismatch(mismatchDescription);
|
|
}
|
|
return mismatchDescription;
|
|
} else {
|
|
return super.describeMismatch(
|
|
item,
|
|
mismatchDescription,
|
|
matchState,
|
|
verbose,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool matches(item, Map<Object?, Object?> matchState) {
|
|
var mismatches = <MismatchDescriber>[];
|
|
populateMismatches(item, mismatches);
|
|
if (mismatches.isEmpty) {
|
|
return true;
|
|
} else {
|
|
addStateInfo(matchState, {'mismatches': mismatches});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Populate [mismatches] with descriptions of all the ways in which [item]
|
|
/// does not match.
|
|
void populateMismatches(Object? item, List<MismatchDescriber> mismatches);
|
|
|
|
/// Create a [MismatchDescriber] describing a mismatch with a simple string.
|
|
MismatchDescriber simpleDescription(String description) => (
|
|
Description mismatchDescription,
|
|
) {
|
|
mismatchDescription.add(description);
|
|
return mismatchDescription;
|
|
};
|
|
}
|