blob: df0ee71400b3b6e6a670379d4ec468f08356298f [file] [log] [blame]
// 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<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
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 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';
}
bool passed;
if (package.requiresFlutter()) {
passed = await _runFlutterTests(package, platform: platform);
} 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<bool> _runFlutterTests(RepositoryPackage package,
{String? platform}) async {
final String experiment = getStringArg(kEnableExperiment);
final int exitCode = await 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',
],
workingDir: package.directory,
);
return exitCode == 0;
}
/// Runs the Dart tests for a non-Flutter package, returning true on success.
Future<bool> _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',
<String>[
'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';
});
}
}