// Copyright 2013 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 'package:file/file.dart'; import 'common/core.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'; /// 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, }) { 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).', ); } static const String _platformFlag = 'platform'; @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 get aliases => ['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 Future 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 bool 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 (_requiresVM(package)) { // 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."); } if (_requiresNonVM(package)) { // 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'; } // All the web tests assume the html renderer currently. final String? webRenderer = (platform == 'chrome') ? 'html' : null; bool passed; if (package.requiresFlutter()) { passed = await _runFlutterTests(package, platform: platform, webRenderer: webRenderer); } else { passed = await _runDartTests(package, platform: platform); } return passed ? PackageResult.success() : PackageResult.fail(); } /// Runs the Dart tests for a Flutter package, returning true on success. Future _runFlutterTests(RepositoryPackage package, {String? platform, String? webRenderer}) async { final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( flutterCommand, [ '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 (webRenderer != null) '--web-renderer=$webRenderer', ], workingDir: package.directory, ); return exitCode == 0; } /// Runs the Dart tests for a non-Flutter package, returning true on success. Future _runDartTests(RepositoryPackage package, {String? platform}) 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 false; } final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( 'dart', [ 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', if (platform != null) '--platform=$platform', ], workingDir: package.directory, ); return exitCode == 0; } bool _requiresVM(RepositoryPackage package) { final File testConfig = package.directory.childFile('dart_test.yaml'); if (!testConfig.existsSync()) { return false; } // 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 that // simple directive is all that is currently supported. final RegExp vmRequrimentRegex = RegExp(r'^test_on:\s*vm$'); return testConfig .readAsLinesSync() .any((String line) => vmRequrimentRegex.hasMatch(line)); } bool _requiresNonVM(RepositoryPackage package) { final File testConfig = package.directory.childFile('dart_test.yaml'); if (!testConfig.existsSync()) { return false; } // 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. final RegExp testOnRegex = RegExp(r'^test_on:\s*([a-z])*\s*$'); return testConfig.readAsLinesSync().any((String line) { final RegExpMatch? match = testOnRegex.firstMatch(line); return match != null && match.group(1) != 'vm'; }); } }