// 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. // Runs the tests for the flutter/flutter repository. // // By default, test output is filtered and only errors are shown. (If a // particular test takes longer than _quietTimeout in utils.dart, the output is // shown then also, in case something has hung.) // // --verbose stops the output cleanup and just outputs everything verbatim. // // By default, errors are non-fatal; all tests are executed and the output // ends with a summary of the errors that were detected. // // Exit code is 1 if there was an error. // // --abort-on-error causes the script to exit immediately when hitting an error. // // By default, all tests are run. However, the tests support being split by // shard and subshard. Inspect the code to see what shards and subshards are // supported, or run with `--dry-run` to get a list of tests that _would_ have // been executed. // // If the CIRRUS_TASK_NAME environment variable exists, it is used to determine // the shard and sub-shard, by parsing it in the form shard-subshard-platform, // ignoring the platform. // // For local testing you can just set the SHARD and SUBSHARD environment // variables. For example, to run all the framework tests you can just set // SHARD=framework_tests. Some shards support named subshards, like // SHARD=framework_tests SUBSHARD=widgets. Others support arbitrary numbered // subsharding, like SHARD=build_tests SUBSHARD=1_2 (where 1_2 means "one of // two" as in run the first half of the tests). // // So for example to run specifically the third subshard of the Web tests you // would set SHARD=web_tests SUBSHARD=2 (it's zero-based). // // By default, where supported, tests within a shard are executed in a random // order to (eventually) catch inter-test dependencies. // // --test-randomize-ordering-seed= sets the shuffle seed for reproducing runs. // // All other arguments are treated as arguments to pass to the flutter tool when // running tests. import 'dart:convert'; import 'dart:core' as system show print; import 'dart:core' hide print; import 'dart:io' as system show exit; import 'dart:io' hide exit; import 'dart:math' as math; import 'package:path/path.dart' as path; import 'run_command.dart'; import 'suite_runners/run_add_to_app_life_cycle_tests.dart'; import 'suite_runners/run_analyze_tests.dart'; import 'suite_runners/run_android_java11_integration_tool_tests.dart'; import 'suite_runners/run_android_preview_integration_tool_tests.dart'; import 'suite_runners/run_customer_testing_tests.dart'; import 'suite_runners/run_docs_tests.dart'; import 'suite_runners/run_flutter_driver_android_tests.dart'; import 'suite_runners/run_flutter_packages_tests.dart'; import 'suite_runners/run_framework_coverage_tests.dart'; import 'suite_runners/run_framework_tests.dart'; import 'suite_runners/run_fuchsia_precache.dart'; import 'suite_runners/run_realm_checker_tests.dart'; import 'suite_runners/run_skp_generator_tests.dart'; import 'suite_runners/run_test_harness_tests.dart'; import 'suite_runners/run_verify_binaries_codesigned_tests.dart'; import 'suite_runners/run_web_tests.dart'; import 'utils.dart'; typedef ShardRunner = Future Function(); /// Environment variables to override the local engine when running `pub test`, /// if such flags are provided to `test.dart`. final Map localEngineEnv = {}; const String CIRRUS_TASK_NAME = 'CIRRUS_TASK_NAME'; /// When you call this, you can pass additional arguments to pass custom /// arguments to flutter test. For example, you might want to call this /// script with the parameter --local-engine=host_debug_unopt to /// use your own build of the engine. /// /// To run the tool_tests part, run it with SHARD=tool_tests /// /// Examples: /// SHARD=tool_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart /// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt --local-engine-host=host_debug_unopt Future main(List args) async { try { printProgress('STARTING ANALYSIS'); bool dryRunArgSet = false; for (final String arg in args) { if (arg.startsWith('--local-engine=')) { localEngineEnv['FLUTTER_LOCAL_ENGINE'] = arg.substring('--local-engine='.length); flutterTestArgs.add(arg); } else if (arg.startsWith('--local-engine-host=')) { localEngineEnv['FLUTTER_LOCAL_ENGINE_HOST'] = arg.substring('--local-engine-host='.length); flutterTestArgs.add(arg); } else if (arg.startsWith('--local-engine-src-path=')) { localEngineEnv['FLUTTER_LOCAL_ENGINE_SRC_PATH'] = arg.substring('--local-engine-src-path='.length); flutterTestArgs.add(arg); } else if (arg.startsWith('--test-randomize-ordering-seed=')) { shuffleSeed = arg.substring('--test-randomize-ordering-seed='.length); } else if (arg.startsWith('--verbose')) { print = (Object? message) { system.print(message); }; } else if (arg.startsWith('--abort-on-error')) { onError = () { system.exit(1); }; } else if (arg == '--dry-run') { dryRunArgSet = true; printProgress('--dry-run enabled. Tests will not actually be executed.'); } else { flutterTestArgs.add(arg); } } if (dryRunArgSet) { enableDryRun(); } if (Platform.environment.containsKey(CIRRUS_TASK_NAME)) { printProgress('Running task: ${Platform.environment[CIRRUS_TASK_NAME]}'); } final WebTestsSuite webTestsSuite = WebTestsSuite(flutterTestArgs); await selectShard({ 'add_to_app_life_cycle_tests': addToAppLifeCycleRunner, 'build_tests': _runBuildTests, 'framework_coverage': frameworkCoverageRunner, 'framework_tests': frameworkTestsRunner, 'tool_tests': _runToolTests, 'web_tool_tests': _runWebToolTests, 'tool_integration_tests': _runIntegrationToolTests, 'android_preview_tool_integration_tests': androidPreviewIntegrationToolTestsRunner, 'android_java11_tool_integration_tests': androidJava11IntegrationToolTestsRunner, 'tool_host_cross_arch_tests': _runToolHostCrossArchTests, // All the unit/widget tests run using `flutter test --platform=chrome --web-renderer=html` 'web_tests': webTestsSuite.runWebHtmlUnitTests, // All the unit/widget tests run using `flutter test --platform=chrome --web-renderer=canvaskit` 'web_canvaskit_tests': webTestsSuite.runWebCanvasKitUnitTests, // All the unit/widget tests run using `flutter test --platform=chrome --wasm --web-renderer=skwasm` 'web_skwasm_tests': webTestsSuite.runWebSkwasmUnitTests, // All web integration tests 'web_long_running_tests': webTestsSuite.webLongRunningTestsRunner, 'flutter_driver_android': runFlutterDriverAndroidTests, 'flutter_plugins': flutterPackagesRunner, 'skp_generator': skpGeneratorTestsRunner, 'realm_checker': realmCheckerTestRunner, 'customer_testing': customerTestingRunner, 'analyze': analyzeRunner, 'fuchsia_precache': fuchsiaPrecacheRunner, 'snippets': _runSnippetsTests, 'docs': docsRunner, 'verify_binaries_codesigned': verifyCodesignedTestRunner, kTestHarnessShardName: testHarnessTestsRunner, // Used for testing this script; also run as part of SHARD=framework_tests, SUBSHARD=misc. }); } catch (error, stackTrace) { foundError([ 'UNEXPECTED ERROR!', error.toString(), ...stackTrace.toString().split('\n'), 'The test.dart script should be corrected to catch this error and call foundError().', '${yellow}Some tests are likely to have been skipped.$reset', ]); system.exit(255); } if (hasError) { reportErrorsAndExit('${bold}Test failed.$reset'); } reportSuccessAndExit('${bold}Test successful.$reset'); } final String _toolsPath = path.join(flutterRoot, 'packages', 'flutter_tools'); Future _runGeneralToolTests() async { await runDartTest( _toolsPath, testPaths: [path.join('test', 'general.shard')], enableFlutterToolAsserts: false, // Detect unit test time regressions (poor time delay handling, etc). // This overrides the 15 minute default for tools tests. // See the README.md and dart_test.yaml files in the flutter_tools package. perTestTimeout: const Duration(seconds: 2), ); } Future _runCommandsToolTests() async { await runDartTest( _toolsPath, forceSingleCore: true, testPaths: [path.join('test', 'commands.shard')], ); } Future _runWebToolTests() async { final List allFiles = Directory(path.join(_toolsPath, 'test', 'web.shard')) .listSync(recursive: true).whereType().toList(); final List allTests = []; for (final File file in allFiles) { if (file.path.endsWith('_test.dart')) { allTests.add(file.path); } } await runDartTest( _toolsPath, forceSingleCore: true, testPaths: selectIndexOfTotalSubshard(allTests), includeLocalEngineEnv: true, ); } Future _runToolHostCrossArchTests() { return runDartTest( _toolsPath, // These are integration tests forceSingleCore: true, testPaths: [path.join('test', 'host_cross_arch.shard')], ); } Future _runIntegrationToolTests() async { final List allTests = Directory(path.join(_toolsPath, 'test', 'integration.shard')) .listSync(recursive: true).whereType() .map((FileSystemEntity entry) => path.relative(entry.path, from: _toolsPath)) .where((String testPath) => path.basename(testPath).endsWith('_test.dart')).toList(); await runDartTest( _toolsPath, forceSingleCore: true, testPaths: selectIndexOfTotalSubshard(allTests), collectMetrics: true, ); } Future _runToolTests() async { await selectSubshard({ 'general': _runGeneralToolTests, 'commands': _runCommandsToolTests, }); } Future _runSnippetsTests() async { final String snippetsPath = path.join(flutterRoot, 'dev', 'snippets'); final List allTests = Directory(path.join(snippetsPath, 'test')) .listSync(recursive: true).whereType() .map((FileSystemEntity entry) => path.relative(entry.path, from: _toolsPath)) .where((String testPath) => path.basename(testPath).endsWith('_test.dart')).toList(); await runDartTest( snippetsPath, forceSingleCore: true, testPaths: selectIndexOfTotalSubshard(allTests), collectMetrics: true, ); } Future runForbiddenFromReleaseTests() async { // Build a release APK to get the snapshot json. final Directory tempDirectory = Directory.systemTemp.createTempSync('flutter_forbidden_imports.'); final List command = [ 'build', 'apk', '--target-platform', 'android-arm64', '--release', '--analyze-size', '--code-size-directory', tempDirectory.path, '-v', ]; await runCommand( flutter, command, workingDirectory: path.join(flutterRoot, 'examples', 'hello_world'), ); // First, a smoke test. final List smokeTestArgs = [ path.join(flutterRoot, 'dev', 'forbidden_from_release_tests', 'bin', 'main.dart'), '--snapshot', path.join(tempDirectory.path, 'snapshot.arm64-v8a.json'), '--package-config', path.join(flutterRoot, 'examples', 'hello_world', '.dart_tool', 'package_config.json'), '--forbidden-type', 'package:flutter/src/widgets/framework.dart::Widget', ]; await runCommand( dart, smokeTestArgs, workingDirectory: flutterRoot, expectNonZeroExit: true, ); // Actual test. final List args = [ path.join(flutterRoot, 'dev', 'forbidden_from_release_tests', 'bin', 'main.dart'), '--snapshot', path.join(tempDirectory.path, 'snapshot.arm64-v8a.json'), '--package-config', path.join(flutterRoot, 'examples', 'hello_world', '.dart_tool', 'package_config.json'), '--forbidden-type', 'package:flutter/src/widgets/widget_inspector.dart::WidgetInspectorService', '--forbidden-type', 'package:flutter/src/widgets/framework.dart::DebugCreator', '--forbidden-type', 'package:flutter/src/foundation/print.dart::debugPrint', ]; await runCommand( dart, args, workingDirectory: flutterRoot, ); } /// Verifies that APK, and IPA (if on macOS), and native desktop builds the /// examples apps without crashing. It does not actually launch the apps. That /// happens later in the devicelab. This is just a smoke-test. In particular, /// this will verify we can build when there are spaces in the path name for the /// Flutter SDK and target app. /// /// Also does some checking about types included in hello_world. Future _runBuildTests() async { final List exampleDirectories = Directory(path.join(flutterRoot, 'examples')).listSync() // API example builds will be tested in a separate shard. .where((FileSystemEntity entity) => entity is Directory && path.basename(entity.path) != 'api').cast().toList() ..add(Directory(path.join(flutterRoot, 'packages', 'integration_test', 'example'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'android_views'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'channels'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'hybrid_android_views'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'flutter_gallery'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ios_platform_view_tests'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ios_app_with_extensions'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'platform_interaction'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'spell_check'))) ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ui'))); // The tests are randomly distributed into subshards so as to get a uniform // distribution of costs, but the seed is fixed so that issues are reproducible. final List tests = [ for (final Directory exampleDirectory in exampleDirectories) () => _runExampleProjectBuildTests(exampleDirectory), ...[ // Web compilation tests. () => _flutterBuildDart2js( path.join('dev', 'integration_tests', 'web'), path.join('lib', 'main.dart'), ), // Should not fail to compile with dart:io. () => _flutterBuildDart2js( path.join('dev', 'integration_tests', 'web_compile_tests'), path.join('lib', 'dart_io_import.dart'), ), // Should be able to compile with a call to: // BackgroundIsolateBinaryMessenger.ensureInitialized. () => _flutterBuildDart2js( path.join('dev', 'integration_tests', 'web_compile_tests'), path.join('lib', 'background_isolate_binary_messenger.dart'), ), ], runForbiddenFromReleaseTests, ]..shuffle(math.Random(0)); await runShardRunnerIndexOfTotalSubshard(tests); } Future _runExampleProjectBuildTests(Directory exampleDirectory, [File? mainFile]) async { // Only verify caching with flutter gallery. final bool verifyCaching = exampleDirectory.path.contains('flutter_gallery'); final String examplePath = path.relative(exampleDirectory.path, from: Directory.current.path); final List additionalArgs = [ if (mainFile != null) path.relative(mainFile.path, from: exampleDirectory.absolute.path), ]; if (Directory(path.join(examplePath, 'android')).existsSync()) { await _flutterBuildApk(examplePath, release: false, additionalArgs: additionalArgs, verifyCaching: verifyCaching); await _flutterBuildApk(examplePath, release: true, additionalArgs: additionalArgs, verifyCaching: verifyCaching); } else { print('Example project ${path.basename(examplePath)} has no android directory, skipping apk'); } if (Platform.isMacOS) { if (Directory(path.join(examplePath, 'ios')).existsSync()) { await _flutterBuildIpa(examplePath, release: false, additionalArgs: additionalArgs, verifyCaching: verifyCaching); await _flutterBuildIpa(examplePath, release: true, additionalArgs: additionalArgs, verifyCaching: verifyCaching); } else { print('Example project ${path.basename(examplePath)} has no ios directory, skipping ipa'); } } if (Platform.isLinux) { if (Directory(path.join(examplePath, 'linux')).existsSync()) { await _flutterBuildLinux(examplePath, release: false, additionalArgs: additionalArgs, verifyCaching: verifyCaching); await _flutterBuildLinux(examplePath, release: true, additionalArgs: additionalArgs, verifyCaching: verifyCaching); } else { print('Example project ${path.basename(examplePath)} has no linux directory, skipping Linux'); } } if (Platform.isMacOS) { if (Directory(path.join(examplePath, 'macos')).existsSync()) { await _flutterBuildMacOS(examplePath, release: false, additionalArgs: additionalArgs, verifyCaching: verifyCaching); await _flutterBuildMacOS(examplePath, release: true, additionalArgs: additionalArgs, verifyCaching: verifyCaching); } else { print('Example project ${path.basename(examplePath)} has no macos directory, skipping macOS'); } } if (Platform.isWindows) { if (Directory(path.join(examplePath, 'windows')).existsSync()) { await _flutterBuildWin32(examplePath, release: false, additionalArgs: additionalArgs, verifyCaching: verifyCaching); await _flutterBuildWin32(examplePath, release: true, additionalArgs: additionalArgs, verifyCaching: verifyCaching); } else { print('Example project ${path.basename(examplePath)} has no windows directory, skipping Win32'); } } } Future _flutterBuildApk(String relativePathToApplication, { required bool release, bool verifyCaching = false, List additionalArgs = const [], }) async { printProgress('${green}Testing APK ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...'); await _flutterBuild(relativePathToApplication, 'APK', 'apk', release: release, verifyCaching: verifyCaching, additionalArgs: additionalArgs ); } Future _flutterBuildIpa(String relativePathToApplication, { required bool release, List additionalArgs = const [], bool verifyCaching = false, }) async { assert(Platform.isMacOS); printProgress('${green}Testing IPA ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...'); await _flutterBuild(relativePathToApplication, 'IPA', 'ios', release: release, verifyCaching: verifyCaching, additionalArgs: [...additionalArgs, '--no-codesign'], ); } Future _flutterBuildLinux(String relativePathToApplication, { required bool release, bool verifyCaching = false, List additionalArgs = const [], }) async { assert(Platform.isLinux); await runCommand(flutter, ['config', '--enable-linux-desktop']); printProgress('${green}Testing Linux ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...'); await _flutterBuild(relativePathToApplication, 'Linux', 'linux', release: release, verifyCaching: verifyCaching, additionalArgs: additionalArgs ); } Future _flutterBuildMacOS(String relativePathToApplication, { required bool release, bool verifyCaching = false, List additionalArgs = const [], }) async { assert(Platform.isMacOS); await runCommand(flutter, ['config', '--enable-macos-desktop']); printProgress('${green}Testing macOS ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...'); await _flutterBuild(relativePathToApplication, 'macOS', 'macos', release: release, verifyCaching: verifyCaching, additionalArgs: additionalArgs ); } Future _flutterBuildWin32(String relativePathToApplication, { required bool release, bool verifyCaching = false, List additionalArgs = const [], }) async { assert(Platform.isWindows); printProgress('${green}Testing ${release ? 'release' : 'debug'} Windows build$reset for $cyan$relativePathToApplication$reset...'); await _flutterBuild(relativePathToApplication, 'Windows', 'windows', release: release, verifyCaching: verifyCaching, additionalArgs: additionalArgs ); } Future _flutterBuild( String relativePathToApplication, String platformLabel, String platformBuildName, { required bool release, bool verifyCaching = false, List additionalArgs = const [], }) async { await runCommand(flutter, [ 'build', platformBuildName, ...additionalArgs, if (release) '--release' else '--debug', '-v', ], workingDirectory: path.join(flutterRoot, relativePathToApplication), ); if (verifyCaching) { printProgress('${green}Testing $platformLabel cache$reset for $cyan$relativePathToApplication$reset...'); await runCommand(flutter, [ 'build', platformBuildName, '--performance-measurement-file=perf.json', ...additionalArgs, if (release) '--release' else '--debug', '-v', ], workingDirectory: path.join(flutterRoot, relativePathToApplication), ); final File file = File(path.join(flutterRoot, relativePathToApplication, 'perf.json')); if (!_allTargetsCached(file)) { foundError([ '${red}Not all build targets cached after second run.$reset', 'The target performance data was: ${file.readAsStringSync().replaceAll('},', '},\n')}', ]); } } } bool _allTargetsCached(File performanceFile) { if (dryRun) { return true; } final Map data = json.decode(performanceFile.readAsStringSync()) as Map; final List> targets = (data['targets']! as List) .cast>(); return targets.every((Map element) => element['skipped'] == true); } Future _flutterBuildDart2js(String relativePathToApplication, String target, { bool expectNonZeroExit = false }) async { printProgress('${green}Testing Dart2JS build$reset for $cyan$relativePathToApplication$reset...'); await runCommand(flutter, ['build', 'web', '-v', '--target=$target'], workingDirectory: path.join(flutterRoot, relativePathToApplication), expectNonZeroExit: expectNonZeroExit, environment: { 'FLUTTER_WEB': 'true', }, ); }