1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:package_config/package_config.dart';
6
7import '../artifacts.dart';
8import '../base/common.dart';
9import '../base/file_system.dart';
10import '../base/io.dart';
11import '../build_info.dart';
12import '../cache.dart';
13import '../compile.dart';
14import '../convert.dart';
15import '../device.dart';
16import '../globals.dart' as globals;
17import '../native_assets.dart';
18import '../project.dart';
19import '../web/chrome.dart';
20import '../web/memory_fs.dart';
21import 'flutter_platform.dart' as loader;
22import 'flutter_web_platform.dart';
23import 'font_config_manager.dart';
24import 'test_config.dart';
25import 'test_time_recorder.dart';
26import 'test_wrapper.dart';
27import 'watcher.dart';
28import 'web_test_compiler.dart';
29
30/// Launching the `flutter_tester` process from the test runner.
31interface class FlutterTestRunner {
32 const FlutterTestRunner();
33
34 /// Runs tests using package:test and the Flutter engine.
35 Future<int> runTests(
36 TestWrapper testWrapper,
37 List<Uri> testFiles, {
38 required DebuggingOptions debuggingOptions,
39 List<String> names = const <String>[],
40 List<String> plainNames = const <String>[],
41 String? tags,
42 String? excludeTags,
43 bool enableVmService = false,
44 bool machine = false,
45 String? precompiledDillPath,
46 Map<String, String>? precompiledDillFiles,
47 bool updateGoldens = false,
48 TestWatcher? watcher,
49 required int? concurrency,
50 String? testAssetDirectory,
51 FlutterProject? flutterProject,
52 String? icudtlPath,
53 Directory? coverageDirectory,
54 bool web = false,
55 String? randomSeed,
56 String? reporter,
57 String? fileReporter,
58 String? timeout,
59 bool ignoreTimeouts = false,
60 bool failFast = false,
61 bool runSkipped = false,
62 int? shardIndex,
63 int? totalShards,
64 Device? integrationTestDevice,
65 String? integrationTestUserIdentifier,
66 TestTimeRecorder? testTimeRecorder,
67 TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
68 required BuildInfo buildInfo,
69 }) async {
70 // Configure package:test to use the Flutter engine for child processes.
71 final String flutterTesterBinPath = globals.artifacts!.getArtifactPath(Artifact.flutterTester);
72
73 // Compute the command-line arguments for package:test.
74 final testArgs = <String>[
75 if (!globals.terminal.supportsColor) '--no-color',
76 if (debuggingOptions.startPaused) '--pause-after-load',
77 if (machine) ...<String>['-r', 'json'] else if (reporter != null) ...<String>['-r', reporter],
78 if (fileReporter != null) '--file-reporter=$fileReporter',
79 if (timeout != null) ...<String>['--timeout', timeout],
80 if (ignoreTimeouts) '--ignore-timeouts',
81 if (concurrency != null) '--concurrency=$concurrency',
82 for (final String name in names) ...<String>['--name', name],
83 for (final String plainName in plainNames) ...<String>['--plain-name', plainName],
84 if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed',
85 if (tags != null) ...<String>['--tags', tags],
86 if (excludeTags != null) ...<String>['--exclude-tags', excludeTags],
87 if (failFast) '--fail-fast',
88 if (runSkipped) '--run-skipped',
89 if (totalShards != null) '--total-shards=$totalShards',
90 if (shardIndex != null) '--shard-index=$shardIndex',
91 '--chain-stack-traces',
92 ];
93
94 if (web) {
95 final String tempBuildDir = globals.fs.systemTempDirectory
96 .createTempSync('flutter_test.')
97 .absolute
98 .uri
99 .toFilePath();
100 final WebMemoryFS result =
101 await WebTestCompiler(
102 logger: globals.logger,
103 fileSystem: globals.fs,
104 platform: globals.platform,
105 artifacts: globals.artifacts!,
106 processManager: globals.processManager,
107 config: globals.config,
108 ).initialize(
109 projectDirectory: flutterProject!.directory,
110 testOutputDir: tempBuildDir,
111 testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(),
112 buildInfo: debuggingOptions.buildInfo,
113 webRenderer: debuggingOptions.webRenderer,
114 useWasm: debuggingOptions.webUseWasm,
115 );
116 testArgs
117 ..add('--platform=chrome')
118 ..add('--')
119 ..addAll(testFiles.map((Uri uri) => uri.toString()));
120 testWrapper.registerPlatformPlugin(<Runtime>[Runtime.chrome], () {
121 return FlutterWebPlatform.start(
122 flutterProject.directory.path,
123 updateGoldens: updateGoldens,
124 flutterTesterBinPath: flutterTesterBinPath,
125 flutterProject: flutterProject,
126 pauseAfterLoad: debuggingOptions.startPaused,
127 buildInfo: debuggingOptions.buildInfo,
128 webMemoryFS: result,
129 logger: globals.logger,
130 fileSystem: globals.fs,
131 buildDirectory: globals.fs.directory(tempBuildDir),
132 artifacts: globals.artifacts,
133 processManager: globals.processManager,
134 chromiumLauncher: ChromiumLauncher(
135 fileSystem: globals.fs,
136 platform: globals.platform,
137 processManager: globals.processManager,
138 operatingSystemUtils: globals.os,
139 browserFinder: findChromeExecutable,
140 logger: globals.logger,
141 ),
142 testTimeRecorder: testTimeRecorder,
143 webRenderer: debuggingOptions.webRenderer,
144 useWasm: debuggingOptions.webUseWasm,
145 );
146 });
147 await testWrapper.main(testArgs);
148 return exitCode;
149 }
150
151 testArgs
152 ..add('--')
153 ..addAll(testFiles.map((Uri uri) => uri.toString()));
154
155 final InternetAddressType serverType = debuggingOptions.ipv6
156 ? InternetAddressType.IPv6
157 : InternetAddressType.IPv4;
158
159 final loader.FlutterPlatform platform = loader.installHook(
160 testWrapper: testWrapper,
161 flutterTesterBinPath: flutterTesterBinPath,
162 debuggingOptions: debuggingOptions,
163 watcher: watcher,
164 enableVmService: enableVmService,
165 machine: machine,
166 serverType: serverType,
167 precompiledDillPath: precompiledDillPath,
168 precompiledDillFiles: precompiledDillFiles,
169 updateGoldens: updateGoldens,
170 testAssetDirectory: testAssetDirectory,
171 projectRootDirectory: globals.fs.currentDirectory.uri,
172 flutterProject: flutterProject,
173 icudtlPath: icudtlPath,
174 integrationTestDevice: integrationTestDevice,
175 integrationTestUserIdentifier: integrationTestUserIdentifier,
176 testTimeRecorder: testTimeRecorder,
177 nativeAssetsBuilder: nativeAssetsBuilder,
178 buildInfo: buildInfo,
179 fileSystem: globals.fs,
180 logger: globals.logger,
181 processManager: globals.processManager,
182 );
183
184 try {
185 globals.printTrace('running test package with arguments: $testArgs');
186 await testWrapper.main(testArgs);
187
188 // test.main() sets dart:io's exitCode global.
189 globals.printTrace('test package returned with exit code $exitCode');
190
191 return exitCode;
192 } finally {
193 await platform.close();
194 }
195 }
196
197 // To compile root_test_isolate_spawner.dart and
198 // child_test_isolate_spawner.dart successfully, we will need to pass a
199 // package_config.json to the frontend server that contains the
200 // union of package:test_core, package:ffi, and all the dependencies of the
201 // project under test. This function generates such a package_config.json.
202 static Future<void> _generateIsolateSpawningTesterPackageConfig({
203 required FlutterProject flutterProject,
204 required File isolateSpawningTesterPackageConfigFile,
205 }) async {
206 final File packageConfigFile = globals.fs
207 .directory(flutterProject.directory.path)
208 .childDirectory('.dart_tool')
209 .childFile('package_config.json');
210 PackageConfig? projectPackageConfig;
211 if (await packageConfigFile.exists()) {
212 projectPackageConfig = PackageConfig.parseBytes(
213 packageConfigFile.readAsBytesSync(),
214 Uri.file(flutterProject.directory.path),
215 );
216 } else {
217 // We can't use this directly, but need to manually check
218 // `flutterProject.directory.path` first, as `findPackageConfig` is from a
219 // different package which does not use package:file. This inhibits
220 // mocking the file system.
221 projectPackageConfig = await findPackageConfig(
222 globals.fs.directory(flutterProject.directory.path),
223 );
224 }
225
226 if (projectPackageConfig == null) {
227 throwToolExit('Could not find package config for ${flutterProject.directory.path}.');
228 }
229
230 // The flutter_tools package_config.json is guaranteed to include
231 // package:ffi and package:test_core.
232 final File flutterToolsPackageConfigFile = globals.fs
233 .directory(globals.fs.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools'))
234 .childDirectory('.dart_tool')
235 .childFile('package_config.json');
236 final PackageConfig flutterToolsPackageConfig = PackageConfig.parseBytes(
237 flutterToolsPackageConfigFile.readAsBytesSync(),
238 flutterToolsPackageConfigFile.uri,
239 );
240
241 final mergedPackages = <Package>[...projectPackageConfig.packages];
242 final projectPackageNames = Set<String>.from(mergedPackages.map((Package p) => p.name));
243 for (final Package p in flutterToolsPackageConfig.packages) {
244 if (!projectPackageNames.contains(p.name)) {
245 mergedPackages.add(p);
246 }
247 }
248
249 final mergedPackageConfig = PackageConfig(mergedPackages);
250 final buffer = StringBuffer();
251 PackageConfig.writeString(mergedPackageConfig, buffer);
252 isolateSpawningTesterPackageConfigFile.writeAsStringSync(buffer.toString());
253 }
254
255 static void _generateChildTestIsolateSpawnerSourceFile(
256 List<Uri> paths, {
257 required List<String> packageTestArgs,
258 required bool autoUpdateGoldenFiles,
259 required File childTestIsolateSpawnerSourceFile,
260 }) {
261 final testConfigPaths = <String, String>{};
262
263 final buffer = StringBuffer();
264 buffer.writeln('''
265import 'dart:ffi';
266import 'dart:isolate';
267import 'dart:ui';
268
269import 'package:ffi/ffi.dart';
270import 'package:flutter_test/flutter_test.dart';
271import 'package:stream_channel/isolate_channel.dart';
272import 'package:test_api/backend.dart'; // flutter_ignore: test_api_import
273''');
274
275 String pathToImport(String path) {
276 assert(path.endsWith('.dart'));
277 return path
278 .replaceAll('.', '_')
279 .replaceAll(':', '_')
280 .replaceAll('/', '_')
281 .replaceAll(r'\', '_')
282 .replaceRange(path.length - '.dart'.length, null, '');
283 }
284
285 final testImports = <String, String>{};
286 final seenTestConfigPaths = <String>{};
287 for (final path in paths) {
288 final String sanitizedPath = !path.path.endsWith('?')
289 ? path.path
290 : path.path.substring(0, path.path.length - 1);
291 final String sanitizedImport = pathToImport(sanitizedPath);
292 buffer.writeln("import '$sanitizedPath' as $sanitizedImport;");
293 testImports[sanitizedPath] = sanitizedImport;
294 final File? testConfigFile = findTestConfigFile(
295 globals.fs.file(
296 globals.platform.isWindows
297 ? sanitizedPath.replaceAll('/', r'\').replaceFirst(r'\', '')
298 : sanitizedPath,
299 ),
300 globals.logger,
301 );
302 if (testConfigFile != null) {
303 final String sanitizedTestConfigImport = pathToImport(testConfigFile.path);
304 testConfigPaths[sanitizedImport] = sanitizedTestConfigImport;
305 if (seenTestConfigPaths.add(testConfigFile.path)) {
306 buffer.writeln(
307 "import '${Uri.file(testConfigFile.path, windows: true)}' as $sanitizedTestConfigImport;",
308 );
309 }
310 }
311 }
312 buffer.writeln();
313
314 buffer.writeln('const List<String> packageTestArgs = <String>[');
315 for (final arg in packageTestArgs) {
316 buffer.writeln(" '$arg',");
317 }
318 buffer.writeln('];');
319 buffer.writeln();
320
321 buffer.writeln('const List<String> testPaths = <String>[');
322 for (final path in paths) {
323 buffer.writeln(" '$path',");
324 }
325 buffer.writeln('];');
326 buffer.writeln();
327
328 buffer.writeln(r'''
329@Native<Void Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'Spawn')
330external void _spawn(Pointer<Utf8> entrypoint, Pointer<Utf8> route);
331
332void spawn({required SendPort port, String entrypoint = 'main', String route = '/'}) {
333 assert(
334 entrypoint != 'main' || route != '/',
335 'Spawn should not be used to spawn main with the default route name',
336 );
337 IsolateNameServer.registerPortWithName(port, route);
338 _spawn(entrypoint.toNativeUtf8(), route.toNativeUtf8());
339}
340''');
341
342 buffer.write('''
343/// Runs on a spawned isolate.
344void createChannelAndConnect(String path, String name, Function testMain) {
345 goldenFileComparator = LocalFileComparator(Uri.parse(path));
346 autoUpdateGoldenFiles = $autoUpdateGoldenFiles;
347 final IsolateChannel<dynamic> channel = IsolateChannel<dynamic>.connectSend(
348 IsolateNameServer.lookupPortByName(name)!,
349 );
350 channel.pipe(RemoteListener.start(() => testMain));
351}
352
353@pragma('vm:entry-point')
354void testMain() {
355 final String route = PlatformDispatcher.instance.defaultRouteName;
356 switch (route) {
357''');
358
359 for (final MapEntry<String, String> kvp in testImports.entries) {
360 final String importName = kvp.value;
361 final String path = kvp.key;
362 final String? testConfigImport = testConfigPaths[importName];
363 if (testConfigImport != null) {
364 buffer.writeln(" case '$importName':");
365 buffer.writeln(
366 " createChannelAndConnect('$path', route, () => $testConfigImport.testExecutable($importName.main));",
367 );
368 } else {
369 buffer.writeln(" case '$importName':");
370 buffer.writeln(" createChannelAndConnect('$path', route, $importName.main);");
371 }
372 }
373
374 buffer.write(r'''
375 }
376}
377
378@pragma('vm:entry-point')
379void main([dynamic sendPort]) {
380 if (sendPort is SendPort) {
381 final ReceivePort receivePort = ReceivePort();
382 receivePort.listen((dynamic msg) {
383 switch (msg as List<dynamic>) {
384 case ['spawn', final SendPort port, final String entrypoint, final String route]:
385 spawn(port: port, entrypoint: entrypoint, route: route);
386 case ['close']:
387 receivePort.close();
388 }
389 });
390
391 sendPort.send(<Object>[receivePort.sendPort, packageTestArgs, testPaths]);
392 }
393}
394''');
395
396 childTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString());
397 }
398
399 static void _generateRootTestIsolateSpawnerSourceFile({
400 required File childTestIsolateSpawnerSourceFile,
401 required File childTestIsolateSpawnerDillFile,
402 required File rootTestIsolateSpawnerSourceFile,
403 }) {
404 final buffer = StringBuffer();
405 buffer.writeln('''
406import 'dart:async';
407import 'dart:ffi';
408import 'dart:io' show exit, exitCode; // flutter_ignore: dart_io_import
409import 'dart:isolate';
410import 'dart:ui';
411
412import 'package:ffi/ffi.dart';
413import 'package:stream_channel/isolate_channel.dart';
414import 'package:stream_channel/stream_channel.dart';
415import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports
416import 'package:test_core/src/platform.dart'; // ignore: implementation_imports
417
418@Native<Handle Function(Pointer<Utf8>)>(symbol: 'LoadLibraryFromKernel')
419external Object _loadLibraryFromKernel(Pointer<Utf8> path);
420
421@Native<Handle Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'LookupEntryPoint')
422external Object _lookupEntryPoint(Pointer<Utf8> library, Pointer<Utf8> name);
423
424late final List<String> packageTestArgs;
425late final List<String> testPaths;
426
427/// Runs on the main isolate.
428Future<void> registerPluginAndRun() {
429 final SpawnPlugin platform = SpawnPlugin();
430 registerPlatformPlugin(
431 <Runtime>[Runtime.vm],
432 () {
433 return platform;
434 },
435 );
436 return test.main(<String>[...packageTestArgs, '--', ...testPaths]);
437}
438
439late final Isolate rootTestIsolate;
440late final SendPort commandPort;
441bool readyToRun = false;
442final Completer<void> readyToRunSignal = Completer<void>();
443
444Future<void> spawn({
445 required SendPort port,
446 String entrypoint = 'main',
447 String route = '/',
448}) async {
449 if (!readyToRun) {
450 await readyToRunSignal.future;
451 }
452
453 commandPort.send(<Object>['spawn', port, entrypoint, route]);
454}
455
456@pragma('vm:entry-point')
457void main() async {
458 final String route = PlatformDispatcher.instance.defaultRouteName;
459
460 if (route == '/') {
461 final ReceivePort port = ReceivePort();
462
463 port.listen((dynamic message) {
464 final [SendPort sendPort, List<String> args, List<String> paths] = message as List<dynamic>;
465
466 commandPort = sendPort;
467 packageTestArgs = args;
468 testPaths = paths;
469 readyToRun = true;
470 readyToRunSignal.complete();
471 });
472
473 rootTestIsolate = await Isolate.spawn(
474 _loadLibraryFromKernel(
475 r'${childTestIsolateSpawnerDillFile.absolute.path}'
476 .toNativeUtf8()) as void Function(SendPort),
477 port.sendPort,
478 );
479
480 await readyToRunSignal.future;
481 port.close(); // Not expecting anything else.
482 await registerPluginAndRun();
483 // The [test.main] call in [registerPluginAndRun] sets dart:io's [exitCode]
484 // global.
485 exit(exitCode);
486 } else {
487 (_lookupEntryPoint(
488 r'file://${childTestIsolateSpawnerSourceFile.absolute.uri.toFilePath(windows: false)}'
489 .toNativeUtf8(),
490 'testMain'.toNativeUtf8()) as void Function())();
491 }
492}
493''');
494
495 buffer.write(r'''
496String pathToImport(String path) {
497 assert(path.endsWith('.dart'));
498 return path
499 .replaceRange(path.length - '.dart'.length, null, '')
500 .replaceAll('.', '_')
501 .replaceAll(':', '_')
502 .replaceAll('/', '_')
503 .replaceAll(r'\', '_');
504}
505
506class SpawnPlugin extends PlatformPlugin {
507 SpawnPlugin();
508
509 final Map<String, IsolateChannel<dynamic>> _channels = <String, IsolateChannel<dynamic>>{};
510
511 Future<void> launchIsolate(String path) async {
512 final String name = pathToImport(path);
513 final ReceivePort port = ReceivePort();
514 _channels[name] = IsolateChannel<dynamic>.connectReceive(port);
515 await spawn(port: port.sendPort, route: name);
516 }
517
518 @override
519 Future<void> close() async {
520 commandPort.send(<String>['close']);
521 }
522''');
523
524 buffer.write('''
525 @override
526 Future<RunnerSuite> load(
527 String path,
528 SuitePlatform platform,
529 SuiteConfiguration suiteConfig,
530 Object message,
531 ) async {
532 final String correctedPath = ${globals.platform.isWindows ? r'"/$path"' : 'path'};
533 await launchIsolate(correctedPath);
534
535 final StreamChannel<dynamic> channel = _channels[pathToImport(correctedPath)]!;
536 final RunnerSuiteController controller = deserializeSuite(correctedPath, platform,
537 suiteConfig, const PluginEnvironment(), channel, message);
538 return controller.suite;
539 }
540}
541''');
542
543 rootTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString());
544 }
545
546 static Future<void> _compileFile({
547 required DebuggingOptions debuggingOptions,
548 required File packageConfigFile,
549 required PackageConfig packageConfig,
550 required File sourceFile,
551 required File outputDillFile,
552 required TestTimeRecorder? testTimeRecorder,
553 Uri? nativeAssetsYaml,
554 }) async {
555 globals.printTrace('Compiling ${sourceFile.absolute.uri}');
556 final compilerTime = Stopwatch()..start();
557 final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Compile);
558
559 final residentCompiler = ResidentCompiler(
560 globals.artifacts!.getArtifactPath(Artifact.flutterPatchedSdkPath),
561 artifacts: globals.artifacts!,
562 logger: globals.logger,
563 processManager: globals.processManager,
564 buildMode: debuggingOptions.buildInfo.mode,
565 trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
566 dartDefines: debuggingOptions.buildInfo.dartDefines,
567 packagesPath: packageConfigFile.path,
568 frontendServerStarterPath: debuggingOptions.buildInfo.frontendServerStarterPath,
569 extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
570 platform: globals.platform,
571 testCompilation: true,
572 fileSystem: globals.fs,
573 fileSystemRoots: debuggingOptions.buildInfo.fileSystemRoots,
574 fileSystemScheme: debuggingOptions.buildInfo.fileSystemScheme,
575 );
576
577 await residentCompiler.recompile(
578 sourceFile.absolute.uri,
579 null,
580 outputPath: outputDillFile.absolute.path,
581 packageConfig: packageConfig,
582 fs: globals.fs,
583 nativeAssetsYaml: nativeAssetsYaml,
584 );
585 residentCompiler.accept();
586
587 globals.printTrace(
588 'Compiling ${sourceFile.absolute.uri} took ${compilerTime.elapsedMilliseconds}ms',
589 );
590 testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!);
591 }
592
593 /// Runs tests using the experimental strategy of spawning each test in a
594 /// separate lightweight Engine.
595 Future<int> runTestsBySpawningLightweightEngines(
596 List<Uri> testFiles, {
597 required DebuggingOptions debuggingOptions,
598 List<String> names = const <String>[],
599 List<String> plainNames = const <String>[],
600 String? tags,
601 String? excludeTags,
602 bool machine = false,
603 bool updateGoldens = false,
604 required int? concurrency,
605 String? testAssetDirectory,
606 FlutterProject? flutterProject,
607 String? icudtlPath,
608 String? randomSeed,
609 String? reporter,
610 String? fileReporter,
611 String? timeout,
612 bool ignoreTimeouts = false,
613 bool failFast = false,
614 bool runSkipped = false,
615 int? shardIndex,
616 int? totalShards,
617 TestTimeRecorder? testTimeRecorder,
618 TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
619 }) async {
620 assert(testFiles.length > 1);
621
622 final Directory buildDirectory = globals.fs.directory(
623 globals.fs.path.join(flutterProject!.directory.path, getBuildDirectory()),
624 );
625 final Directory isolateSpawningTesterDirectory = buildDirectory.childDirectory(
626 'isolate_spawning_tester',
627 );
628 isolateSpawningTesterDirectory.createSync();
629
630 final File isolateSpawningTesterPackageConfigFile = isolateSpawningTesterDirectory
631 .childDirectory('.dart_tool')
632 .childFile('package_config.json');
633 isolateSpawningTesterPackageConfigFile.createSync(recursive: true);
634 await _generateIsolateSpawningTesterPackageConfig(
635 flutterProject: flutterProject,
636 isolateSpawningTesterPackageConfigFile: isolateSpawningTesterPackageConfigFile,
637 );
638 final PackageConfig isolateSpawningTesterPackageConfig = PackageConfig.parseBytes(
639 isolateSpawningTesterPackageConfigFile.readAsBytesSync(),
640 isolateSpawningTesterPackageConfigFile.uri,
641 );
642
643 final File childTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile(
644 'child_test_isolate_spawner.dart',
645 );
646 final File rootTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile(
647 'root_test_isolate_spawner.dart',
648 );
649 final File childTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile(
650 'child_test_isolate_spawner.dill',
651 );
652 final File rootTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile(
653 'root_test_isolate_spawner.dill',
654 );
655
656 // Compute the command-line arguments for package:test.
657 final packageTestArgs = <String>[
658 if (!globals.terminal.supportsColor) '--no-color',
659 if (machine) ...<String>['-r', 'json'] else if (reporter != null) ...<String>['-r', reporter],
660 if (fileReporter != null) '--file-reporter=$fileReporter',
661 if (timeout != null) ...<String>['--timeout', timeout],
662 if (ignoreTimeouts) '--ignore-timeouts',
663 if (concurrency != null) '--concurrency=$concurrency',
664 for (final String name in names) ...<String>['--name', name],
665 for (final String plainName in plainNames) ...<String>['--plain-name', plainName],
666 if (randomSeed != null) '--test-randomize-ordering-seed=$randomSeed',
667 if (tags != null) ...<String>['--tags', tags],
668 if (excludeTags != null) ...<String>['--exclude-tags', excludeTags],
669 if (failFast) '--fail-fast',
670 if (runSkipped) '--run-skipped',
671 if (totalShards != null) '--total-shards=$totalShards',
672 if (shardIndex != null) '--shard-index=$shardIndex',
673 '--chain-stack-traces',
674 ];
675
676 _generateChildTestIsolateSpawnerSourceFile(
677 testFiles,
678 packageTestArgs: packageTestArgs,
679 autoUpdateGoldenFiles: updateGoldens,
680 childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile,
681 );
682
683 _generateRootTestIsolateSpawnerSourceFile(
684 childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile,
685 childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile,
686 rootTestIsolateSpawnerSourceFile: rootTestIsolateSpawnerSourceFile,
687 );
688
689 await _compileFile(
690 debuggingOptions: debuggingOptions,
691 packageConfigFile: isolateSpawningTesterPackageConfigFile,
692 packageConfig: isolateSpawningTesterPackageConfig,
693 sourceFile: childTestIsolateSpawnerSourceFile,
694 outputDillFile: childTestIsolateSpawnerDillFile,
695 testTimeRecorder: testTimeRecorder,
696 );
697
698 await _compileFile(
699 debuggingOptions: debuggingOptions,
700 packageConfigFile: isolateSpawningTesterPackageConfigFile,
701 packageConfig: isolateSpawningTesterPackageConfig,
702 sourceFile: rootTestIsolateSpawnerSourceFile,
703 outputDillFile: rootTestIsolateSpawnerDillFile,
704 testTimeRecorder: testTimeRecorder,
705 );
706
707 final command = <String>[
708 globals.artifacts!.getArtifactPath(Artifact.flutterTester),
709 '--disable-vm-service',
710 if (icudtlPath != null) '--icu-data-file-path=$icudtlPath',
711 '--enable-checked-mode',
712 '--verify-entry-points',
713 '--enable-software-rendering',
714 '--skia-deterministic-rendering',
715 if (debuggingOptions.enableDartProfiling) '--enable-dart-profiling',
716 '--non-interactive',
717 '--use-test-fonts',
718 '--disable-asset-fonts',
719 '--packages=${debuggingOptions.buildInfo.packageConfigPath}',
720 if (testAssetDirectory != null) '--flutter-assets-dir=$testAssetDirectory',
721 ...debuggingOptions.dartEntrypointArgs,
722 rootTestIsolateSpawnerDillFile.absolute.path,
723 ];
724
725 // If the FLUTTER_TEST environment variable has been set, then pass it on
726 // for package:flutter_test to handle the value.
727 //
728 // If FLUTTER_TEST has not been set, assume from this context that this
729 // call was invoked by the command 'flutter test'.
730 final String flutterTest = globals.platform.environment.containsKey('FLUTTER_TEST')
731 ? globals.platform.environment['FLUTTER_TEST']!
732 : 'true';
733 final environment = <String, String>{
734 'FLUTTER_TEST': flutterTest,
735 'FONTCONFIG_FILE': FontConfigManager().fontConfigFile.path,
736 'APP_NAME': flutterProject.manifest.appName,
737 if (testAssetDirectory != null) 'UNIT_TEST_ASSETS': testAssetDirectory,
738 if (nativeAssetsBuilder != null && globals.platform.isWindows)
739 'PATH':
740 '${nativeAssetsBuilder.windowsBuildDirectory(flutterProject)};${globals.platform.environment['PATH']}',
741 };
742
743 globals.logger.printTrace(
744 'Starting flutter_tester process with command=$command, environment=$environment',
745 );
746 final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Run);
747 final Process process = await globals.processManager.start(command, environment: environment);
748 globals.logger.printTrace('Started flutter_tester process at pid ${process.pid}');
749
750 for (final stream in <Stream<List<int>>>[process.stderr, process.stdout]) {
751 stream.transform<String>(utf8.decoder).listen(globals.stdio.stdoutWrite);
752 }
753
754 return process.exitCode.then((int exitCode) {
755 testTimeRecorder?.stop(TestTimePhases.Run, testTimeRecorderStopwatch!);
756 globals.logger.printTrace(
757 'flutter_tester process at pid ${process.pid} exited with code=$exitCode',
758 );
759 return exitCode;
760 });
761 }
762}
763