// 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:package_config/package_config.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../build_info.dart'; import '../cache.dart'; import '../compile.dart'; import '../convert.dart'; import '../device.dart'; import '../globals.dart' as globals; 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'; /// A class that abstracts launching the test process from the test runner. abstract class FlutterTestRunner { const factory FlutterTestRunner() = _FlutterTestRunnerImpl; /// Runs tests using package:test and the Flutter engine. Future runTests( TestWrapper testWrapper, List testFiles, { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], String? tags, String? excludeTags, bool enableVmService = false, bool machine = false, String? precompiledDillPath, Map? precompiledDillFiles, bool updateGoldens = false, TestWatcher? watcher, required int? concurrency, String? testAssetDirectory, FlutterProject? flutterProject, String? icudtlPath, Directory? coverageDirectory, bool web = false, String? randomSeed, String? reporter, String? fileReporter, String? timeout, bool failFast = false, bool runSkipped = false, int? shardIndex, int? totalShards, Device? integrationTestDevice, String? integrationTestUserIdentifier, TestTimeRecorder? testTimeRecorder, TestCompilerNativeAssetsBuilder? nativeAssetsBuilder, BuildInfo? buildInfo, }); /// Runs tests using the experimental strategy of spawning each test in a /// separate lightweight Engine. Future runTestsBySpawningLightweightEngines( List testFiles, { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], String? tags, String? excludeTags, bool machine = false, bool updateGoldens = false, required int? concurrency, String? testAssetDirectory, FlutterProject? flutterProject, String? icudtlPath, String? randomSeed, String? reporter, String? fileReporter, String? timeout, bool failFast = false, bool runSkipped = false, int? shardIndex, int? totalShards, TestTimeRecorder? testTimeRecorder, TestCompilerNativeAssetsBuilder? nativeAssetsBuilder, }); } class _FlutterTestRunnerImpl implements FlutterTestRunner { const _FlutterTestRunnerImpl(); @override Future runTests( TestWrapper testWrapper, List testFiles, { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], String? tags, String? excludeTags, bool enableVmService = false, bool machine = false, String? precompiledDillPath, Map? precompiledDillFiles, bool updateGoldens = false, TestWatcher? watcher, required int? concurrency, String? testAssetDirectory, FlutterProject? flutterProject, String? icudtlPath, Directory? coverageDirectory, bool web = false, String? randomSeed, String? reporter, String? fileReporter, String? timeout, bool failFast = false, bool runSkipped = false, int? shardIndex, int? totalShards, Device? integrationTestDevice, String? integrationTestUserIdentifier, TestTimeRecorder? testTimeRecorder, TestCompilerNativeAssetsBuilder? nativeAssetsBuilder, BuildInfo? buildInfo, }) async { // Configure package:test to use the Flutter engine for child processes. final String shellPath = globals.artifacts!.getArtifactPath(Artifact.flutterTester); // Compute the command-line arguments for package:test. final List testArgs = [ if (!globals.terminal.supportsColor) '--no-color', if (debuggingOptions.startPaused) '--pause-after-load', if (machine) ...['-r', 'json'] else if (reporter != null) ...['-r', reporter], if (fileReporter != null) '--file-reporter=$fileReporter', if (timeout != null) ...['--timeout', timeout], if (concurrency != null) '--concurrency=$concurrency', for (final String name in names) ...['--name', name], for (final String plainName in plainNames) ...['--plain-name', plainName], if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', if (tags != null) ...['--tags', tags], if (excludeTags != null) ...['--exclude-tags', excludeTags], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', if (shardIndex != null) '--shard-index=$shardIndex', '--chain-stack-traces', ]; if (web) { final String tempBuildDir = globals.fs.systemTempDirectory .createTempSync('flutter_test.') .absolute .uri .toFilePath(); final WebMemoryFS result = await WebTestCompiler( logger: globals.logger, fileSystem: globals.fs, platform: globals.platform, artifacts: globals.artifacts!, processManager: globals.processManager, config: globals.config, ).initialize( projectDirectory: flutterProject!.directory, testOutputDir: tempBuildDir, testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(), buildInfo: debuggingOptions.buildInfo, webRenderer: debuggingOptions.webRenderer, useWasm: debuggingOptions.webUseWasm, ); testArgs ..add('--platform=chrome') ..add('--') ..addAll(testFiles.map((Uri uri) => uri.toString())); testWrapper.registerPlatformPlugin( [Runtime.chrome], () { return FlutterWebPlatform.start( flutterProject.directory.path, updateGoldens: updateGoldens, shellPath: shellPath, flutterProject: flutterProject, pauseAfterLoad: debuggingOptions.startPaused, nullAssertions: debuggingOptions.nullAssertions, buildInfo: debuggingOptions.buildInfo, webMemoryFS: result, logger: globals.logger, fileSystem: globals.fs, buildDirectory: globals.fs.directory(tempBuildDir), artifacts: globals.artifacts, processManager: globals.processManager, chromiumLauncher: ChromiumLauncher( fileSystem: globals.fs, platform: globals.platform, processManager: globals.processManager, operatingSystemUtils: globals.os, browserFinder: findChromeExecutable, logger: globals.logger, ), testTimeRecorder: testTimeRecorder, webRenderer: debuggingOptions.webRenderer, useWasm: debuggingOptions.webUseWasm, ); }, ); 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 platform = loader.installHook( testWrapper: testWrapper, shellPath: shellPath, debuggingOptions: debuggingOptions, watcher: watcher, enableVmService: enableVmService, machine: machine, serverType: serverType, precompiledDillPath: precompiledDillPath, precompiledDillFiles: precompiledDillFiles, updateGoldens: updateGoldens, testAssetDirectory: testAssetDirectory, projectRootDirectory: globals.fs.currentDirectory.uri, flutterProject: flutterProject, icudtlPath: icudtlPath, integrationTestDevice: integrationTestDevice, integrationTestUserIdentifier: integrationTestUserIdentifier, testTimeRecorder: testTimeRecorder, nativeAssetsBuilder: nativeAssetsBuilder, buildInfo: buildInfo, ); try { globals.printTrace('running test package with arguments: $testArgs'); await testWrapper.main(testArgs); // test.main() sets dart:io's exitCode global. globals.printTrace('test package returned with exit code $exitCode'); return exitCode; } finally { await platform.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 _generateIsolateSpawningTesterPackageConfig({ required FlutterProject flutterProject, required File isolateSpawningTesterPackageConfigFile, }) async { final File projectPackageConfigFile = globals.fs.directory( flutterProject.directory.path, ).childDirectory('.dart_tool').childFile('package_config.json'); final PackageConfig projectPackageConfig = PackageConfig.parseBytes( projectPackageConfigFile.readAsBytesSync(), projectPackageConfigFile.uri, ); // The flutter_tools package_config.json is guaranteed to include // package:ffi and package:test_core. final File flutterToolsPackageConfigFile = globals.fs.directory( globals.fs.path.join( Cache.flutterRoot!, 'packages', 'flutter_tools', ), ).childDirectory('.dart_tool').childFile('package_config.json'); final PackageConfig flutterToolsPackageConfig = PackageConfig.parseBytes( flutterToolsPackageConfigFile.readAsBytesSync(), flutterToolsPackageConfigFile.uri, ); final List mergedPackages = [ ...projectPackageConfig.packages, ]; final Set projectPackageNames = Set.from( mergedPackages.map((Package p) => p.name), ); for (final Package p in flutterToolsPackageConfig.packages) { if (!projectPackageNames.contains(p.name)) { mergedPackages.add(p); } } final PackageConfig mergedPackageConfig = PackageConfig(mergedPackages); final StringBuffer buffer = StringBuffer(); PackageConfig.writeString(mergedPackageConfig, buffer); isolateSpawningTesterPackageConfigFile.writeAsStringSync(buffer.toString()); } static void _generateChildTestIsolateSpawnerSourceFile( List paths, { required List packageTestArgs, required bool autoUpdateGoldenFiles, required File childTestIsolateSpawnerSourceFile, required File childTestIsolateSpawnerDillFile, }) { final Map testConfigPaths = {}; final StringBuffer 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('.', '_') .replaceAll(':', '_') .replaceAll('/', '_') .replaceAll(r'\', '_') .replaceRange(path.length - '.dart'.length, null, ''); } final Map testImports = {}; final Set seenTestConfigPaths = {}; for (final Uri 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( globals.fs.file( globals.platform.isWindows ? sanitizedPath.replaceAll('/', r'\').replaceFirst(r'\', '') : sanitizedPath, ), globals.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 packageTestArgs = ['); for (final String arg in packageTestArgs) { buffer.writeln(" '$arg',"); } buffer.writeln('];'); buffer.writeln(); buffer.writeln('const List testPaths = ['); for (final Uri path in paths) { buffer.writeln(" '$path',"); } buffer.writeln('];'); buffer.writeln(); buffer.writeln(r''' @Native, Pointer)>(symbol: 'Spawn') external void _spawn(Pointer entrypoint, Pointer 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 channel = IsolateChannel.connectSend( IsolateNameServer.lookupPortByName(name)!, ); channel.pipe(RemoteListener.start(() => testMain)); } void testMain() { final String route = PlatformDispatcher.instance.defaultRouteName; switch (route) { '''); for (final MapEntry 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''' } } void main([dynamic sendPort]) { if (sendPort is SendPort) { final ReceivePort receivePort = ReceivePort(); receivePort.listen((dynamic msg) { switch (msg as List) { case ['spawn', final SendPort port, final String entrypoint, final String route]: spawn(port: port, entrypoint: entrypoint, route: route); case ['close']: receivePort.close(); } }); sendPort.send([receivePort.sendPort, packageTestArgs, testPaths]); } } '''); childTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString()); } static void _generateRootTestIsolateSpawnerSourceFile({ required File childTestIsolateSpawnerSourceFile, required File childTestIsolateSpawnerDillFile, required File rootTestIsolateSpawnerSourceFile, }) { final StringBuffer 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)>(symbol: 'LoadLibraryFromKernel') external Object _loadLibraryFromKernel(Pointer path); @Native, Pointer)>(symbol: 'LookupEntryPoint') external Object _lookupEntryPoint(Pointer library, Pointer name); late final List packageTestArgs; late final List testPaths; /// Runs on the main isolate. Future registerPluginAndRun() { final SpawnPlugin platform = SpawnPlugin(); registerPlatformPlugin( [Runtime.vm], () { return platform; }, ); return test.main([...packageTestArgs, '--', ...testPaths]); } late final Isolate rootTestIsolate; late final SendPort commandPort; bool readyToRun = false; final Completer readyToRunSignal = Completer(); Future spawn({ required SendPort port, String entrypoint = 'main', String route = '/', }) async { if (!readyToRun) { await readyToRunSignal.future; } commandPort.send(['spawn', port, entrypoint, route]); } void main() async { final String route = PlatformDispatcher.instance.defaultRouteName; if (route == '/') { final ReceivePort port = ReceivePort(); port.listen((dynamic message) { final [SendPort sendPort, List args, List paths] = message as List; 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('.', '_') .replaceAll(':', '_') .replaceAll('/', '_') .replaceAll(r'\', '_'); } class SpawnPlugin extends PlatformPlugin { SpawnPlugin(); final Map> _channels = >{}; Future launchIsolate(String path) async { final String name = pathToImport(path); final ReceivePort port = ReceivePort(); _channels[name] = IsolateChannel.connectReceive(port); await spawn(port: port.sendPort, route: name); } @override Future close() async { commandPort.send(['close']); } '''); buffer.write(''' @override Future load( String path, SuitePlatform platform, SuiteConfiguration suiteConfig, Object message, ) async { final String correctedPath = ${globals.platform.isWindows ? r'"/$path"' : 'path'}; await launchIsolate(correctedPath); final StreamChannel channel = _channels[pathToImport(correctedPath)]!; final RunnerSuiteController controller = deserializeSuite(correctedPath, platform, suiteConfig, const PluginEnvironment(), channel, message); return controller.suite; } } '''); rootTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString()); } static Future _compileFile({ required DebuggingOptions debuggingOptions, required File packageConfigFile, required PackageConfig packageConfig, required File sourceFile, required File outputDillFile, required TestTimeRecorder? testTimeRecorder, Uri? nativeAssetsYaml, }) async { globals.printTrace('Compiling ${sourceFile.absolute.uri}'); final Stopwatch compilerTime = Stopwatch()..start(); final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Compile); final ResidentCompiler residentCompiler = ResidentCompiler( globals.artifacts!.getArtifactPath(Artifact.flutterPatchedSdkPath), artifacts: globals.artifacts!, logger: globals.logger, processManager: globals.processManager, buildMode: debuggingOptions.buildInfo.mode, trackWidgetCreation: debuggingOptions. buildInfo.trackWidgetCreation, dartDefines: debuggingOptions.buildInfo.dartDefines, packagesPath: packageConfigFile.path, frontendServerStarterPath: debuggingOptions.buildInfo.frontendServerStarterPath, extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions, platform: globals.platform, testCompilation: true, fileSystem: globals.fs, fileSystemRoots: debuggingOptions.buildInfo.fileSystemRoots, fileSystemScheme: debuggingOptions.buildInfo.fileSystemScheme, ); await residentCompiler.recompile( sourceFile.absolute.uri, null, outputPath: outputDillFile.absolute.path, packageConfig: packageConfig, fs: globals.fs, nativeAssetsYaml: nativeAssetsYaml, ); residentCompiler.accept(); globals.printTrace('Compiling ${sourceFile.absolute.uri} took ${compilerTime.elapsedMilliseconds}ms'); testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!); } @override Future runTestsBySpawningLightweightEngines( List testFiles, { required DebuggingOptions debuggingOptions, List names = const [], List plainNames = const [], String? tags, String? excludeTags, bool machine = false, bool updateGoldens = false, required int? concurrency, String? testAssetDirectory, FlutterProject? flutterProject, String? icudtlPath, String? randomSeed, String? reporter, String? fileReporter, String? timeout, bool failFast = false, bool runSkipped = false, int? shardIndex, int? totalShards, TestTimeRecorder? testTimeRecorder, TestCompilerNativeAssetsBuilder? nativeAssetsBuilder, }) async { assert(testFiles.length > 1); final Directory buildDirectory = globals.fs.directory(globals.fs.path.join( flutterProject!.directory.path, getBuildDirectory(), )); 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, ); final PackageConfig isolateSpawningTesterPackageConfig = PackageConfig.parseBytes( isolateSpawningTesterPackageConfigFile.readAsBytesSync(), isolateSpawningTesterPackageConfigFile.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 packageTestArgs = [ if (!globals.terminal.supportsColor) '--no-color', if (machine) ...['-r', 'json'] else if (reporter != null) ...['-r', reporter], if (fileReporter != null) '--file-reporter=$fileReporter', if (timeout != null) ...['--timeout', timeout], if (concurrency != null) '--concurrency=$concurrency', for (final String name in names) ...['--name', name], for (final String plainName in plainNames) ...['--plain-name', plainName], if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed', if (tags != null) ...['--tags', tags], if (excludeTags != null) ...['--exclude-tags', excludeTags], if (failFast) '--fail-fast', if (runSkipped) '--run-skipped', if (totalShards != null) '--total-shards=$totalShards', if (shardIndex != null) '--shard-index=$shardIndex', '--chain-stack-traces', ]; _generateChildTestIsolateSpawnerSourceFile( testFiles, packageTestArgs: packageTestArgs, autoUpdateGoldenFiles: updateGoldens, childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile, childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile, ); _generateRootTestIsolateSpawnerSourceFile( childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile, childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile, rootTestIsolateSpawnerSourceFile: rootTestIsolateSpawnerSourceFile, ); final Uri? nativeAssetsYaml = await nativeAssetsBuilder?.build( debuggingOptions.buildInfo, ); await _compileFile( debuggingOptions: debuggingOptions, packageConfigFile: isolateSpawningTesterPackageConfigFile, packageConfig: isolateSpawningTesterPackageConfig, sourceFile: childTestIsolateSpawnerSourceFile, outputDillFile: childTestIsolateSpawnerDillFile, testTimeRecorder: testTimeRecorder, nativeAssetsYaml: nativeAssetsYaml, ); await _compileFile( debuggingOptions: debuggingOptions, packageConfigFile: isolateSpawningTesterPackageConfigFile, packageConfig: isolateSpawningTesterPackageConfig, sourceFile: rootTestIsolateSpawnerSourceFile, outputDillFile: rootTestIsolateSpawnerDillFile, testTimeRecorder: testTimeRecorder, ); final List command = [ globals.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', if (debuggingOptions.nullAssertions) '--dart-flags=--null_assertions', ...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 = globals.platform.environment.containsKey('FLUTTER_TEST') ? globals.platform.environment['FLUTTER_TEST']! : 'true'; final Map environment = { 'FLUTTER_TEST': flutterTest, 'FONTCONFIG_FILE': FontConfigManager().fontConfigFile.path, 'APP_NAME': flutterProject.manifest.appName, if (testAssetDirectory != null) 'UNIT_TEST_ASSETS': testAssetDirectory, }; globals.logger.printTrace('Starting flutter_tester process with command=$command, environment=$environment'); final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Run); final Process process = await globals.processManager.start(command, environment: environment); globals.logger.printTrace('Started flutter_tester process at pid ${process.pid}'); for (final Stream> stream in >>[ process.stderr, process.stdout, ]) { stream .transform(utf8.decoder) .listen(globals.stdio.stdoutWrite); } return process.exitCode.then((int exitCode) { testTimeRecorder?.stop(TestTimePhases.Run, testTimeRecorderStopwatch!); globals.logger.printTrace('flutter_tester process at pid ${process.pid} exited with code=$exitCode'); return exitCode; }); } }