| // 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 'dart:convert'; | |
| import 'dart:io' | |
| show | |
| Directory, | |
| File, | |
| FileSystemEntity, | |
| HttpClient, | |
| HttpClientRequest, | |
| HttpClientResponse, | |
| Platform, | |
| Process, | |
| RawSocket, | |
| SocketDirection, | |
| SocketException; | |
| import 'dart:math' as math; | |
| import 'package:file/local.dart'; | |
| import 'package:path/path.dart' as path; | |
| import '../browser.dart'; | |
| import '../run_command.dart'; | |
| import '../service_worker_test.dart'; | |
| import '../utils.dart'; | |
| typedef ShardRunner = Future<void> Function(); | |
| class WebTestsSuite { | |
| WebTestsSuite(this.flutterTestArgs); | |
| /// Tests that we don't run on Web. | |
| /// | |
| /// In general avoid adding new tests here. If a test cannot run on the web | |
| /// because it fails at runtime, such as when a piece of functionality is not | |
| /// implemented or not implementable on the web, prefer using `skip` in the | |
| /// test code. Only add tests here that cannot be skipped using `skip`. For | |
| /// example: | |
| /// | |
| /// * Test code cannot be compiled because it uses Dart VM-specific | |
| /// functionality. In this case `skip` doesn't help because the code cannot | |
| /// reach the point where it can even run the skipping logic. | |
| /// * Migrations. It is OK to put tests here that need to be temporarily | |
| /// disabled in certain modes because of some migration or initial bringup. | |
| /// | |
| /// The key in the map is whether it's for `wasm` mode or not. The value | |
| /// is the list of tests known to fail for that mode. | |
| // | |
| // TODO(yjbanov): we're getting rid of this as part of https://github.com/flutter/flutter/projects/60 | |
| static const Map<bool, List<String>> kWebTestFileKnownFailures = <bool, List<String>>{ | |
| // useWasm: false | |
| false: <String>[ | |
| // These tests are not compilable on the web due to dependencies on | |
| // VM-specific functionality. | |
| 'test/services/message_codecs_vm_test.dart', | |
| 'test/examples/sector_layout_test.dart', | |
| // These tests are broken and need to be fixed. | |
| // TODO(yjbanov): https://github.com/flutter/flutter/issues/71604 | |
| 'test/material/text_field_test.dart', | |
| 'test/widgets/performance_overlay_test.dart', | |
| 'test/widgets/html_element_view_test.dart', | |
| 'test/cupertino/scaffold_test.dart', | |
| 'test/rendering/platform_view_test.dart', | |
| ], | |
| // useWasm: true | |
| true: <String>[ | |
| // These tests are not compilable on the web due to dependencies on | |
| // VM-specific functionality. | |
| 'test/services/message_codecs_vm_test.dart', | |
| 'test/examples/sector_layout_test.dart', | |
| // These tests are broken and need to be fixed. | |
| // TODO(jacksongardner): https://github.com/flutter/flutter/issues/71604 | |
| 'test/material/text_field_test.dart', | |
| 'test/widgets/performance_overlay_test.dart', | |
| ], | |
| }; | |
| static const List<String> _kAllBuildModes = <String>['debug', 'profile', 'release']; | |
| final List<String> flutterTestArgs; | |
| /// Coarse-grained integration tests running on the Web. | |
| Future<void> webLongRunningTestsRunner() async { | |
| final String engineRealmFile = path.join(flutterRoot, 'bin', 'cache', 'engine.realm'); | |
| // NOTE(codefu): this reading of engine.stamp is fine because it's signalling to | |
| // the Web framework which content-hash to download. | |
| final String engineVersion = File(engineVersionFile).readAsStringSync().trim(); | |
| final String engineRealm = File(engineRealmFile).readAsStringSync().trim(); | |
| final tests = <ShardRunner>[ | |
| for (final String buildMode in _kAllBuildModes) ...<ShardRunner>[ | |
| () => _runFlutterDriverWebTest( | |
| testAppDirectory: path.join('packages', 'integration_test', 'example'), | |
| target: path.join('test_driver', 'failure.dart'), | |
| buildMode: buildMode, | |
| useWasm: false, | |
| // This test intentionally fails and prints stack traces in the browser | |
| // logs. To avoid confusion, silence browser output. | |
| silenceBrowserOutput: true, | |
| ), | |
| () => _runFlutterDriverWebTest( | |
| testAppDirectory: path.join('packages', 'integration_test', 'example'), | |
| target: path.join('integration_test', 'example_test.dart'), | |
| driver: path.join('test_driver', 'integration_test.dart'), | |
| buildMode: buildMode, | |
| useWasm: false, | |
| expectWriteResponseFile: true, | |
| expectResponseFileContent: 'null', | |
| ), | |
| () => _runFlutterDriverWebTest( | |
| testAppDirectory: path.join('packages', 'integration_test', 'example'), | |
| target: path.join('integration_test', 'extended_test.dart'), | |
| driver: path.join('test_driver', 'extended_integration_test.dart'), | |
| buildMode: buildMode, | |
| useWasm: false, | |
| expectWriteResponseFile: true, | |
| expectResponseFileContent: ''' | |
| { | |
| "screenshots": [ | |
| { | |
| "screenshotName": "platform_name", | |
| "bytes": [] | |
| }, | |
| { | |
| "screenshotName": "platform_name_2", | |
| "bytes": [] | |
| } | |
| ] | |
| }''', | |
| ), | |
| ], | |
| // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x wasm mode matrix. | |
| () => _runWebE2eTest('profile_diagnostics_integration', buildMode: 'debug', useWasm: false), | |
| () => _runWebE2eTest('profile_diagnostics_integration', buildMode: 'profile', useWasm: false), | |
| () => _runWebE2eTest('profile_diagnostics_integration', buildMode: 'release', useWasm: false), | |
| // This test is only known to work in debug mode. | |
| () => _runWebE2eTest('scroll_wheel_integration', buildMode: 'debug', useWasm: false), | |
| // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x wasm mode matrix. | |
| // These tests have been extremely flaky, so we are temporarily disabling them until we figure out how to make them more robust. | |
| () => _runWebE2eTest('text_editing_integration', buildMode: 'debug', useWasm: false), | |
| () => _runWebE2eTest('text_editing_integration', buildMode: 'profile', useWasm: false), | |
| () => _runWebE2eTest('text_editing_integration', buildMode: 'release', useWasm: false), | |
| // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x wasm mode matrix. | |
| () => _runWebE2eTest('url_strategy_integration', buildMode: 'debug', useWasm: false), | |
| () => _runWebE2eTest('url_strategy_integration', buildMode: 'profile', useWasm: false), | |
| () => _runWebE2eTest('url_strategy_integration', buildMode: 'release', useWasm: false), | |
| // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x wasm mode matrix. | |
| () => | |
| _runWebE2eTest('capabilities_integration_canvaskit', buildMode: 'debug', useWasm: false), | |
| () => _runWebE2eTest( | |
| 'capabilities_integration_canvaskit', | |
| buildMode: 'profile', | |
| useWasm: false, | |
| ), | |
| () => _runWebE2eTest( | |
| 'capabilities_integration_canvaskit', | |
| buildMode: 'release', | |
| useWasm: false, | |
| ), | |
| // This test doesn't do anything interesting w.r.t. rendering, so we don't run the full build mode x wasm mode matrix. | |
| () => _runWebE2eTest( | |
| 'cache_width_cache_height_integration', | |
| buildMode: 'debug', | |
| useWasm: false, | |
| ), | |
| () => _runWebE2eTest( | |
| 'cache_width_cache_height_integration', | |
| buildMode: 'profile', | |
| useWasm: false, | |
| ), | |
| () => _runWebE2eTest('deferred_loading_integration', buildMode: 'release', useWasm: false), | |
| () => _runWebE2eTest('deferred_loading_integration', buildMode: 'release', useWasm: true), | |
| () => _runWebTreeshakeTest(), | |
| () => _runFlutterDriverWebTest( | |
| testAppDirectory: path.join(flutterRoot, 'examples', 'hello_world'), | |
| target: 'test_driver/smoke_web_engine.dart', | |
| buildMode: 'profile', | |
| useWasm: false, | |
| ), | |
| () => _runGalleryE2eWebTest('debug'), | |
| () => _runGalleryE2eWebTest('profile'), | |
| () => _runGalleryE2eWebTest('release'), | |
| // TODO(mdebbar): This test is flaky: https://github.com/flutter/flutter/issues/178032 | |
| // () => runServiceWorkerCleanupTest(headless: true), | |
| () => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'), | |
| () => _runWebStackTraceTest('release', 'lib/stack_trace.dart'), | |
| () => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'), | |
| () => _runWebStackTraceTest('release', 'lib/framework_stack_trace.dart'), | |
| () => _runWebDebugTest('lib/stack_trace.dart'), | |
| () => _runWebDebugTest('lib/framework_stack_trace.dart'), | |
| () => _runWebDebugTest('lib/web_directory_loading.dart'), | |
| // Don't run the CDN test if we're targeting presubmit, since engine artifacts won't actually | |
| // be uploaded to CDN yet. | |
| if (engineRealm.isEmpty) | |
| () => _runWebDebugTest( | |
| 'lib/web_resources_cdn_test.dart', | |
| additionalArguments: <String>['--dart-define=TEST_FLUTTER_ENGINE_VERSION=$engineVersion'], | |
| ), | |
| () => _runWebDebugTest('test/test.dart'), | |
| () => _runWebDebugTest('lib/null_safe_main.dart'), | |
| () => _runWebDebugTest( | |
| 'lib/web_define_loading.dart', | |
| additionalArguments: <String>[ | |
| '--dart-define=test.valueA=Example,A', | |
| '--dart-define=test.valueB=Value', | |
| ], | |
| ), | |
| () => _runWebReleaseTest( | |
| 'lib/web_define_loading.dart', | |
| additionalArguments: <String>[ | |
| '--dart-define=test.valueA=Example,A', | |
| '--dart-define=test.valueB=Value', | |
| ], | |
| ), | |
| () => _runWebDebugTest('lib/assertion_test.dart'), | |
| () => _runWebReleaseTest('lib/assertion_test.dart'), | |
| () => _runWebDebugTest('lib/sound_mode.dart'), | |
| () => _runWebReleaseTest('lib/sound_mode.dart'), | |
| () => _runFlutterWebTest(path.join(flutterRoot, 'packages', 'integration_test'), <String>[ | |
| 'test/web_extension_test.dart', | |
| ], false), | |
| () => _runFlutterWebTest(path.join(flutterRoot, 'packages', 'integration_test'), <String>[ | |
| 'test/web_extension_test.dart', | |
| ], true), | |
| ]; | |
| // Shuffling mixes fast tests with slow tests so shards take roughly the same | |
| // amount of time to run. | |
| tests.shuffle(math.Random(0)); | |
| await _ensureChromeDriverIsRunning(); | |
| await runShardRunnerIndexOfTotalSubshard(tests); | |
| await _stopChromeDriver(); | |
| } | |
| Future<void> runWebCanvasKitUnitTests() { | |
| return _runWebUnitTests(useWasm: false); | |
| } | |
| Future<void> runWebSkwasmUnitTests() { | |
| return _runWebUnitTests(useWasm: true); | |
| } | |
| /// Runs one of the `dev/integration_tests/web_e2e_tests` tests. | |
| Future<void> _runWebE2eTest( | |
| String name, { | |
| required String buildMode, | |
| required bool useWasm, | |
| }) async { | |
| await _runFlutterDriverWebTest( | |
| target: path.join('test_driver', '$name.dart'), | |
| buildMode: buildMode, | |
| useWasm: useWasm, | |
| testAppDirectory: path.join(flutterRoot, 'dev', 'integration_tests', 'web_e2e_tests'), | |
| ); | |
| } | |
| Future<void> _runFlutterDriverWebTest({ | |
| required String target, | |
| required String buildMode, | |
| required bool useWasm, | |
| required String testAppDirectory, | |
| String? driver, | |
| bool silenceBrowserOutput = false, | |
| bool expectWriteResponseFile = false, | |
| String expectResponseFileContent = '', | |
| }) async { | |
| printProgress('${green}Running integration tests $target in $buildMode mode.$reset'); | |
| await runCommand(flutter, <String>['clean'], workingDirectory: testAppDirectory); | |
| // This must match the testOutputsDirectory defined in flutter_driver's driver/common.dart. | |
| final String driverOutputPath = | |
| Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? path.join(testAppDirectory, 'build'); | |
| final String responseFile = path.join(driverOutputPath, 'integration_response_data.json'); | |
| if (File(responseFile).existsSync()) { | |
| File(responseFile).deleteSync(); | |
| } | |
| await runCommand( | |
| flutter, | |
| <String>[ | |
| ...flutterTestArgs, | |
| 'drive', | |
| if (driver != null) '--driver=$driver', | |
| '--target=$target', | |
| '--browser-name=chrome', | |
| '-d', | |
| 'web-server', | |
| '--$buildMode', | |
| if (useWasm) '--wasm', | |
| '--no-web-resources-cdn', | |
| ], | |
| workingDirectory: testAppDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| removeLine: (String line) { | |
| if (!silenceBrowserOutput) { | |
| return false; | |
| } | |
| if (line.trim().startsWith('[INFO]')) { | |
| return true; | |
| } | |
| return false; | |
| }, | |
| ); | |
| if (expectWriteResponseFile) { | |
| if (!File(responseFile).existsSync()) { | |
| foundError(<String>[ | |
| '$bold${red}Command did not write the response file but expected response file written.$reset', | |
| ]); | |
| } else { | |
| final String response = File(responseFile).readAsStringSync(); | |
| if (response != expectResponseFileContent) { | |
| foundError(<String>[ | |
| '$bold${red}Command write the response file with $response but expected response file with $expectResponseFileContent.$reset', | |
| ]); | |
| } | |
| } | |
| } | |
| } | |
| // Compiles a sample web app and checks that its JS doesn't contain certain | |
| // debug code that we expect to be tree shaken out. | |
| // | |
| // The app is compiled in `--profile` mode to prevent the compiler from | |
| // minifying the symbols. | |
| Future<void> _runWebTreeshakeTest() async { | |
| final String testAppDirectory = path.join( | |
| flutterRoot, | |
| 'dev', | |
| 'integration_tests', | |
| 'web_e2e_tests', | |
| ); | |
| final String target = path.join('lib', 'treeshaking_main.dart'); | |
| await runCommand(flutter, <String>['clean'], workingDirectory: testAppDirectory); | |
| await runCommand( | |
| flutter, | |
| <String>['build', 'web', '--target=$target', '--profile', '--no-web-resources-cdn'], | |
| workingDirectory: testAppDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| ); | |
| final mainDartJs = File(path.join(testAppDirectory, 'build', 'web', 'main.dart.js')); | |
| final String javaScript = mainDartJs.readAsStringSync(); | |
| // Check that we're not looking at minified JS. Otherwise this test would result in false positive. | |
| expect(javaScript.contains('RootElement'), true); | |
| const word = 'debugFillProperties'; | |
| var count = 0; | |
| int pos = javaScript.indexOf(word); | |
| final int contentLength = javaScript.length; | |
| while (pos != -1) { | |
| count += 1; | |
| pos += word.length; | |
| if (pos >= contentLength || count > 100) { | |
| break; | |
| } | |
| pos = javaScript.indexOf(word, pos); | |
| } | |
| // The following are classes from `timeline.dart` that should be treeshaken | |
| // off unless the app (typically a benchmark) uses methods that need them. | |
| expect(javaScript.contains('AggregatedTimedBlock'), false); | |
| expect(javaScript.contains('AggregatedTimings'), false); | |
| expect(javaScript.contains('_BlockBuffer'), false); | |
| expect(javaScript.contains('_StringListChain'), false); | |
| expect(javaScript.contains('_Float64ListChain'), false); | |
| const kMaxExpectedDebugFillProperties = 11; | |
| if (count > kMaxExpectedDebugFillProperties) { | |
| throw Exception( | |
| 'Too many occurrences of "$word" in compiled JavaScript.\n' | |
| 'Expected no more than $kMaxExpectedDebugFillProperties, but found $count.', | |
| ); | |
| } | |
| } | |
| /// Exercises the old gallery in a browser for a long period of time, looking | |
| /// for memory leaks and dangling pointers. | |
| /// | |
| /// This is not a performance test. | |
| /// | |
| /// The test is written using `package:integration_test` (despite the "e2e" in | |
| /// the name, which is there for historic reasons). | |
| Future<void> _runGalleryE2eWebTest(String buildMode) async { | |
| printProgress( | |
| '${green}Running flutter_gallery integration test in --$buildMode using CanvasKit.$reset', | |
| ); | |
| final String testAppDirectory = path.join( | |
| flutterRoot, | |
| 'dev', | |
| 'integration_tests', | |
| 'flutter_gallery', | |
| ); | |
| await runCommand(flutter, <String>['clean'], workingDirectory: testAppDirectory); | |
| await runCommand( | |
| flutter, | |
| <String>[ | |
| ...flutterTestArgs, | |
| 'drive', | |
| '--dart-define=FLUTTER_WEB_USE_SKIA=true', | |
| '--driver=test_driver/transitions_perf_e2e_test.dart', | |
| '--target=test_driver/transitions_perf_e2e.dart', | |
| '--browser-name=chrome', | |
| '-d', | |
| 'web-server', | |
| '--$buildMode', | |
| '--no-web-resources-cdn', | |
| ], | |
| workingDirectory: testAppDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| ); | |
| } | |
| Future<void> _runWebStackTraceTest(String buildMode, String entrypoint) async { | |
| final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web'); | |
| final String appBuildDirectory = path.join(testAppDirectory, 'build', 'web'); | |
| // Build the app. | |
| await runCommand(flutter, <String>['clean'], workingDirectory: testAppDirectory); | |
| await runCommand( | |
| flutter, | |
| <String>['build', 'web', '--$buildMode', '-t', entrypoint, '--no-web-resources-cdn'], | |
| workingDirectory: testAppDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| ); | |
| // Run the app. | |
| final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests(); | |
| final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests(); | |
| final String result = await evalTestAppInChrome( | |
| appUrl: 'http://localhost:$serverPort/index.html', | |
| appDirectory: appBuildDirectory, | |
| serverPort: serverPort, | |
| browserDebugPort: browserDebugPort, | |
| ); | |
| if (!result.contains('--- TEST SUCCEEDED ---')) { | |
| foundError(<String>[result, '${red}Web stack trace integration test failed.$reset']); | |
| } | |
| } | |
| /// Debug mode is special because `flutter build web` doesn't build in debug mode. | |
| /// | |
| /// Instead, we use `flutter run --debug` and sniff out the standard output. | |
| Future<void> _runWebDebugTest( | |
| String target, { | |
| List<String> additionalArguments = const <String>[], | |
| }) async { | |
| final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web'); | |
| var success = false; | |
| final environment = <String, String>{'FLUTTER_WEB': 'true'}; | |
| adjustEnvironmentToEnableFlutterAsserts(environment); | |
| final CommandResult result = await runCommand( | |
| flutter, | |
| <String>[ | |
| 'run', | |
| '--verbose', | |
| '--debug', | |
| '-d', | |
| 'chrome', | |
| '--web-run-headless', | |
| '--dart-define=FLUTTER_WEB_USE_SKIA=false', | |
| ...additionalArguments, | |
| '-t', | |
| target, | |
| ], | |
| outputMode: OutputMode.capture, | |
| outputListener: (String line, Process process) { | |
| var shutdownFlutterTool = false; | |
| if (line.contains('--- TEST SUCCEEDED ---')) { | |
| success = true; | |
| shutdownFlutterTool = true; | |
| } | |
| if (line.contains('--- TEST FAILED ---')) { | |
| shutdownFlutterTool = true; | |
| } | |
| if (shutdownFlutterTool) { | |
| process.stdin.add('q'.codeUnits); | |
| } | |
| }, | |
| workingDirectory: testAppDirectory, | |
| environment: environment, | |
| ); | |
| if (!success) { | |
| foundError(<String>[ | |
| result.flattenedStdout!, | |
| result.flattenedStderr!, | |
| '${red}Web stack trace integration test failed.$reset', | |
| ]); | |
| } | |
| } | |
| /// Run a web integration test in release mode. | |
| Future<void> _runWebReleaseTest( | |
| String target, { | |
| List<String> additionalArguments = const <String>[], | |
| }) async { | |
| final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web'); | |
| final String appBuildDirectory = path.join(testAppDirectory, 'build', 'web'); | |
| // Build the app. | |
| await runCommand(flutter, <String>['clean'], workingDirectory: testAppDirectory); | |
| await runCommand( | |
| flutter, | |
| <String>[ | |
| ...flutterTestArgs, | |
| 'build', | |
| 'web', | |
| '--release', | |
| '--no-web-resources-cdn', | |
| ...additionalArguments, | |
| '-t', | |
| target, | |
| ], | |
| workingDirectory: testAppDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| ); | |
| // Run the app. | |
| final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests(); | |
| final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests(); | |
| final String result = await evalTestAppInChrome( | |
| appUrl: 'http://localhost:$serverPort/index.html', | |
| appDirectory: appBuildDirectory, | |
| serverPort: serverPort, | |
| browserDebugPort: browserDebugPort, | |
| ); | |
| if (!result.contains('--- TEST SUCCEEDED ---')) { | |
| foundError(<String>[result, '${red}Web release mode test failed.$reset']); | |
| } | |
| } | |
| Future<void> _runWebUnitTests({required bool useWasm}) async { | |
| final flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter')); | |
| final flutterPackageTestDirectory = Directory(path.join(flutterPackageDirectory.path, 'test')); | |
| final List<String> allTests = | |
| flutterPackageTestDirectory | |
| .listSync() | |
| .whereType<Directory>() | |
| .expand( | |
| (Directory directory) => directory | |
| .listSync(recursive: true) | |
| .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart')), | |
| ) | |
| .whereType<File>() | |
| .map<String>( | |
| (File file) => path.relative(file.path, from: flutterPackageDirectory.path), | |
| ) | |
| .where( | |
| (String filePath) => | |
| !kWebTestFileKnownFailures[useWasm]!.contains(path.split(filePath).join('/')), | |
| ) | |
| .toList() | |
| // Finally we shuffle the list because we want the average cost per file to be uniformly | |
| // distributed. If the list is not sorted then different shards and batches may have | |
| // very different characteristics. | |
| // We use a constant seed for repeatability. | |
| ..shuffle(math.Random(0)); | |
| final String? subshardName = Platform.environment[kSubshardKey]; | |
| var isLastShard = true; | |
| var testsToRun = allTests; | |
| if (subshardName != null) { | |
| final Pattern indexTotalPattern = RegExp(r'^(\d+)_(\d+)$'); | |
| final Match? match = indexTotalPattern.matchAsPrefix(subshardName); | |
| if (match == null) { | |
| foundError(<String>[ | |
| '${red}Invalid subshard name "$subshardName". Expected format "[int]_[int]" (e.g. "1_8").', | |
| ]); | |
| throw Exception('Invalid subshard name: $subshardName'); | |
| } | |
| final int index = int.parse(match.group(1)!); | |
| final int total = int.parse(match.group(2)!); | |
| isLastShard = index == total; | |
| testsToRun = selectIndexOfTotalSubshard<String>(allTests); | |
| } | |
| await _runFlutterWebTest(flutterPackageDirectory.path, testsToRun, useWasm); | |
| // The last shard also runs the flutter_web_plugins and flutter_driver tests. | |
| if (isLastShard) { | |
| await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter_web_plugins'), <String>[ | |
| 'test', | |
| ], useWasm); | |
| await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter_driver'), <String>[ | |
| path.join('test', 'src', 'web_tests', 'web_extension_test.dart'), | |
| ], useWasm); | |
| } | |
| } | |
| Future<void> _runFlutterWebTest(String workingDirectory, List<String> tests, bool useWasm) async { | |
| const fileSystem = LocalFileSystem(); | |
| final suffix = DateTime.now().microsecondsSinceEpoch.toString(); | |
| final File metricFile = fileSystem.systemTempDirectory.childFile('metrics_$suffix.json'); | |
| await runCommand( | |
| flutter, | |
| <String>[ | |
| 'test', | |
| '--reporter=expanded', | |
| '--file-reporter=json:${metricFile.path}', | |
| '-v', | |
| '--platform=chrome', | |
| if (useWasm) '--wasm', | |
| '--dart-define=DART_HHH_BOT=$runningInDartHHHBot', | |
| ...flutterTestArgs, | |
| ...tests, | |
| ], | |
| workingDirectory: workingDirectory, | |
| environment: <String, String>{'FLUTTER_WEB': 'true'}, | |
| ); | |
| // metriciFile is a transitional file that needs to be deleted once it is parsed. | |
| // TODO(godofredoc): Ensure metricFile is parsed and aggregated before deleting. | |
| // https://github.com/flutter/flutter/issues/146003 | |
| if (!dryRun) { | |
| metricFile.deleteSync(); | |
| } | |
| } | |
| // The `chromedriver` process created by this test. | |
| // | |
| // If an existing chromedriver is already available on port 4444, the existing | |
| // process is reused and this variable remains null. | |
| Command? _chromeDriver; | |
| Future<bool> _isChromeDriverRunning() async { | |
| try { | |
| final RawSocket socket = await RawSocket.connect('localhost', 4444); | |
| socket.shutdown(SocketDirection.both); | |
| await socket.close(); | |
| return true; | |
| } on SocketException { | |
| return false; | |
| } | |
| } | |
| Future<void> _stopChromeDriver() async { | |
| if (_chromeDriver == null) { | |
| return; | |
| } | |
| print('Stopping chromedriver'); | |
| _chromeDriver!.process.kill(); | |
| } | |
| Future<void> _ensureChromeDriverIsRunning() async { | |
| // If we cannot connect to ChromeDriver, assume it is not running. Launch it. | |
| if (!await _isChromeDriverRunning()) { | |
| printProgress('Starting chromedriver'); | |
| // Assume chromedriver is in the PATH. | |
| _chromeDriver = await startCommand( | |
| // TODO(ianh): this is the only remaining consumer of startCommand other than runCommand | |
| // and it doesn't use most of startCommand's features; we could simplify this a lot by | |
| // inlining the relevant parts of startCommand here. | |
| 'chromedriver', | |
| <String>['--port=4444', '--log-level=INFO', '--enable-chrome-logs'], | |
| ); | |
| while (!await _isChromeDriverRunning()) { | |
| await Future<void>.delayed(const Duration(milliseconds: 100)); | |
| print('Waiting for chromedriver to start up.'); | |
| } | |
| } | |
| final client = HttpClient(); | |
| final Uri chromeDriverUrl = Uri.parse('http://localhost:4444/status'); | |
| final HttpClientRequest request = await client.getUrl(chromeDriverUrl); | |
| final HttpClientResponse response = await request.close(); | |
| final String responseString = await response.transform(utf8.decoder).join(); | |
| final webDriverStatus = json.decode(responseString) as Map<String, dynamic>; | |
| client.close(); | |
| final webDriverReady = (webDriverStatus['value'] as Map<String, dynamic>)['ready'] as bool; | |
| if (!webDriverReady) { | |
| throw Exception('WebDriver not available.'); | |
| } | |
| } | |
| } |