| 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 | |
| 5 | import 'package:package_config/package_config.dart' ; |
| 6 | |
| 7 | import '../artifacts.dart'; |
| 8 | import '../base/common.dart'; |
| 9 | import '../base/file_system.dart'; |
| 10 | import '../base/io.dart'; |
| 11 | import '../build_info.dart'; |
| 12 | import '../cache.dart'; |
| 13 | import '../compile.dart'; |
| 14 | import '../convert.dart'; |
| 15 | import '../device.dart'; |
| 16 | import '../globals.dart' as globals; |
| 17 | import '../native_assets.dart'; |
| 18 | import '../project.dart'; |
| 19 | import '../web/chrome.dart'; |
| 20 | import '../web/memory_fs.dart'; |
| 21 | import 'flutter_platform.dart' as loader; |
| 22 | import 'flutter_web_platform.dart'; |
| 23 | import 'font_config_manager.dart'; |
| 24 | import 'test_config.dart'; |
| 25 | import 'test_time_recorder.dart'; |
| 26 | import 'test_wrapper.dart'; |
| 27 | import 'watcher.dart'; |
| 28 | import 'web_test_compiler.dart'; |
| 29 | |
| 30 | /// Launching the `flutter_tester` process from the test runner. |
| 31 | interface 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(''' |
| 265 | import 'dart:ffi'; |
| 266 | import 'dart:isolate'; |
| 267 | import 'dart:ui'; |
| 268 | |
| 269 | import 'package:ffi/ffi.dart'; |
| 270 | import 'package:flutter_test/flutter_test.dart'; |
| 271 | import 'package:stream_channel/isolate_channel.dart'; |
| 272 | import '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') |
| 330 | external void _spawn(Pointer<Utf8> entrypoint, Pointer<Utf8> route); |
| 331 | |
| 332 | void 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. |
| 344 | void 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') |
| 354 | void 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') |
| 379 | void 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(''' |
| 406 | import 'dart:async'; |
| 407 | import 'dart:ffi'; |
| 408 | import 'dart:io' show exit, exitCode; // flutter_ignore: dart_io_import |
| 409 | import 'dart:isolate'; |
| 410 | import 'dart:ui'; |
| 411 | |
| 412 | import 'package:ffi/ffi.dart'; |
| 413 | import 'package:stream_channel/isolate_channel.dart'; |
| 414 | import 'package:stream_channel/stream_channel.dart'; |
| 415 | import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports |
| 416 | import 'package:test_core/src/platform.dart'; // ignore: implementation_imports |
| 417 | |
| 418 | @Native<Handle Function(Pointer<Utf8>)>(symbol: 'LoadLibraryFromKernel') |
| 419 | external Object _loadLibraryFromKernel(Pointer<Utf8> path); |
| 420 | |
| 421 | @Native<Handle Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'LookupEntryPoint') |
| 422 | external Object _lookupEntryPoint(Pointer<Utf8> library, Pointer<Utf8> name); |
| 423 | |
| 424 | late final List<String> packageTestArgs; |
| 425 | late final List<String> testPaths; |
| 426 | |
| 427 | /// Runs on the main isolate. |
| 428 | Future<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 | |
| 439 | late final Isolate rootTestIsolate; |
| 440 | late final SendPort commandPort; |
| 441 | bool readyToRun = false; |
| 442 | final Completer<void> readyToRunSignal = Completer<void>(); |
| 443 | |
| 444 | Future<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') |
| 457 | void 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''' |
| 496 | String 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 | |
| 506 | class 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 | |