blob: d4d8ab8e399f8530aeff6e1aed11e0f1d592b25d [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. 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:io';
import 'dart:math';
import 'package:args/args.dart';
import 'package:boolean_selector/boolean_selector.dart';
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
import '../../util/io.dart';
import '../configuration.dart';
import '../runtime_selection.dart';
import 'reporters.dart';
import 'values.dart';
/// The parser used to parse the command-line arguments.
final ArgParser _parser = (() {
var parser = ArgParser(allowTrailingOptions: true);
var allRuntimes = Runtime.builtIn.toList()..remove(Runtime.vm);
if (!Platform.isMacOS) allRuntimes.remove(Runtime.safari);
if (!Platform.isWindows) allRuntimes.remove(Runtime.internetExplorer);
parser.addFlag('help',
abbr: 'h', negatable: false, help: 'Shows this usage information.');
parser.addFlag('version',
negatable: false, help: "Shows the package's version.");
// Note that defaultsTo declarations here are only for documentation purposes.
// We pass null instead of the default so that it merges properly with the
// config file.
parser.addSeparator('======== Selecting Tests');
parser.addMultiOption('name',
abbr: 'n',
help: 'A substring of the name of the test to run.\n'
'Regular expression syntax is supported.\n'
'If passed multiple times, tests must match all substrings.',
splitCommas: false);
parser.addMultiOption('plain-name',
abbr: 'N',
help: 'A plain-text substring of the name of the test to run.\n'
'If passed multiple times, tests must match all substrings.',
splitCommas: false);
parser.addMultiOption('tags',
abbr: 't',
help: 'Run only tests with all of the specified tags.\n'
'Supports boolean selector syntax.');
parser.addMultiOption('tag', hide: true);
parser.addMultiOption('exclude-tags',
abbr: 'x',
help: "Don't run tests with any of the specified tags.\n"
'Supports boolean selector syntax.');
parser.addMultiOption('exclude-tag', hide: true);
parser.addFlag('run-skipped',
help: 'Run skipped tests instead of skipping them.');
parser.addSeparator('======== Running Tests');
// The UI term "platform" corresponds with the implementation term "runtime".
// The [Runtime] class used to be called [TestPlatform], but it was changed to
// avoid conflicting with [SuitePlatform]. We decided not to also change the
// UI to avoid a painful migration.
parser.addMultiOption('platform',
abbr: 'p',
help: 'The platform(s) on which to run the tests.\n'
'[vm (default), '
'${allRuntimes.map((runtime) => runtime.identifier).join(", ")}]');
parser.addMultiOption('preset',
abbr: 'P', help: 'The configuration preset(s) to use.');
parser.addOption('concurrency',
abbr: 'j',
help: 'The number of concurrent test suites run.',
defaultsTo: defaultConcurrency.toString(),
valueHelp: 'threads');
parser.addOption('total-shards',
help: 'The total number of invocations of the test runner being run.');
parser.addOption('shard-index',
help: 'The index of this test runner invocation (of --total-shards).');
parser.addOption('pub-serve',
help: 'The port of a pub serve instance serving "test/".',
valueHelp: 'port');
parser.addOption('timeout',
help: 'The default test timeout. For example: 15s, 2x, none',
defaultsTo: '30s');
parser.addFlag('pause-after-load',
help: 'Pauses for debugging before any tests execute.\n'
'Implies --concurrency=1, --debug, and --timeout=none.\n'
'Currently only supported for browser tests.',
negatable: false);
parser.addFlag('debug',
help: 'Runs the VM and Chrome tests in debug mode.', negatable: false);
parser.addOption('coverage',
help: 'Gathers coverage and outputs it to the specified directory.\n'
'Implies --debug.',
valueHelp: 'directory');
parser.addFlag('chain-stack-traces',
help: 'Chained stack traces to provide greater exception details\n'
'especially for asynchronous code. It may be useful to disable\n'
'to provide improved test performance but at the cost of\n'
'debuggability.',
defaultsTo: true);
parser.addFlag('no-retry',
help: "Don't re-run tests that have retry set.",
defaultsTo: false,
negatable: false);
parser.addOption('test-randomize-ordering-seed',
help: 'If positive, use this as a seed to randomize the execution\n'
'of test cases (must be a 32bit unsigned integer).\n'
'If "random", pick a random seed to use.\n'
'If 0 or not set, do not randomize test case execution order.\n');
var reporterDescriptions = <String, String>{};
for (var reporter in allReporters.keys) {
reporterDescriptions[reporter] = allReporters[reporter].description;
}
parser.addSeparator('======== Output');
parser.addOption('reporter',
abbr: 'r',
help: 'The runner used to print test results.',
defaultsTo: defaultReporter,
allowed: reporterDescriptions.keys.toList(),
allowedHelp: reporterDescriptions);
parser.addOption('file-reporter',
help: 'The reporter used to write test results to a file.\n'
'Should be in the form <reporter>:<filepath>, '
'e.g. "json:reports/tests.json"');
parser.addFlag('verbose-trace',
negatable: false,
help: 'Whether to emit stack traces with core library frames.');
parser.addFlag('js-trace',
negatable: false,
help: 'Whether to emit raw JavaScript stack traces for browser tests.');
parser.addFlag('color',
help: 'Whether to use terminal colors.\n(auto-detected by default)');
/// The following options are used only by the internal Google test runner.
/// They're hidden and not supported as stable API surface outside Google.
parser.addOption('configuration',
help: 'The path to the configuration file.', hide: true);
parser.addOption('dart2js-path',
help: 'The path to the dart2js executable.', hide: true);
parser.addMultiOption('dart2js-args',
help: 'Extra arguments to pass to dart2js.', hide: true);
// If we're running test/dir/my_test.dart, we'll look for
// test/dir/my_test.dart.html in the precompiled directory.
parser.addOption('precompiled',
help: 'The path to a mirror of the package directory containing HTML '
'that points to precompiled JS.',
hide: true);
return parser;
})();
/// The usage string for the command-line arguments.
String get usage => _parser.usage;
/// Parses the configuration from [args].
///
/// Throws a [FormatException] if [args] are invalid.
Configuration parse(List<String> args) => _Parser(args).parse();
/// A class for parsing an argument list.
///
/// This is used to provide access to the arg results across helper methods.
class _Parser {
/// The parsed options.
final ArgResults _options;
_Parser(List<String> args) : _options = _parser.parse(args);
/// Returns the parsed configuration.
Configuration parse() {
var patterns = (_options['name'] as List<String>)
.map<Pattern>(
(value) => _wrapFormatException('name', () => RegExp(value)))
.toList()
..addAll(_options['plain-name'] as List<String>);
var includeTagSet = Set.from(_options['tags'] as Iterable ?? [])
..addAll(_options['tag'] as Iterable ?? []);
var includeTags = includeTagSet.fold(BooleanSelector.all,
(BooleanSelector selector, tag) {
var tagSelector = BooleanSelector.parse(tag as String);
return selector.intersection(tagSelector);
});
var excludeTagSet = Set.from(_options['exclude-tags'] as Iterable ?? [])
..addAll(_options['exclude-tag'] as Iterable ?? []);
var excludeTags = excludeTagSet.fold(BooleanSelector.none,
(BooleanSelector selector, tag) {
var tagSelector = BooleanSelector.parse(tag as String);
return selector.union(tagSelector);
});
var shardIndex = _parseOption('shard-index', int.parse);
var totalShards = _parseOption('total-shards', int.parse);
if ((shardIndex == null) != (totalShards == null)) {
throw FormatException(
'--shard-index and --total-shards may only be passed together.');
} else if (shardIndex != null) {
if (shardIndex < 0) {
throw FormatException('--shard-index may not be negative.');
} else if (shardIndex >= totalShards) {
throw FormatException(
'--shard-index must be less than --total-shards.');
}
}
var testRandomizeOrderingSeed =
_parseOption('test-randomize-ordering-seed', (value) {
var seed = value == 'random'
? Random().nextInt(4294967295)
: int.parse(value).toUnsigned(32);
if (seed != null) {
print('Shuffling test order with --test-randomize-ordering-seed=$seed');
}
return seed;
});
var color = _ifParsed<bool>('color') ?? canUseSpecialChars;
var platform = _ifParsed<List<String>>('platform')
?.map((runtime) => RuntimeSelection(runtime))
?.toList();
if (platform
?.any((runtime) => runtime.name == Runtime.phantomJS.identifier) ??
false) {
var yellow = color ? '\u001b[33m' : '';
var noColor = color ? '\u001b[0m' : '';
print('${yellow}Warning:$noColor '
'PhatomJS is deprecated and will be removed in version ^2.0.0');
}
return Configuration(
help: _ifParsed('help'),
version: _ifParsed('version'),
verboseTrace: _ifParsed('verbose-trace'),
chainStackTraces: _ifParsed('chain-stack-traces'),
jsTrace: _ifParsed('js-trace'),
pauseAfterLoad: _ifParsed('pause-after-load'),
debug: _ifParsed('debug'),
color: color,
configurationPath: _ifParsed('configuration'),
dart2jsPath: _ifParsed('dart2js-path'),
dart2jsArgs: _ifParsed('dart2js-args'),
precompiledPath: _ifParsed('precompiled'),
reporter: _ifParsed('reporter'),
fileReporters: _parseFileReporterOption(),
coverage: _ifParsed('coverage'),
pubServePort: _parseOption('pub-serve', int.parse),
concurrency: _parseOption('concurrency', int.parse),
shardIndex: shardIndex,
totalShards: totalShards,
timeout: _parseOption('timeout', (value) => Timeout.parse(value)),
patterns: patterns,
runtimes: platform,
runSkipped: _ifParsed('run-skipped'),
chosenPresets: _ifParsed('preset'),
paths: _options.rest.isEmpty ? null : _options.rest,
includeTags: includeTags,
excludeTags: excludeTags,
noRetry: _ifParsed('no-retry'),
testRandomizeOrderingSeed: testRandomizeOrderingSeed);
}
/// Returns the parsed option for [name], or `null` if none was parsed.
///
/// If the user hasn't explicitly chosen a value, we want to pass null values
/// to [new Configuration] so that it considers those fields unset when
/// merging with configuration from the config file.
T _ifParsed<T>(String name) =>
_options.wasParsed(name) ? _options[name] as T : null;
/// Runs [parse] on the value of the option [name], and wraps any
/// [FormatException] it throws with additional information.
T _parseOption<T>(String name, T Function(String) parse) {
if (!_options.wasParsed(name)) return null;
var value = _options[name];
if (value == null) return null;
return _wrapFormatException(name, () => parse(value as String));
}
Map<String, String> _parseFileReporterOption() =>
_parseOption('file-reporter', (value) {
if (!value.contains(':')) {
throw FormatException(
'option must be in the form <reporter>:<filepath>, e.g. '
'"json:reports/tests.json"');
}
final sep = value.indexOf(':');
final reporter = value.substring(0, sep);
if (!allReporters.containsKey(reporter)) {
throw FormatException('"$reporter" is not a supported reporter');
}
return {reporter: value.substring(sep + 1)};
});
/// Runs [parse], and wraps any [FormatException] it throws with additional
/// information.
T _wrapFormatException<T>(String name, T Function() parse) {
try {
return parse();
} on FormatException catch (error) {
throw FormatException('Couldn\'t parse --$name "${_options[name]}": '
'${error.message}');
}
}
}