| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:meta/meta.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:process/process.dart'; | |
| import '../artifacts.dart'; | |
| import '../base/common.dart'; | |
| import '../base/config.dart'; | |
| import '../base/file_system.dart'; | |
| import '../base/io.dart'; | |
| import '../base/logger.dart'; | |
| import '../base/os.dart'; | |
| import '../base/platform.dart'; | |
| import '../base/process.dart'; | |
| import '../base/template.dart'; | |
| import '../base/terminal.dart'; | |
| import '../build_info.dart'; | |
| import '../compile.dart'; | |
| import '../context/tool_context.dart'; | |
| import '../convert.dart'; | |
| import '../device.dart'; | |
| import '../native_assets.dart'; | |
| import '../project.dart'; | |
| import '../web/chrome.dart'; | |
| import '../web/memory_fs.dart'; | |
| import 'flutter_platform.dart' as loader; | |
| import 'flutter_web_platform.dart'; | |
| import 'font_config_manager.dart'; | |
| import 'test_config.dart'; | |
| import 'test_time_recorder.dart'; | |
| import 'test_wrapper.dart'; | |
| import 'watcher.dart'; | |
| import 'web_test_compiler.dart'; | |
| /// Immutable specification of test filtering, selection, and sharding options for `package:test`. | |
| @immutable | |
| class TestFilterSpecification { | |
| const TestFilterSpecification({ | |
| this.names = const <String>[], | |
| this.plainNames = const <String>[], | |
| this.tags = const <String>[], | |
| this.excludeTags = const <String>[], | |
| this.presets = const <String>[], | |
| this.runSkipped = false, | |
| this.shardIndex, | |
| this.totalShards, | |
| }); | |
| final List<String> names; | |
| final List<String> plainNames; | |
| final List<String> tags; | |
| final List<String> excludeTags; | |
| final List<String> presets; | |
| final bool runSkipped; | |
| final int? shardIndex; | |
| final int? totalShards; | |
| } | |
| /// Immutable specification consolidating execution, reporting, and engine parameters | |
| /// for [FlutterTestRunner]. | |
| @immutable | |
| class TestExecutionSpecification { | |
| const TestExecutionSpecification({ | |
| required this.debuggingOptions, | |
| required this.buildInfo, | |
| this.filter = const TestFilterSpecification(), | |
| this.enableVmService = false, | |
| this.machine = false, | |
| this.precompiledDillPath, | |
| this.precompiledDillFiles, | |
| this.updateGoldens = false, | |
| this.watcher, | |
| this.concurrency, | |
| this.testAssetDirectory, | |
| this.flutterProject, | |
| this.icudtlPath, | |
| this.coverageDirectory, | |
| this.web = false, | |
| this.randomSeed, | |
| this.reporter, | |
| this.fileReporter, | |
| this.timeout, | |
| this.ignoreTimeouts = false, | |
| this.failFast = false, | |
| this.integrationTestDevice, | |
| this.integrationTestUserIdentifier, | |
| this.testTimeRecorder, | |
| this.nativeAssetsBuilder, | |
| }); | |
| final DebuggingOptions debuggingOptions; | |
| final BuildInfo buildInfo; | |
| final TestFilterSpecification filter; | |
| final bool enableVmService; | |
| final bool machine; | |
| final String? precompiledDillPath; | |
| final Map<String, String>? precompiledDillFiles; | |
| final bool updateGoldens; | |
| final TestWatcher? watcher; | |
| final int? concurrency; | |
| final String? testAssetDirectory; | |
| final FlutterProject? flutterProject; | |
| final String? icudtlPath; | |
| final Directory? coverageDirectory; | |
| final bool web; | |
| final String? randomSeed; | |
| final String? reporter; | |
| final String? fileReporter; | |
| final String? timeout; | |
| final bool ignoreTimeouts; | |
| final bool failFast; | |
| final Device? integrationTestDevice; | |
| final String? integrationTestUserIdentifier; | |
| final TestTimeRecorder? testTimeRecorder; | |
| final TestCompilerNativeAssetsBuilder? nativeAssetsBuilder; | |
| /// Computes the command-line arguments passed to `package:test`. | |
| List<String> toPackageTestArgs({required bool supportsColor, bool pauseAfterLoad = false}) { | |
| return <String>[ | |
| if (!supportsColor) '--no-color', | |
| if (pauseAfterLoad) '--pause-after-load', | |
| if (machine) ...<String>['-r', 'json'] else if (reporter != null) ...<String>[ | |
| '-r', | |
| reporter!, | |
| ], | |
| if (fileReporter != null) '--file-reporter=$fileReporter', | |
| if (timeout != null) ...<String>['--timeout', timeout!], | |
| if (ignoreTimeouts) '--ignore-timeouts', | |
| if (concurrency != null) '--concurrency=$concurrency', | |
| for (final String name in filter.names) ...<String>['--name', name], | |
| for (final String plainName in filter.plainNames) ...<String>['--plain-name', plainName], | |
| if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', | |
| for (final String tag in filter.tags) ...<String>['--tags', tag], | |
| for (final String excludeTag in filter.excludeTags) ...<String>['--exclude-tags', excludeTag], | |
| for (final String preset in filter.presets) ...<String>['--preset', preset], | |
| if (failFast) '--fail-fast', | |
| if (filter.runSkipped) '--run-skipped', | |
| if (filter.totalShards != null) '--total-shards=${filter.totalShards}', | |
| if (filter.shardIndex != null) '--shard-index=${filter.shardIndex}', | |
| '--chain-stack-traces', | |
| ]; | |
| } | |
| } | |
| /// Launching the `flutter_tester` process from the test runner. | |
| interface class FlutterTestRunner { | |
| const FlutterTestRunner({required this._templateRenderer, required this._toolContext}); | |
| final TemplateRenderer _templateRenderer; | |
| final ToolContext _toolContext; | |
| /// Runs tests using package:test and the Flutter engine. | |
| Future<int> runTests( | |
| TestWrapper testWrapper, | |
| List<Uri> testFiles, | |
| TestExecutionSpecification spec, | |
| ) async { | |
| final ToolContext( | |
| :Artifacts artifacts, | |
| :FileSystem fs, | |
| :Logger logger, | |
| :OperatingSystemUtils os, | |
| :Platform platform, | |
| :ProcessManager processManager, | |
| :AnsiTerminal terminal, | |
| ) = _toolContext; | |
| // Configure package:test to use the Flutter engine for child processes. | |
| final String flutterTesterBinPath = artifacts.getArtifactPath(Artifact.flutterTester); | |
| final TestExecutionSpecification( | |
| :DebuggingOptions debuggingOptions, | |
| :FlutterProject? flutterProject, | |
| ) = spec; | |
| // Compute the command-line arguments for package:test. | |
| final List<String> testArgs = spec.toPackageTestArgs( | |
| supportsColor: terminal.supportsColor, | |
| pauseAfterLoad: debuggingOptions.startPaused, | |
| ); | |
| if (spec.web) { | |
| // Unsupported for general Flutter developers. | |
| // This is only used by the Flutter Framework tests. | |
| // See: https://github.com/flutter/flutter/pull/65984. | |
| final String tempBuildDir = fs.systemTempDirectory | |
| .createTempSync('flutter_test.') | |
| .absolute | |
| .uri | |
| .toFilePath(); | |
| final WebMemoryFS result = await WebTestCompiler(toolContext: _toolContext).initialize( | |
| projectDirectory: flutterProject!.directory, | |
| testOutputDir: tempBuildDir, | |
| testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(), | |
| buildInfo: debuggingOptions.buildInfo, | |
| webRenderer: debuggingOptions.webConfig.renderer, | |
| useWasm: debuggingOptions.webConfig.useWasm, | |
| ); | |
| testArgs | |
| ..add('--platform=chrome') | |
| ..add('--') | |
| ..addAll(testFiles.map((Uri uri) => uri.toString())); | |
| testWrapper.registerPlatformPlugin(<Runtime>[Runtime.chrome], () { | |
| return FlutterWebPlatform.start( | |
| flutterProject.directory.path, | |
| buildDirectory: fs.directory(tempBuildDir), | |
| buildInfo: debuggingOptions.buildInfo, | |
| chromiumLauncher: ChromiumLauncher( | |
| fileSystem: fs, | |
| platform: platform, | |
| processManager: processManager, | |
| operatingSystemUtils: os, | |
| browserFinder: findChromeExecutable, | |
| logger: logger, | |
| ), | |
| crossOriginIsolation: debuggingOptions.webConfig.crossOriginIsolation, | |
| flutterProject: flutterProject, | |
| flutterTesterBinPath: flutterTesterBinPath, | |
| templateRenderer: _templateRenderer, | |
| toolContext: _toolContext, | |
| useWasm: debuggingOptions.webConfig.useWasm, | |
| webMemoryFS: result, | |
| webRenderer: debuggingOptions.webConfig.renderer, | |
| pauseAfterLoad: debuggingOptions.startPaused, | |
| testTimeRecorder: spec.testTimeRecorder, | |
| updateGoldens: spec.updateGoldens, | |
| ); | |
| }); | |
| await testWrapper.main(testArgs); | |
| return exitCode; | |
| } | |
| testArgs | |
| ..add('--') | |
| ..addAll(testFiles.map((Uri uri) => uri.toString())); | |
| final InternetAddressType serverType = debuggingOptions.ipv6 | |
| ? InternetAddressType.IPv6 | |
| : InternetAddressType.IPv4; | |
| final loader.FlutterPlatform platformInstance = loader.installHook( | |
| buildInfo: spec.buildInfo, | |
| debuggingOptions: debuggingOptions, | |
| flutterTesterBinPath: flutterTesterBinPath, | |
| templateRenderer: _templateRenderer, | |
| toolContext: _toolContext, | |
| enableVmService: spec.enableVmService, | |
| flutterProject: flutterProject, | |
| icudtlPath: spec.icudtlPath, | |
| integrationTestDevice: spec.integrationTestDevice, | |
| integrationTestUserIdentifier: spec.integrationTestUserIdentifier, | |
| machine: spec.machine, | |
| nativeAssetsBuilder: spec.nativeAssetsBuilder, | |
| precompiledDillFiles: spec.precompiledDillFiles, | |
| precompiledDillPath: spec.precompiledDillPath, | |
| projectRootDirectory: fs.currentDirectory.uri, | |
| serverType: serverType, | |
| testAssetDirectory: spec.testAssetDirectory, | |
| testTimeRecorder: spec.testTimeRecorder, | |
| testWrapper: testWrapper, | |
| updateGoldens: spec.updateGoldens, | |
| watcher: spec.watcher, | |
| ); | |
| try { | |
| logger.printTrace('running test package with arguments: $testArgs'); | |
| await testWrapper.main(testArgs); | |
| // test.main() sets dart:io's exitCode global. | |
| logger.printTrace('test package returned with exit code $exitCode'); | |
| return exitCode; | |
| } finally { | |
| await platformInstance.close(); | |
| } | |
| } | |
| // To compile root_test_isolate_spawner.dart and | |
| // child_test_isolate_spawner.dart successfully, we will need to pass a | |
| // package_config.json to the frontend server that contains the | |
| // union of package:test_core, package:ffi, and all the dependencies of the | |
| // project under test. This function generates such a package_config.json. | |
| static Future<void> _generateIsolateSpawningTesterPackageConfig({ | |
| required FlutterProject flutterProject, | |
| required File isolateSpawningTesterPackageConfigFile, | |
| required ToolContext toolContext, | |
| }) async { | |
| final FileSystem fs = toolContext.fs; | |
| final File packageConfigFile = fs | |
| .directory(flutterProject.directory.path) | |
| .childDirectory('.dart_tool') | |
| .childFile('package_config.json'); | |
| PackageConfig? projectPackageConfig; | |
| if (packageConfigFile.existsSync()) { | |
| projectPackageConfig = PackageConfig.parseBytes( | |
| packageConfigFile.readAsBytesSync(), | |
| packageConfigFile.absolute.uri, | |
| ); | |
| } else { | |
| // We can't use this directly, but need to manually check | |
| // `flutterProject.directory.path` first, as `findPackageConfig` is from a | |
| // different package which does not use package:file. This inhibits | |
| // mocking the file system. | |
| projectPackageConfig = await findPackageConfig(fs.directory(flutterProject.directory.path)); | |
| } | |
| if (projectPackageConfig == null) { | |
| throwToolExit('Could not find package config for ${flutterProject.directory.path}.'); | |
| } | |
| // The flutter_tools package_config.json is guaranteed to include | |
| // package:ffi and package:test_core. | |
| final File flutterToolsPackageConfigFile = fs | |
| .directory(fs.path.join(toolContext.cache.flutterRoot, 'packages', 'flutter_tools')) | |
| .childDirectory('.dart_tool') | |
| .childFile('package_config.json'); | |
| final PackageConfig flutterToolsPackageConfig = PackageConfig.parseBytes( | |
| flutterToolsPackageConfigFile.readAsBytesSync(), | |
| flutterToolsPackageConfigFile.absolute.uri, | |
| ); | |
| final mergedPackages = <Package>[...projectPackageConfig.packages]; | |
| final projectPackageNames = Set<String>.from(mergedPackages.map((Package p) => p.name)); | |
| for (final Package p in flutterToolsPackageConfig.packages) { | |
| if (!projectPackageNames.contains(p.name)) { | |
| mergedPackages.add(p); | |
| } | |
| } | |
| final mergedPackageConfig = PackageConfig(mergedPackages); | |
| final buffer = StringBuffer(); | |
| PackageConfig.writeString(mergedPackageConfig, buffer); | |
| isolateSpawningTesterPackageConfigFile.writeAsStringSync(buffer.toString()); | |
| } | |
| static void _generateChildTestIsolateSpawnerSourceFile( | |
| List<Uri> paths, { | |
| required bool autoUpdateGoldenFiles, | |
| required File childTestIsolateSpawnerSourceFile, | |
| required List<String> packageTestArgs, | |
| required ToolContext toolContext, | |
| }) { | |
| final ToolContext(:FileSystem fs, :Logger logger, :Platform platform) = toolContext; | |
| final testConfigPaths = <String, String>{}; | |
| final buffer = StringBuffer(); | |
| buffer.writeln(''' | |
| import 'dart:ffi'; | |
| import 'dart:isolate'; | |
| import 'dart:ui'; | |
| import 'package:ffi/ffi.dart'; | |
| import 'package:flutter_test/flutter_test.dart'; | |
| import 'package:stream_channel/isolate_channel.dart'; | |
| import 'package:test_api/backend.dart'; // flutter_ignore: test_api_import | |
| '''); | |
| String pathToImport(String path) { | |
| assert(path.endsWith('.dart')); | |
| return path | |
| .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_') | |
| .replaceRange(path.length - '.dart'.length, null, ''); | |
| } | |
| final testImports = <String, String>{}; | |
| final seenTestConfigPaths = <String>{}; | |
| for (final path in paths) { | |
| final String sanitizedPath = !path.path.endsWith('?') | |
| ? path.path | |
| : path.path.substring(0, path.path.length - 1); | |
| final String sanitizedImport = pathToImport(sanitizedPath); | |
| buffer.writeln("import '$sanitizedPath' as $sanitizedImport;"); | |
| testImports[sanitizedPath] = sanitizedImport; | |
| final File? testConfigFile = findTestConfigFile( | |
| fs.file( | |
| platform.isWindows | |
| ? sanitizedPath.replaceAll('/', r'\').replaceFirst(r'\', '') | |
| : sanitizedPath, | |
| ), | |
| logger, | |
| ); | |
| if (testConfigFile != null) { | |
| final String sanitizedTestConfigImport = pathToImport(testConfigFile.path); | |
| testConfigPaths[sanitizedImport] = sanitizedTestConfigImport; | |
| if (seenTestConfigPaths.add(testConfigFile.path)) { | |
| buffer.writeln( | |
| "import '${Uri.file(testConfigFile.path, windows: true)}' as $sanitizedTestConfigImport;", | |
| ); | |
| } | |
| } | |
| } | |
| buffer.writeln(); | |
| buffer.writeln('const List<String> packageTestArgs = <String>['); | |
| for (final arg in packageTestArgs) { | |
| buffer.writeln(" '$arg',"); | |
| } | |
| buffer.writeln('];'); | |
| buffer.writeln(); | |
| buffer.writeln('const List<String> testPaths = <String>['); | |
| for (final path in paths) { | |
| buffer.writeln(" '$path',"); | |
| } | |
| buffer.writeln('];'); | |
| buffer.writeln(); | |
| buffer.writeln(r''' | |
| @Native<Void Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'Spawn') | |
| external void _spawn(Pointer<Utf8> entrypoint, Pointer<Utf8> route); | |
| void spawn({required SendPort port, String entrypoint = 'main', String route = '/'}) { | |
| assert( | |
| entrypoint != 'main' || route != '/', | |
| 'Spawn should not be used to spawn main with the default route name', | |
| ); | |
| IsolateNameServer.registerPortWithName(port, route); | |
| _spawn(entrypoint.toNativeUtf8(), route.toNativeUtf8()); | |
| } | |
| '''); | |
| buffer.write(''' | |
| /// Runs on a spawned isolate. | |
| void createChannelAndConnect(String path, String name, Function testMain) { | |
| goldenFileComparator = LocalFileComparator(Uri.parse(path)); | |
| autoUpdateGoldenFiles = $autoUpdateGoldenFiles; | |
| final IsolateChannel<dynamic> channel = IsolateChannel<dynamic>.connectSend( | |
| IsolateNameServer.lookupPortByName(name)!, | |
| ); | |
| channel.pipe(RemoteListener.start(() => testMain)); | |
| } | |
| @pragma('vm:entry-point') | |
| void testMain() { | |
| final String route = PlatformDispatcher.instance.defaultRouteName; | |
| switch (route) { | |
| '''); | |
| for (final MapEntry<String, String> kvp in testImports.entries) { | |
| final String importName = kvp.value; | |
| final String path = kvp.key; | |
| final String? testConfigImport = testConfigPaths[importName]; | |
| if (testConfigImport != null) { | |
| buffer.writeln(" case '$importName':"); | |
| buffer.writeln( | |
| " createChannelAndConnect('$path', route, () => $testConfigImport.testExecutable($importName.main));", | |
| ); | |
| } else { | |
| buffer.writeln(" case '$importName':"); | |
| buffer.writeln(" createChannelAndConnect('$path', route, $importName.main);"); | |
| } | |
| } | |
| buffer.write(r''' | |
| } | |
| } | |
| @pragma('vm:entry-point') | |
| void main([dynamic sendPort]) { | |
| if (sendPort is SendPort) { | |
| final ReceivePort receivePort = ReceivePort(); | |
| receivePort.listen((dynamic msg) { | |
| switch (msg as List<dynamic>) { | |
| case ['spawn', final SendPort port, final String entrypoint, final String route]: | |
| spawn(port: port, entrypoint: entrypoint, route: route); | |
| case ['close']: | |
| receivePort.close(); | |
| } | |
| }); | |
| sendPort.send(<Object>[receivePort.sendPort, packageTestArgs, testPaths]); | |
| } | |
| } | |
| '''); | |
| childTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString()); | |
| } | |
| static void _generateRootTestIsolateSpawnerSourceFile({ | |
| required File childTestIsolateSpawnerDillFile, | |
| required File childTestIsolateSpawnerSourceFile, | |
| required Platform platform, | |
| required File rootTestIsolateSpawnerSourceFile, | |
| }) { | |
| final buffer = StringBuffer(); | |
| buffer.writeln(''' | |
| import 'dart:async'; | |
| import 'dart:ffi'; | |
| import 'dart:io' show exit, exitCode; // flutter_ignore: dart_io_import | |
| import 'dart:isolate'; | |
| import 'dart:ui'; | |
| import 'package:ffi/ffi.dart'; | |
| import 'package:stream_channel/isolate_channel.dart'; | |
| import 'package:stream_channel/stream_channel.dart'; | |
| import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports | |
| import 'package:test_core/src/platform.dart'; // ignore: implementation_imports | |
| @Native<Handle Function(Pointer<Utf8>)>(symbol: 'LoadLibraryFromKernel') | |
| external Object _loadLibraryFromKernel(Pointer<Utf8> path); | |
| @Native<Handle Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'LookupEntryPoint') | |
| external Object _lookupEntryPoint(Pointer<Utf8> library, Pointer<Utf8> name); | |
| late final List<String> packageTestArgs; | |
| late final List<String> testPaths; | |
| /// Runs on the main isolate. | |
| Future<void> registerPluginAndRun() { | |
| final SpawnPlugin platform = SpawnPlugin(); | |
| registerPlatformPlugin( | |
| <Runtime>[Runtime.vm], | |
| () { | |
| return platform; | |
| }, | |
| ); | |
| return test.main(<String>[...packageTestArgs, '--', ...testPaths]); | |
| } | |
| late final Isolate rootTestIsolate; | |
| late final SendPort commandPort; | |
| bool readyToRun = false; | |
| final Completer<void> readyToRunSignal = Completer<void>(); | |
| Future<void> spawn({ | |
| required SendPort port, | |
| String entrypoint = 'main', | |
| String route = '/', | |
| }) async { | |
| if (!readyToRun) { | |
| await readyToRunSignal.future; | |
| } | |
| commandPort.send(<Object>['spawn', port, entrypoint, route]); | |
| } | |
| @pragma('vm:entry-point') | |
| void main() async { | |
| final String route = PlatformDispatcher.instance.defaultRouteName; | |
| if (route == '/') { | |
| final ReceivePort port = ReceivePort(); | |
| port.listen((dynamic message) { | |
| final [SendPort sendPort, List<String> args, List<String> paths] = message as List<dynamic>; | |
| commandPort = sendPort; | |
| packageTestArgs = args; | |
| testPaths = paths; | |
| readyToRun = true; | |
| readyToRunSignal.complete(); | |
| }); | |
| rootTestIsolate = await Isolate.spawn( | |
| _loadLibraryFromKernel( | |
| r'${childTestIsolateSpawnerDillFile.absolute.path}' | |
| .toNativeUtf8()) as void Function(SendPort), | |
| port.sendPort, | |
| ); | |
| await readyToRunSignal.future; | |
| port.close(); // Not expecting anything else. | |
| await registerPluginAndRun(); | |
| // The [test.main] call in [registerPluginAndRun] sets dart:io's [exitCode] | |
| // global. | |
| exit(exitCode); | |
| } else { | |
| (_lookupEntryPoint( | |
| r'file://${childTestIsolateSpawnerSourceFile.absolute.uri.toFilePath(windows: false)}' | |
| .toNativeUtf8(), | |
| 'testMain'.toNativeUtf8()) as void Function())(); | |
| } | |
| } | |
| '''); | |
| buffer.write(r''' | |
| String pathToImport(String path) { | |
| assert(path.endsWith('.dart')); | |
| return path | |
| .replaceRange(path.length - '.dart'.length, null, '') | |
| .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_'); | |
| } | |
| class SpawnPlugin extends PlatformPlugin { | |
| SpawnPlugin(); | |
| final Map<String, IsolateChannel<dynamic>> _channels = <String, IsolateChannel<dynamic>>{}; | |
| Future<void> launchIsolate(String path) async { | |
| final String name = pathToImport(path); | |
| final ReceivePort port = ReceivePort(); | |
| _channels[name] = IsolateChannel<dynamic>.connectReceive(port); | |
| await spawn(port: port.sendPort, route: name); | |
| } | |
| @override | |
| Future<void> close() async { | |
| commandPort.send(<String>['close']); | |
| } | |
| '''); | |
| buffer.write(''' | |
| @override | |
| Future<RunnerSuite> load( | |
| String path, | |
| SuitePlatform platform, | |
| SuiteConfiguration suiteConfig, | |
| Object message, | |
| ) async { | |
| final String correctedPath = ${platform.isWindows ? r'"/$path"' : 'path'}; | |
| await launchIsolate(correctedPath); | |
| final StreamChannel<dynamic> channel = _channels[pathToImport(correctedPath)]!; | |
| final RunnerSuiteController controller = deserializeSuite(correctedPath, platform, | |
| suiteConfig, const PluginEnvironment(), channel, message); | |
| return controller.suite; | |
| } | |
| } | |
| '''); | |
| rootTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString()); | |
| } | |
| static Future<void> _compileFile({ | |
| required BuildInfo buildInfo, | |
| required File outputDillFile, | |
| required File sourceFile, | |
| required ToolContext toolContext, | |
| Uri? nativeAssetsYaml, | |
| TestTimeRecorder? testTimeRecorder, | |
| }) async { | |
| final ToolContext( | |
| :Artifacts artifacts, | |
| :Config config, | |
| :FileSystem fs, | |
| :Logger logger, | |
| :Platform platform, | |
| :ProcessManager processManager, | |
| :ShutdownHooks shutdownHooks, | |
| ) = toolContext; | |
| logger.printTrace('Compiling ${sourceFile.absolute.uri}'); | |
| final compilerTime = Stopwatch()..start(); | |
| final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Compile); | |
| final ResidentCompiler residentCompiler = residentCompilerFactory.create( | |
| targetPlatform: .tester, | |
| artifacts: artifacts, | |
| logger: logger, | |
| processManager: processManager, | |
| buildInfo: buildInfo, | |
| platform: platform, | |
| testCompilation: true, | |
| fileSystem: fs, | |
| shutdownHooks: shutdownHooks, | |
| config: config, | |
| ); | |
| await residentCompiler.recompile( | |
| sourceFile.absolute.uri, | |
| null, | |
| outputPath: outputDillFile.absolute.path, | |
| packageConfig: buildInfo.packageConfig, | |
| fs: fs, | |
| nativeAssetsYaml: nativeAssetsYaml, | |
| ); | |
| residentCompiler.accept(); | |
| logger.printTrace( | |
| 'Compiling ${sourceFile.absolute.uri} took ${compilerTime.elapsedMilliseconds}ms', | |
| ); | |
| testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!); | |
| } | |
| /// Runs tests using the experimental strategy of spawning each test in a | |
| /// separate lightweight Engine. | |
| Future<int> runTestsBySpawningLightweightEngines( | |
| List<Uri> testFiles, | |
| TestExecutionSpecification spec, | |
| ) async { | |
| final ToolContext( | |
| :Artifacts artifacts, | |
| :Config config, | |
| :FileSystem fs, | |
| :Logger logger, | |
| :Platform platform, | |
| :ProcessManager processManager, | |
| :Stdio stdio, | |
| :AnsiTerminal terminal, | |
| ) = _toolContext; | |
| assert(testFiles.length > 1); | |
| final TestExecutionSpecification( | |
| :FlutterProject flutterProject!, | |
| :DebuggingOptions debuggingOptions, | |
| :TestTimeRecorder? testTimeRecorder, | |
| :String? testAssetDirectory, | |
| :String? icudtlPath, | |
| :TestCompilerNativeAssetsBuilder? nativeAssetsBuilder, | |
| ) = spec; | |
| final Directory buildDirectory = fs.directory( | |
| fs.path.join(flutterProject.directory.path, getBuildDirectory(config, fs)), | |
| ); | |
| final Directory isolateSpawningTesterDirectory = buildDirectory.childDirectory( | |
| 'isolate_spawning_tester', | |
| ); | |
| isolateSpawningTesterDirectory.createSync(); | |
| final File isolateSpawningTesterPackageConfigFile = isolateSpawningTesterDirectory | |
| .childDirectory('.dart_tool') | |
| .childFile('package_config.json'); | |
| isolateSpawningTesterPackageConfigFile.createSync(recursive: true); | |
| await _generateIsolateSpawningTesterPackageConfig( | |
| flutterProject: flutterProject, | |
| isolateSpawningTesterPackageConfigFile: isolateSpawningTesterPackageConfigFile, | |
| toolContext: _toolContext, | |
| ); | |
| final PackageConfig isolateSpawningTesterPackageConfig = PackageConfig.parseBytes( | |
| isolateSpawningTesterPackageConfigFile.readAsBytesSync(), | |
| isolateSpawningTesterPackageConfigFile.absolute.uri, | |
| ); | |
| final File childTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile( | |
| 'child_test_isolate_spawner.dart', | |
| ); | |
| final File rootTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile( | |
| 'root_test_isolate_spawner.dart', | |
| ); | |
| final File childTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile( | |
| 'child_test_isolate_spawner.dill', | |
| ); | |
| final File rootTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile( | |
| 'root_test_isolate_spawner.dill', | |
| ); | |
| // Compute the command-line arguments for package:test. | |
| final List<String> packageTestArgs = spec.toPackageTestArgs( | |
| supportsColor: terminal.supportsColor, | |
| ); | |
| _generateChildTestIsolateSpawnerSourceFile( | |
| testFiles, | |
| autoUpdateGoldenFiles: spec.updateGoldens, | |
| childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile, | |
| packageTestArgs: packageTestArgs, | |
| toolContext: _toolContext, | |
| ); | |
| _generateRootTestIsolateSpawnerSourceFile( | |
| childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile, | |
| childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile, | |
| rootTestIsolateSpawnerSourceFile: rootTestIsolateSpawnerSourceFile, | |
| platform: platform, | |
| ); | |
| final BuildInfo buildInfo = debuggingOptions.buildInfo.copyWith( | |
| packageConfig: isolateSpawningTesterPackageConfig, | |
| packageConfigPath: isolateSpawningTesterPackageConfigFile.path, | |
| ); | |
| await _compileFile( | |
| buildInfo: buildInfo, | |
| outputDillFile: childTestIsolateSpawnerDillFile, | |
| sourceFile: childTestIsolateSpawnerSourceFile, | |
| toolContext: _toolContext, | |
| testTimeRecorder: testTimeRecorder, | |
| ); | |
| await _compileFile( | |
| buildInfo: buildInfo, | |
| outputDillFile: rootTestIsolateSpawnerDillFile, | |
| sourceFile: rootTestIsolateSpawnerSourceFile, | |
| toolContext: _toolContext, | |
| testTimeRecorder: testTimeRecorder, | |
| ); | |
| final command = <String>[ | |
| artifacts.getArtifactPath(Artifact.flutterTester), | |
| '--disable-vm-service', | |
| if (icudtlPath != null) '--icu-data-file-path=$icudtlPath', | |
| '--enable-checked-mode', | |
| '--verify-entry-points', | |
| '--enable-software-rendering', | |
| '--skia-deterministic-rendering', | |
| if (debuggingOptions.enableDartProfiling) '--enable-dart-profiling', | |
| '--non-interactive', | |
| '--use-test-fonts', | |
| '--disable-asset-fonts', | |
| '--packages=${debuggingOptions.buildInfo.packageConfigPath}', | |
| if (testAssetDirectory != null) '--flutter-assets-dir=$testAssetDirectory', | |
| ...debuggingOptions.dartEntrypointArgs, | |
| rootTestIsolateSpawnerDillFile.absolute.path, | |
| ]; | |
| // If the FLUTTER_TEST environment variable has been set, then pass it on | |
| // for package:flutter_test to handle the value. | |
| // | |
| // If FLUTTER_TEST has not been set, assume from this context that this | |
| // call was invoked by the command 'flutter test'. | |
| final String flutterTest = platform.environment.containsKey('FLUTTER_TEST') | |
| ? platform.environment['FLUTTER_TEST']! | |
| : 'true'; | |
| final environment = <String, String>{ | |
| 'FLUTTER_TEST': flutterTest, | |
| 'FONTCONFIG_FILE': FontConfigManager(toolContext: _toolContext).fontConfigFile.path, | |
| 'APP_NAME': flutterProject.manifest.appName, | |
| 'UNIT_TEST_ASSETS': ?testAssetDirectory, | |
| if (nativeAssetsBuilder != null && platform.isWindows) | |
| 'PATH': | |
| '${nativeAssetsBuilder.windowsBuildDirectory(flutterProject, toolContext: _toolContext)};${platform.environment['PATH']}', | |
| }; | |
| logger.printTrace( | |
| 'Starting flutter_tester process with command=$command, environment=$environment', | |
| ); | |
| final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Run); | |
| final Process process = await processManager.start(command, environment: environment); | |
| logger.printTrace('Started flutter_tester process at pid ${process.pid}'); | |
| for (final stream in <Stream<List<int>>>[process.stderr, process.stdout]) { | |
| // Use permissive decoder for test output which may contain invalid UTF-8 | |
| stream.transform<String>(utf8AllowMalformed.decoder).listen(stdio.stdoutWrite); | |
| } | |
| return process.exitCode.then((int exitCode) { | |
| testTimeRecorder?.stop(TestTimePhases.Run, testTimeRecorderStopwatch!); | |
| logger.printTrace('flutter_tester process at pid ${process.pid} exited with code=$exitCode'); | |
| return exitCode; | |
| }); | |
| } | |
| } |