| // Copyright 2013 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:file/file.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import 'common/core.dart'; | |
| import 'common/file_filters.dart'; | |
| import 'common/output_utils.dart'; | |
| import 'common/package_looping_command.dart'; | |
| import 'common/plugin_utils.dart'; | |
| import 'common/pub_utils.dart'; | |
| import 'common/repository_package.dart'; | |
| const int _exitUnknownTestPlatform = 3; | |
| const int _exitNoTestsRan = 79; | |
| enum _TestPlatform { | |
| // Must run in the command-line VM. | |
| vm, | |
| // Must run in a browser. | |
| browser, | |
| } | |
| /// A command to run Dart unit tests for packages. | |
| class DartTestCommand extends PackageLoopingCommand { | |
| /// Creates an instance of the test command. | |
| DartTestCommand(super.packagesDir, {super.processRunner, super.platform, super.gitDir}) { | |
| argParser.addOption( | |
| kEnableExperiment, | |
| defaultsTo: '', | |
| help: | |
| 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' | |
| 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' | |
| 'for details.', | |
| ); | |
| argParser.addOption( | |
| _platformFlag, | |
| help: | |
| 'Runs tests on the given platform instead of the default platform ' | |
| '("vm" in most cases, "chrome" for web plugin implementations).', | |
| ); | |
| argParser.addFlag(kWebWasmFlag, help: 'Compile to WebAssembly rather than JavaScript'); | |
| argParser.addOption( | |
| _packageTagsFlag, | |
| help: | |
| 'Path to a YAML file that maps package names to tags to run for that ' | |
| 'package.', | |
| ); | |
| } | |
| static const String _platformFlag = 'platform'; | |
| static const String _packageTagsFlag = 'package-tags'; | |
| Map<String, String>? _packageTags; | |
| @override | |
| final String name = 'dart-test'; | |
| // TODO(stuartmorgan): Eventually remove 'test', which is a legacy name from | |
| // before there were other test commands that made it ambiguous. For now it's | |
| // an alias to avoid breaking people's workflows. | |
| @override | |
| List<String> get aliases => <String>['test', 'test-dart']; | |
| @override | |
| final String description = | |
| 'Runs the Dart tests for all packages.\n\n' | |
| 'This command requires "flutter" to be in your path.'; | |
| @override | |
| PackageLoopingType get packageLoopingType => PackageLoopingType.includeAllSubpackages; | |
| @override | |
| bool shouldIgnoreFile(String path) { | |
| return isRepoLevelNonCodeImpactingFile(path) || | |
| isNativeCodeFile(path) || | |
| isPackageSupportFile(path); | |
| } | |
| @override | |
| Future<void> initializeRun() async { | |
| _initializePackageTags(); | |
| } | |
| @override | |
| Future<PackageResult> runForPackage(RepositoryPackage package) async { | |
| if (!package.testDirectory.existsSync()) { | |
| return PackageResult.skip('No test/ directory.'); | |
| } | |
| String? platform = getNullableStringArg(_platformFlag); | |
| // Skip running plugin tests for non-web-supporting plugins (or non-web | |
| // federated plugin implementations) on web, since there's no reason to | |
| // expect them to work. | |
| final bool webPlatform = platform != null && platform != 'vm'; | |
| final explicitVMPlatform = platform == 'vm'; | |
| final bool isWebOnlyPluginImplementation = | |
| pluginSupportsPlatform(platformWeb, package, requiredMode: PlatformSupport.inline) && | |
| package.directory.basename.endsWith('_web'); | |
| if (webPlatform) { | |
| if (isFlutterPlugin(package) && !pluginSupportsPlatform(platformWeb, package)) { | |
| return PackageResult.skip("Non-web plugin tests don't need web testing."); | |
| } | |
| if (_testOnTarget(package) == _TestPlatform.vm) { | |
| // This explict skip is necessary because trying to run tests in a mode | |
| // that the package has opted out of returns a non-zero exit code. | |
| return PackageResult.skip('Package has opted out of non-vm testing.'); | |
| } | |
| } else if (explicitVMPlatform) { | |
| if (isWebOnlyPluginImplementation) { | |
| return PackageResult.skip("Web plugin tests don't need vm testing."); | |
| } | |
| final _TestPlatform? target = _testOnTarget(package); | |
| if (target != null && _testOnTarget(package) != _TestPlatform.vm) { | |
| // This explict skip is necessary because trying to run tests in a mode | |
| // that the package has opted out of returns a non-zero exit code. | |
| return PackageResult.skip('Package has opted out of vm testing.'); | |
| } | |
| } else if (platform == null && isWebOnlyPluginImplementation) { | |
| // If no explicit mode is requested, run web plugin implementations in | |
| // Chrome since their tests are not expected to work in vm mode. This | |
| // allows easily running all unit tests locally, without having to run | |
| // both modes. | |
| platform = 'chrome'; | |
| } | |
| // Whether to run web tests compiled to wasm. | |
| final bool wasm = platform != 'vm' && getBoolArg(kWebWasmFlag); | |
| final String? tag = _getTagForPackage(package); | |
| final int exitCode; | |
| if (package.requiresFlutter()) { | |
| exitCode = await _runFlutterTests(package, platform: platform, wasm: wasm, tag: tag); | |
| } else { | |
| exitCode = await _runDartTests(package, platform: platform, wasm: wasm, tag: tag); | |
| } | |
| if (tag != null && exitCode == _exitNoTestsRan) { | |
| return PackageResult.skip('No tests match the requested tag selector.'); | |
| } | |
| return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); | |
| } | |
| /// Runs the Dart tests for a Flutter package, returning the process exit code. | |
| Future<int> _runFlutterTests( | |
| RepositoryPackage package, { | |
| String? platform, | |
| bool wasm = false, | |
| String? tag, | |
| }) async { | |
| final String experiment = getStringArg(kEnableExperiment); | |
| return processRunner.runAndStream(flutterCommand, <String>[ | |
| 'test', | |
| '--color', | |
| if (experiment.isNotEmpty) '--enable-experiment=$experiment', | |
| // Flutter defaults to VM mode (under a different name) and explicitly | |
| // setting it is deprecated, so pass nothing in that case. | |
| if (platform != null && platform != 'vm') '--platform=$platform', | |
| if (wasm) '--wasm', | |
| if (tag != null) '--tags=$tag', | |
| ], workingDir: package.directory); | |
| } | |
| /// Runs the Dart tests for a non-Flutter package, returning the process exit code. | |
| Future<int> _runDartTests( | |
| RepositoryPackage package, { | |
| String? platform, | |
| bool wasm = false, | |
| String? tag, | |
| }) async { | |
| // Unlike `flutter test`, `dart run test` does not automatically get | |
| // packages | |
| if (!await runPubGet(package, processRunner, super.platform)) { | |
| printError('Unable to fetch dependencies.'); | |
| return 1; | |
| } | |
| final String experiment = getStringArg(kEnableExperiment); | |
| return processRunner.runAndStream('dart', <String>[ | |
| 'run', | |
| if (experiment.isNotEmpty) '--enable-experiment=$experiment', | |
| 'test', | |
| if (platform != null) '--platform=$platform', | |
| if (wasm) '--compiler=dart2wasm', | |
| if (tag != null) '--tags=$tag', | |
| ], workingDir: package.directory); | |
| } | |
| /// Parses the `--package-tags` argument and populates the `_packageTags` | |
| /// map with the resulting package names to tags. | |
| void _initializePackageTags() { | |
| final packageTags = <String, String>{}; | |
| final String? arg = getNullableStringArg(_packageTagsFlag); | |
| if (arg == null || arg.isEmpty) { | |
| _packageTags = packageTags; | |
| return; | |
| } | |
| final File file = packagesDir.fileSystem.file(arg); | |
| if (!file.existsSync()) { | |
| printError('The package tags file "$arg" does not exist.'); | |
| throw ToolExit(exitInvalidArguments); | |
| } | |
| final Object? yaml = loadYaml(file.readAsStringSync()); | |
| // Treat an empty or comment-only file as an empty map (no package tags). | |
| if (yaml == null) { | |
| _packageTags = packageTags; | |
| return; | |
| } | |
| if (yaml is YamlMap) { | |
| for (final MapEntry<dynamic, dynamic> entry in yaml.entries) { | |
| final pkg = entry.key.toString(); | |
| final dynamic val = entry.value; | |
| if (val != null) { | |
| packageTags[pkg] = val.toString(); | |
| } | |
| } | |
| } else { | |
| printError('The package tags file "$arg" is not a valid YAML map.'); | |
| throw ToolExit(exitInvalidArguments); | |
| } | |
| _packageTags = packageTags; | |
| return; | |
| } | |
| /// Returns the tag to apply for [package] based on `--package-tags`, or null | |
| /// if none. | |
| /// | |
| /// This checks the package's [RepositoryPackage.displayName], and falls back | |
| /// to checking the [RepositoryPackage.displayName] of any enclosing packages | |
| /// up the directory tree. This allows tagging an entire plugin (including its | |
| /// nested examples) by simply tagging the enclosing package. | |
| String? _getTagForPackage(RepositoryPackage package) { | |
| final Map<String, String>? packageTags = _packageTags; | |
| if (packageTags != null && packageTags.isNotEmpty) { | |
| final candidates = <String>{package.displayName}; | |
| RepositoryPackage? enclosing = package.getEnclosingPackage(); | |
| while (enclosing != null) { | |
| candidates.add(enclosing.displayName); | |
| enclosing = enclosing.getEnclosingPackage(); | |
| } | |
| for (final candidate in candidates) { | |
| if (packageTags.containsKey(candidate)) { | |
| return packageTags[candidate]; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /// Returns the required test environment, or null if none is specified. | |
| /// | |
| /// Throws if the target is not recognized. | |
| _TestPlatform? _testOnTarget(RepositoryPackage package) { | |
| final File testConfig = package.directory.childFile('dart_test.yaml'); | |
| if (!testConfig.existsSync()) { | |
| return null; | |
| } | |
| final Object? root = loadYaml(testConfig.readAsStringSync()); | |
| if (root is! YamlMap) { | |
| return null; | |
| } | |
| final Object? targetFilter = root['test_on']; | |
| if (targetFilter == null || targetFilter is! String) { | |
| return null; | |
| } | |
| // test_on lines can be very complex, but in pratice the packages in | |
| // this repo currently only need the ability to require vm or not, so a | |
| // simple one-target directive is all that's supported currently. | |
| // Making it deliberately strict avoids the possibility of accidentally | |
| // skipping vm coverage due to a complex expression that's not handled | |
| // correctly. | |
| switch (targetFilter) { | |
| case 'vm': | |
| return _TestPlatform.vm; | |
| case 'browser': | |
| return _TestPlatform.browser; | |
| default: | |
| printError( | |
| 'Unknown "test_on" value: "$targetFilter"\n' | |
| "If this value needs to be supported for this package's tests, " | |
| 'please update the repository tooling to support more test_on ' | |
| 'modes.', | |
| ); | |
| throw ToolExit(_exitUnknownTestPlatform); | |
| } | |
| } | |
| } |