| // 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, |
| useWasm: debuggingOptions.webUseWasm, |
| ); |
| 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, |
| 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 = |
| 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; |
| }); |
| } |
| } |