// 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<int> runTests(
    TestWrapper testWrapper,
    List<Uri> testFiles, {
    required DebuggingOptions debuggingOptions,
    List<String> names = const <String>[],
    List<String> plainNames = const <String>[],
    String? tags,
    String? excludeTags,
    bool enableVmService = false,
    bool ipv6 = false,
    bool machine = false,
    String? precompiledDillPath,
    Map<String, String>? 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 runSkipped = false,
    int? shardIndex,
    int? totalShards,
    Device? integrationTestDevice,
    String? integrationTestUserIdentifier,
    TestTimeRecorder? testTimeRecorder,
    TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
  });

  /// Runs tests using the experimental strategy of spawning each test in a
  /// separate lightweight Engine.
  Future<int> runTestsBySpawningLightweightEngines(
    List<Uri> testFiles, {
    required DebuggingOptions debuggingOptions,
    List<String> names = const <String>[],
    List<String> plainNames = const <String>[],
    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 runSkipped = false,
    int? shardIndex,
    int? totalShards,
    TestTimeRecorder? testTimeRecorder,
    TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
  });
}

class _FlutterTestRunnerImpl implements FlutterTestRunner {
  const _FlutterTestRunnerImpl();

  @override
  Future<int> runTests(
    TestWrapper testWrapper,
    List<Uri> testFiles, {
    required DebuggingOptions debuggingOptions,
    List<String> names = const <String>[],
    List<String> plainNames = const <String>[],
    String? tags,
    String? excludeTags,
    bool enableVmService = false,
    bool ipv6 = false,
    bool machine = false,
    String? precompiledDillPath,
    Map<String, String>? 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 runSkipped = false,
    int? shardIndex,
    int? totalShards,
    Device? integrationTestDevice,
    String? integrationTestUserIdentifier,
    TestTimeRecorder? testTimeRecorder,
    TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
  }) 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<String> testArgs = <String>[
      if (!globals.terminal.supportsColor)
        '--no-color',
      if (debuggingOptions.startPaused)
        '--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 (concurrency != null)
        '--concurrency=$concurrency',
      for (final String name in names)
        ...<String>['--name', name],
      for (final String plainName in plainNames)
        ...<String>['--plain-name', plainName],
      if (randomSeed != null)
        '--test-randomize-ordering-seed=$randomSeed',
      if (tags != null)
        ...<String>['--tags', tags],
      if (excludeTags != null)
        ...<String>['--exclude-tags', excludeTags],
      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,
      );
      testArgs
        ..add('--platform=chrome')
        ..add('--')
        ..addAll(testFiles.map((Uri uri) => uri.toString()));
      testWrapper.registerPlatformPlugin(
        <Runtime>[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,
            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,
          );
        },
      );
      await testWrapper.main(testArgs);
      return exitCode;
    }

    testArgs
      ..add('--')
      ..addAll(testFiles.map((Uri uri) => uri.toString()));

    final InternetAddressType serverType =
        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,
    );

    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<void> _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<Package> mergedPackages = <Package>[
      ...projectPackageConfig.packages,
    ];
    final Set<String> 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 PackageConfig mergedPackageConfig = PackageConfig(mergedPackages);
    final StringBuffer buffer = StringBuffer();
    PackageConfig.writeString(mergedPackageConfig, buffer);
    isolateSpawningTesterPackageConfigFile.writeAsStringSync(buffer.toString());
  }

  static void _generateChildTestIsolateSpawnerSourceFile(
    List<Uri> paths, {
    required List<String> packageTestArgs,
    required bool autoUpdateGoldenFiles,
    required File childTestIsolateSpawnerSourceFile,
    required File childTestIsolateSpawnerDillFile,
  }) {
    final Map<String, String> testConfigPaths = <String, String>{};

    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<String, String> testImports = <String, String>{};
    final Set<String> seenTestConfigPaths = <String>{};
    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<String> packageTestArgs = <String>[');
    for (final String arg in packageTestArgs) {
      buffer.writeln("  '$arg',");
    }
    buffer.writeln('];');
    buffer.writeln();

    buffer.writeln('const List<String> testPaths = <String>[');
    for (final Uri 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));
}

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'''
  }
}

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 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<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]);
}

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('.', '_')
      .replaceAll(':', '_')
      .replaceAll('/', '_')
      .replaceAll(r'\', '_');
}

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 = ${globals.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 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<int> runTestsBySpawningLightweightEngines(
    List<Uri> testFiles, {
    required DebuggingOptions debuggingOptions,
    List<String> names = const <String>[],
    List<String> plainNames = const <String>[],
    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 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<String> packageTestArgs = <String>[
      if (!globals.terminal.supportsColor)
        '--no-color',
      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 (concurrency != null)
        '--concurrency=$concurrency',
      for (final String name in names)
        ...<String>['--name', name],
      for (final String plainName in plainNames)
        ...<String>['--plain-name', plainName],
      if (randomSeed != null)
        '--test-randomize-ordering-seed=$randomSeed',
      if (tags != null)
        ...<String>['--tags', tags],
      if (excludeTags != null)
        ...<String>['--exclude-tags', excludeTags],
      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<String> command = <String>[
      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.packagesPath}',
      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<String, String> environment = <String, String>{
      '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<List<int>> stream in <Stream<List<int>>>[
      process.stderr,
      process.stdout,
    ]) {
      stream
        .transform<String>(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;
    });
  }
}
