2da102b4e9
`connected_app_service_test` went from flaky to always failing on Windows recently (on what seems like a completely unrelated CL of mine). The cause is failure to delete this directory due to file locks, which is something we often see on Windows and usually wrap deletes with some retries instead of immediately failing. This adds the same here, Change-Id: I3f9b4f52d2327c3b3ef3e70674059366caa26093 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/461420 Commit-Queue: Ben Konyi <bkonyi@google.com> Reviewed-by: Jessy Yameogo <yjessy@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com>
367 lines
10 KiB
Dart
367 lines
10 KiB
Dart
// Copyright (c) 2020, 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:convert';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:cli_util/cli_logging.dart';
|
|
import 'package:dartdev/dartdev.dart';
|
|
import 'package:dartdev/src/core.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:unified_analytics/unified_analytics.dart';
|
|
|
|
/// A long [Timeout] is provided for tests that start a process on
|
|
/// `bin/dartdev.dart` as the command is not compiled ahead of time, and each
|
|
/// invocation requires the VM to compile the entire dependency graph.
|
|
const Timeout longTimeout = Timeout(Duration(minutes: 5));
|
|
|
|
/// This version of dart is the last guaranteed pre-null safety language
|
|
/// version:
|
|
const String dartVersionFilePrefix2_9 = '''
|
|
// ignore: illegal_language_version_override
|
|
// @dart = 2.9
|
|
''';
|
|
|
|
/// Return the root URI of the SDK by walking up from the pkg/dartdev folder.
|
|
final sdkRootUri = resolveDartDevUri('../../');
|
|
|
|
void initGlobalState() {
|
|
log = Logger.standard();
|
|
}
|
|
|
|
/// Creates a test-project in a temp-dir that will [dispose] itself at the end
|
|
/// of the test.
|
|
TestProject project(
|
|
{String? mainSrc,
|
|
String? analysisOptions,
|
|
String name = TestProject._defaultProjectName,
|
|
VersionConstraint? sdkConstraint,
|
|
Map<String, dynamic>? pubspecExtras}) {
|
|
var testProject = TestProject(
|
|
mainSrc: mainSrc,
|
|
name: name,
|
|
analysisOptions: analysisOptions,
|
|
sdkConstraint: sdkConstraint,
|
|
pubspecExtras: pubspecExtras);
|
|
addTearDown(() => testProject.dispose());
|
|
return testProject;
|
|
}
|
|
|
|
class TestProject {
|
|
static const String _defaultProjectName = 'dartdev_temp';
|
|
|
|
late Directory root;
|
|
|
|
Directory get dir => Directory(dirPath);
|
|
|
|
String get dirPath => path.join(root.path, 'myapp');
|
|
|
|
String get pubCachePath => path.join(root.path, 'pub_cache');
|
|
|
|
String get pubCacheBinPath => path.join(pubCachePath, 'bin');
|
|
|
|
String get mainPath => path.join(dirPath, relativeFilePath);
|
|
|
|
String get analysisOptionsPath => path.join(dirPath, 'analysis_options.yaml');
|
|
|
|
String get packageConfigPath => path.join(
|
|
dirPath,
|
|
'.dart_tool',
|
|
'package_config.json',
|
|
);
|
|
|
|
final String name;
|
|
|
|
String get relativeFilePath => 'lib/main.dart';
|
|
|
|
Process? _process;
|
|
|
|
TestProject({
|
|
String? mainSrc,
|
|
String? analysisOptions,
|
|
this.name = _defaultProjectName,
|
|
VersionConstraint? sdkConstraint,
|
|
Map<String, dynamic>? pubspecExtras,
|
|
}) {
|
|
initGlobalState();
|
|
root = Directory.systemTemp.createTempSync('dartdev');
|
|
file(
|
|
'pubspec.yaml',
|
|
JsonEncoder.withIndent(' ').convert(
|
|
{
|
|
'name': name,
|
|
'environment': {'sdk': sdkConstraint?.toString() ?? '^3.0.0'},
|
|
...?pubspecExtras,
|
|
},
|
|
),
|
|
);
|
|
file(
|
|
'.dart_tool/package_config.json',
|
|
JsonEncoder.withIndent(' ').convert(
|
|
{
|
|
'configVersion': 2,
|
|
'generator': 'utils.dart',
|
|
'packages': [
|
|
{
|
|
'name': name,
|
|
'rootUri': '../',
|
|
'packageUri': 'lib/',
|
|
'languageVersion': '3.2',
|
|
},
|
|
],
|
|
},
|
|
),
|
|
);
|
|
file(
|
|
'.dart_tool/package_graph.json',
|
|
JsonEncoder.withIndent(' ').convert({
|
|
'roots': [name],
|
|
'packages': [
|
|
{
|
|
'name': name,
|
|
'version': '1.0.0',
|
|
'dependencies': [],
|
|
'devDependencies': []
|
|
},
|
|
],
|
|
'configVersion': 1
|
|
}),
|
|
);
|
|
if (analysisOptions != null) {
|
|
file('analysis_options.yaml', analysisOptions);
|
|
}
|
|
if (mainSrc != null) {
|
|
file(relativeFilePath, mainSrc);
|
|
}
|
|
}
|
|
|
|
void file(String name, String contents) {
|
|
var file = File(path.join(dir.path, name));
|
|
file.parent.createSync(recursive: true);
|
|
file.writeAsStringSync(contents);
|
|
}
|
|
|
|
void deleteFile(String name) {
|
|
var file = File(path.join(dir.path, name));
|
|
assert(file.existsSync());
|
|
file.deleteSync();
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
_process?.kill();
|
|
await _process?.exitCode;
|
|
_process = null;
|
|
await deleteDirectory(root);
|
|
}
|
|
|
|
Future<void> kill() async {
|
|
_process?.kill();
|
|
_process = null;
|
|
}
|
|
|
|
Future<ProcessResult> runAnalyze(
|
|
List<String> arguments, {
|
|
String? workingDir,
|
|
}) async {
|
|
return run(['analyze', '--suppress-analytics', ...arguments]);
|
|
}
|
|
|
|
Future<ProcessResult> runFix(
|
|
List<String> arguments, {
|
|
String? workingDir,
|
|
}) async {
|
|
return run(['fix', '--suppress-analytics', ...arguments]);
|
|
}
|
|
|
|
Future<ProcessResult> run(
|
|
List<String> arguments, {
|
|
String? workingDir,
|
|
}) async {
|
|
final process = await start(arguments, workingDir: workingDir);
|
|
final stdoutContents = process.stdout.transform(utf8.decoder).join();
|
|
final stderrContents = process.stderr.transform(utf8.decoder).join();
|
|
final code = await process.exitCode;
|
|
return ProcessResult(
|
|
process.pid,
|
|
code,
|
|
await stdoutContents,
|
|
await stderrContents,
|
|
);
|
|
}
|
|
|
|
Future<Process> start(
|
|
List<String> arguments, {
|
|
String? workingDir,
|
|
}) {
|
|
return Process.start(
|
|
Platform.resolvedExecutable,
|
|
[
|
|
...arguments,
|
|
],
|
|
workingDirectory: workingDir ?? dir.path,
|
|
environment: {
|
|
'PUB_CACHE': pubCachePath,
|
|
})
|
|
..then((p) => _process = p);
|
|
}
|
|
|
|
Future<void> runWithVmService(
|
|
List<String> arguments,
|
|
void Function(String) onStdout, {
|
|
String? workingDir,
|
|
}) async {
|
|
final process = await start(arguments, workingDir: workingDir);
|
|
final completer = Completer<void>();
|
|
late StreamSubscription<String> sub;
|
|
late StreamSubscription<String> subError;
|
|
|
|
void onData(event) {
|
|
print('stdout: $event');
|
|
onStdout(event);
|
|
}
|
|
|
|
void onStderr(event) async {
|
|
print('stderr: $event');
|
|
await subError.cancel();
|
|
await sub.cancel();
|
|
completer.complete();
|
|
fail('stderr is expected to be empty');
|
|
}
|
|
|
|
void onError(error) async {
|
|
kill();
|
|
await subError.cancel();
|
|
await sub.cancel();
|
|
completer.complete();
|
|
}
|
|
|
|
void onDone() async {
|
|
await subError.cancel();
|
|
await sub.cancel();
|
|
completer.complete();
|
|
}
|
|
|
|
sub = process.stdout
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())
|
|
.listen(onData, onError: onError, onDone: onDone);
|
|
subError = process.stderr.transform(utf8.decoder).listen(onStderr);
|
|
|
|
// Wait for process to start and run.
|
|
await completer.future;
|
|
}
|
|
|
|
Future<FakeAnalytics> runLocalWithFakeAnalytics(
|
|
List<String> arguments, {
|
|
String? workingDir,
|
|
}) async {
|
|
final analytics = _createFakeAnalytics();
|
|
|
|
final originalDir = Directory.current;
|
|
Directory.current = dir;
|
|
|
|
final runner = DartdevRunner(arguments, analyticsOverride: analytics);
|
|
await runner.runCommand(runner.parse(arguments));
|
|
|
|
Directory.current = originalDir;
|
|
return analytics;
|
|
}
|
|
|
|
FakeAnalytics _createFakeAnalytics() {
|
|
final fs = MemoryFileSystem.test(style: FileSystemStyle.posix);
|
|
final homeDirectory = fs.directory('/');
|
|
final FakeAnalytics initialAnalytics = Analytics.fake(
|
|
tool: DashTool.dartTool,
|
|
homeDirectory: homeDirectory,
|
|
dartVersion: 'dartVersion',
|
|
fs: fs,
|
|
);
|
|
initialAnalytics.clientShowedMessage();
|
|
|
|
return Analytics.fake(
|
|
tool: DashTool.dartTool,
|
|
homeDirectory: homeDirectory,
|
|
dartVersion: 'dartVersion',
|
|
fs: fs,
|
|
);
|
|
}
|
|
|
|
String get absolutePathToDartdevFile =>
|
|
sdkRootUri.resolve('pkg/dartdev/bin/dartdev.dart').toFilePath();
|
|
|
|
Directory? findDirectory(String name) {
|
|
var directory = Directory(path.join(dir.path, name));
|
|
return directory.existsSync() ? directory : null;
|
|
}
|
|
|
|
File? findFile(String name) {
|
|
var file = File(path.join(dir.path, name));
|
|
return file.existsSync() ? file : null;
|
|
}
|
|
}
|
|
|
|
/// Deletes [dir] with some retries to reduce the chances of file locking errors
|
|
/// on Windows where files may fail to delete for a short period after the
|
|
/// process that locked them terminates.
|
|
Future<void> deleteDirectory(Directory dir) async {
|
|
int deleteAttempts = 5;
|
|
while (deleteAttempts >= 0) {
|
|
deleteAttempts--;
|
|
try {
|
|
if (!dir.existsSync()) {
|
|
return;
|
|
}
|
|
dir.deleteSync(recursive: true);
|
|
} catch (e) {
|
|
if (deleteAttempts <= 0) {
|
|
rethrow;
|
|
}
|
|
await Future.delayed(Duration(milliseconds: 500));
|
|
log.stdout('Got $e while deleting $dir. Trying again...');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks that this is the `dart` executable in the bin folder rather than the
|
|
/// `dart` in the root of the build folder.
|
|
///
|
|
/// Many of this package tests rely on having the SDK folder layout.
|
|
void ensureRunFromSdkBinDart() {
|
|
final uri = Uri.file(Platform.resolvedExecutable);
|
|
final pathReversed = uri.pathSegments.reversed.toList();
|
|
if (!pathReversed[0].startsWith('dart')) {
|
|
throw StateError('Main executable is not Dart: ${uri.toFilePath()}.');
|
|
}
|
|
if (pathReversed.length < 2 || pathReversed[1] != 'bin') {
|
|
throw StateError(
|
|
'''Main executable is not from an SDK build: ${uri.toFilePath()}.
|
|
The `pkg/dartdev` tests must be run with the `dart` executable in the `bin` folder.
|
|
''');
|
|
}
|
|
}
|
|
|
|
/// Replaces the path [filePath] case-insensitively in [input] with itself.
|
|
///
|
|
/// This allows comparing strings that contain file paths where the casing may
|
|
/// be different but is not important to the test (for example where a drive
|
|
/// letter might have different casing).
|
|
String replacePathsWithMatchingCase(String input, {required String filePath}) {
|
|
return input.replaceAll(
|
|
RegExp(RegExp.escape(filePath), caseSensitive: false),
|
|
filePath,
|
|
);
|
|
}
|
|
|
|
/// Resolves a relative URI from the pkg/dartdev folder.
|
|
Uri resolveDartDevUri(String path) {
|
|
final dartDevLibUri =
|
|
Isolate.resolvePackageUriSync(Uri.parse('package:dartdev/'));
|
|
return dartDevLibUri!.resolve('../$path');
|
|
}
|