// 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? 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? 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 dispose() async { _process?.kill(); await _process?.exitCode; _process = null; await deleteDirectory(root); } Future kill() async { _process?.kill(); _process = null; } Future runAnalyze( List arguments, { String? workingDir, }) async { return run(['analyze', '--suppress-analytics', ...arguments]); } Future runFix( List arguments, { String? workingDir, }) async { return run(['fix', '--suppress-analytics', ...arguments]); } Future run( List 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 start( List arguments, { String? workingDir, }) { return Process.start( Platform.resolvedExecutable, [ ...arguments, ], workingDirectory: workingDir ?? dir.path, environment: { 'PUB_CACHE': pubCachePath, }) ..then((p) => _process = p); } Future runWithVmService( List arguments, void Function(String) onStdout, { String? workingDir, }) async { final process = await start(arguments, workingDir: workingDir); final completer = Completer(); late StreamSubscription sub; late StreamSubscription 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 runLocalWithFakeAnalytics( List 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; } } Future 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'); }