| // 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: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 | |
| '''; | |
| const residentFrontendCompilerPrefix = | |
| 'The Resident Frontend Compiler is listening at 127.0.0.1:'; | |
| /// Return the root URI of the SDK by walking up from the pkg/dartdev folder. | |
| final sdkRootUri = resolveDartDevUri('../../'); | |
| /// 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, | |
| String? languageVersion, | |
| Map<String, dynamic>? pubspecExtras, | |
| }) { | |
| var testProject = TestProject( | |
| mainSrc: mainSrc, | |
| name: name, | |
| analysisOptions: analysisOptions, | |
| sdkConstraint: sdkConstraint, | |
| languageVersion: languageVersion, | |
| 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); | |
| Uri get mainUri => Uri.file(mainPath); | |
| 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 => path.join('lib', 'main.dart'); | |
| Process? _process; | |
| TestProject({ | |
| String? mainSrc, | |
| String? analysisOptions, | |
| this.name = _defaultProjectName, | |
| VersionConstraint? sdkConstraint, | |
| String? languageVersion, | |
| Map<String, dynamic>? pubspecExtras, | |
| }) { | |
| root = Directory.systemTemp | |
| .createTempSync('dartdev') | |
| .withUppercaseDriveLetter; | |
| 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': 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, flush: true); | |
| } | |
| 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> runMigrate( | |
| List<String> arguments, { | |
| String? workingDir, | |
| }) async { | |
| return run(['migrate', '--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'); | |
| if (event.contains(residentFrontendCompilerPrefix)) { | |
| onStdout(event); | |
| return; | |
| } | |
| await subError.cancel(); | |
| await sub.cancel(); | |
| completer.complete(); | |
| fail('stderr is expected to be empty, but got: $event'); | |
| } | |
| 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}) { | |
| var output = input.replaceAll( | |
| RegExp(RegExp.escape(filePath), caseSensitive: false), | |
| filePath, | |
| ); | |
| try { | |
| final canonicalPath = Directory(filePath).resolveSymbolicLinksSync(); | |
| if (canonicalPath != filePath) { | |
| output = output.replaceAll( | |
| RegExp(RegExp.escape(canonicalPath), caseSensitive: false), | |
| filePath, | |
| ); | |
| } | |
| } catch (_) {} | |
| return output; | |
| } | |
| /// 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'); | |
| } | |
| extension on Directory { | |
| /// Returns the directory with an uppercase drive letter on Windows. | |
| /// | |
| /// This avoids some mismatches on machines where the temp folder has a | |
| /// lowercase drive letter and get back uppercase from the server. | |
| Directory get withUppercaseDriveLetter { | |
| return Platform.isWindows | |
| ? Directory(this.path[0].toUpperCase() + this.path.substring(1)) | |
| : this; | |
| } | |
| } |