blob: b8e2ed5eac9aec66797e78620254cd965d6a9374 [file] [log] [blame]
// Copyright (c) 2015, 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 'package:boolean_selector/boolean_selector.dart';
import 'package:collection/collection.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as p;
import '../backend/metadata.dart';
import '../backend/platform_selector.dart';
import '../backend/test_platform.dart';
import '../frontend/timeout.dart';
import '../util/io.dart';
import '../utils.dart';
import 'configuration/args.dart' as args;
import 'configuration/load.dart';
import 'configuration/values.dart';
/// A class that encapsulates the command-line configuration of the test runner.
class Configuration {
/// An empty configuration with only default values.
///
/// Using this is slightly more efficient than manually constructing a new
/// configuration with no arguments.
static final empty = new Configuration._();
/// The usage string for the command-line arguments.
static String get usage => args.usage;
/// Whether `--help` was passed.
bool get help => _help ?? false;
final bool _help;
/// Whether `--version` was passed.
bool get version => _version ?? false;
final bool _version;
/// Whether stack traces should be presented as-is or folded to remove
/// irrelevant packages.
bool get verboseTrace => _verboseTrace ?? false;
final bool _verboseTrace;
/// Whether JavaScript stack traces should be left as-is or converted to
/// Dart-like traces.
bool get jsTrace => _jsTrace ?? false;
final bool _jsTrace;
/// Whether tests should be skipped.
bool get skip => _skip ?? false;
final bool _skip;
/// The reason tests or suites should be skipped, if given.
final String skipReason;
/// The selector indicating which platforms the tests support.
///
/// When [merge]d, this is intersected with the other configuration's
/// supported platforms.
final PlatformSelector testOn;
/// Whether to pause for debugging after loading each test suite.
bool get pauseAfterLoad => _pauseAfterLoad ?? false;
final bool _pauseAfterLoad;
/// The package root for resolving "package:" URLs.
String get packageRoot => _packageRoot ?? p.join(p.current, 'packages');
final String _packageRoot;
/// The name of the reporter to use to display results.
String get reporter => _reporter ?? defaultReporter;
final String _reporter;
/// The URL for the `pub serve` instance from which to load tests, or `null`
/// if tests should be loaded from the filesystem.
final Uri pubServeUrl;
/// The default test timeout.
///
/// When [merge]d, this combines with the other configuration's timeout using
/// [Timeout.merge].
final Timeout timeout;
/// Whether to use command-line color escapes.
bool get color => _color ?? canUseSpecialChars;
final bool _color;
/// How many tests to run concurrently.
int get concurrency =>
pauseAfterLoad ? 1 : (_concurrency ?? defaultConcurrency);
final int _concurrency;
/// The paths from which to load tests.
List<String> get paths => _paths ?? ["test"];
final List<String> _paths;
/// Whether the load paths were passed explicitly or the default was used.
bool get explicitPaths => _paths != null;
/// The glob matching the basename of tests to run.
///
/// This is used to find tests within a directory.
Glob get filename => _filename ?? defaultFilename;
final Glob _filename;
/// The pattern to match against test names to decide which to run, or `null`
/// if all tests should be run.
final Pattern pattern;
/// The set of platforms on which to run tests.
List<TestPlatform> get platforms => _platforms ?? [TestPlatform.vm];
final List<TestPlatform> _platforms;
/// The set of presets to use.
///
/// Any chosen presets for the parent configuration are added to the chosen
/// preset sets for child configurations as well.
///
/// Note that the order of this set matters.
final Set<String> chosenPresets;
/// Only run tests whose tags match this selector.
///
/// When [merge]d, this is intersected with the other configuration's included
/// tags.
final BooleanSelector includeTags;
/// Do not run tests whose tags match this selector.
///
/// When [merge]d, this is unioned with the other configuration's
/// excluded tags.
final BooleanSelector excludeTags;
/// Configuration for particular tags.
///
/// The keys are tag selectors, and the values are configurations for tests
/// whose tags match those selectors. The configuration should only contain
/// test-level configuration fields, but that isn't enforced.
final Map<BooleanSelector, Configuration> tags;
/// Tags that are added to the tests.
///
/// This is usually only used for scoped configuration.
final Set<String> addTags;
/// The global test metadata derived from this configuration.
Metadata get metadata => new Metadata(
timeout: timeout,
verboseTrace: verboseTrace,
skip: skip,
skipReason: skipReason,
testOn: testOn,
tags: addTags,
forTag: mapMap(tags, value: (_, config) => config.metadata),
onPlatform: mapMap(onPlatform, value: (_, config) => config.metadata));
/// The set of tags that have been declared in any way in this configuration.
Set<String> get knownTags {
if (_knownTags != null) return _knownTags;
var known = includeTags.variables.toSet()
..addAll(excludeTags.variables)
..addAll(addTags);
for (var selector in tags.keys) {
known.addAll(selector.variables);
}
for (var configuration in _children) {
known.addAll(configuration.knownTags);
}
_knownTags = new UnmodifiableSetView(known);
return _knownTags;
}
Set<String> _knownTags;
/// Configuration for particular platforms.
///
/// The keys are platform selectors, and the values are configurations for
/// those platforms. These configuration should only contain test-level
/// configuration fields, but that isn't enforced.
final Map<PlatformSelector, Configuration> onPlatform;
/// Configuration presets.
///
/// These are configurations that can be explicitly selected by the user via
/// the command line. Preset configuration takes precedence over the base
/// configuration.
///
/// This is guaranteed not to have any keys that match [chosenPresets]; those
/// are resolved when the configuration is constructed.
final Map<String, Configuration> presets;
/// All preset names that are known to be valid.
///
/// This includes presets that have already been resolved.
Set<String> get knownPresets {
if (_knownPresets != null) return _knownPresets;
var known = presets.keys.toSet();
for (var configuration in _children) {
known.addAll(configuration.knownPresets);
}
_knownPresets = new UnmodifiableSetView(known);
return _knownPresets;
}
Set<String> _knownPresets;
/// All child configurations of [this] that may be selected under various
/// circumstances.
Iterable<Configuration> get _children sync* {
yield* tags.values;
yield* onPlatform.values;
yield* presets.values;
}
/// Parses the configuration from [args].
///
/// Throws a [FormatException] if [args] are invalid.
factory Configuration.parse(List<String> arguments) => args.parse(arguments);
/// Loads the configuration from [path].
///
/// Throws an [IOException] if [path] does not exist or cannot be read. Throws
/// a [FormatException] if its contents are invalid.
factory Configuration.load(String path) => load(path);
factory Configuration({
bool help,
bool version,
bool verboseTrace,
bool jsTrace,
bool skip,
String skipReason,
PlatformSelector testOn,
bool pauseAfterLoad,
bool color,
String packageRoot,
String reporter,
int pubServePort,
int concurrency,
Timeout timeout,
Pattern pattern,
Iterable<TestPlatform> platforms,
Iterable<String> paths,
Glob filename,
Iterable<String> chosenPresets,
BooleanSelector includeTags,
BooleanSelector excludeTags,
Iterable addTags,
Map<BooleanSelector, Configuration> tags,
Map<PlatformSelector, Configuration> onPlatform,
Map<String, Configuration> presets}) {
_unresolved() => new Configuration._(
help: help,
version: version,
verboseTrace: verboseTrace,
jsTrace: jsTrace,
skip: skip,
skipReason: skipReason,
testOn: testOn,
pauseAfterLoad: pauseAfterLoad,
color: color,
packageRoot: packageRoot,
reporter: reporter,
pubServePort: pubServePort,
concurrency: concurrency,
timeout: timeout,
pattern: pattern,
platforms: platforms,
paths: paths,
filename: filename,
chosenPresets: chosenPresets,
includeTags: includeTags,
excludeTags: excludeTags,
addTags: addTags,
// Make sure we pass [chosenPresets] to the child configurations as
// well. This ensures that
tags: _withChosenPresets(tags, chosenPresets),
onPlatform: _withChosenPresets(onPlatform, chosenPresets),
presets: _withChosenPresets(presets, chosenPresets));
if (chosenPresets == null) return _unresolved();
chosenPresets = new Set.from(chosenPresets);
if (presets == null) return _unresolved();
presets = new Map.from(presets);
var knownPresets = presets.keys.toSet();
var merged = chosenPresets.fold(Configuration.empty, (merged, preset) {
if (!presets.containsKey(preset)) return merged;
return merged.merge(presets.remove(preset));
});
var result = merged == Configuration.empty
? _unresolved()
: _unresolved().merge(merged);
// Make sure the configuration knows about presets that were selected and
// thus removed from [presets].
result._knownPresets = result.knownPresets.union(knownPresets);
return result;
}
static Map<Object, Configuration> _withChosenPresets(
Map<Object, Configuration> map, Set<String> chosenPresets) {
if (map == null || chosenPresets == null) return map;
return mapMap(map, value: (_, config) => config.change(
chosenPresets: config.chosenPresets.union(chosenPresets)));
}
/// Creates new Configuration.
///
/// Unlike [new Configuration], this assumes [presets] is already resolved.
Configuration._({
bool help,
bool version,
bool verboseTrace,
bool jsTrace,
bool skip,
this.skipReason,
PlatformSelector testOn,
bool pauseAfterLoad,
bool color,
String packageRoot,
String reporter,
int pubServePort,
int concurrency,
Timeout timeout,
this.pattern,
Iterable<TestPlatform> platforms,
Iterable<String> paths,
Glob filename,
Iterable<String> chosenPresets,
BooleanSelector includeTags,
BooleanSelector excludeTags,
Iterable addTags,
Map<BooleanSelector, Configuration> tags,
Map<PlatformSelector, Configuration> onPlatform,
Map<String, Configuration> presets})
: _help = help,
_version = version,
_verboseTrace = verboseTrace,
_jsTrace = jsTrace,
_skip = skip,
testOn = testOn ?? PlatformSelector.all,
_pauseAfterLoad = pauseAfterLoad,
_color = color,
_packageRoot = packageRoot,
_reporter = reporter,
pubServeUrl = pubServePort == null
? null
: Uri.parse("http://localhost:$pubServePort"),
_concurrency = concurrency,
timeout = (pauseAfterLoad ?? false)
? Timeout.none
: (timeout == null ? new Timeout.factor(1) : timeout),
_platforms = _list(platforms),
_paths = _list(paths),
_filename = filename,
chosenPresets = new Set.from(chosenPresets ?? []),
includeTags = includeTags ?? BooleanSelector.all,
excludeTags = excludeTags ?? BooleanSelector.none,
addTags = new UnmodifiableSetView(addTags?.toSet() ?? new Set()),
tags = _map(tags),
onPlatform = _map(onPlatform),
presets = _map(presets) {
if (_filename != null && _filename.context.style != p.style) {
throw new ArgumentError(
"filename's context must match the current operating system, was "
"${_filename.context.style}.");
}
}
/// Returns a [input] as an unmodifiable list or `null`.
///
/// If [input] is `null` or empty, this returns `null`. Otherwise, it returns
/// `input.toList()`.
static List _list(Iterable input) {
if (input == null) return null;
input = new List.unmodifiable(input);
if (input.isEmpty) return null;
return input;
}
/// Returns an modifiable copy of [input] or an empty unmodifiable map.
static Map _map(Map input) {
if (input == null) return const {};
return new Map.unmodifiable(input);
}
/// Merges this with [other].
///
/// For most fields, if both configurations have values set, [other]'s value
/// takes precedence. However, certain fields are merged together instead.
/// This is indicated in those fields' documentation.
Configuration merge(Configuration other) {
if (this == Configuration.empty) return other;
if (other == Configuration.empty) return this;
var result = new Configuration(
help: other._help ?? _help,
version: other._version ?? _version,
verboseTrace: other._verboseTrace ?? _verboseTrace,
jsTrace: other._jsTrace ?? _jsTrace,
skip: other._skip ?? _skip,
skipReason: other.skipReason ?? skipReason,
testOn: testOn.intersection(other.testOn),
pauseAfterLoad: other._pauseAfterLoad ?? _pauseAfterLoad,
color: other._color ?? _color,
packageRoot: other._packageRoot ?? _packageRoot,
reporter: other._reporter ?? _reporter,
pubServePort: (other.pubServeUrl ?? pubServeUrl)?.port,
concurrency: other._concurrency ?? _concurrency,
timeout: timeout.merge(other.timeout),
pattern: other.pattern ?? pattern,
platforms: other._platforms ?? _platforms,
paths: other._paths ?? _paths,
filename: other._filename ?? _filename,
chosenPresets: chosenPresets.union(other.chosenPresets),
includeTags: includeTags.intersection(other.includeTags),
excludeTags: excludeTags.union(other.excludeTags),
addTags: other.addTags.union(addTags),
tags: _mergeConfigMaps(tags, other.tags),
onPlatform: _mergeConfigMaps(onPlatform, other.onPlatform),
presets: _mergeConfigMaps(presets, other.presets));
// Make sure the merged config preserves any presets that were chosen and
// discarded.
result._knownPresets = knownPresets.union(other.knownPresets);
return result;
}
/// Returns a copy of this configuration with the given fields updated.
///
/// Note that unlike [merge], this has no merging behavior—the old value is
/// always replaced by the new one.
Configuration change({
bool help,
bool version,
bool verboseTrace,
bool jsTrace,
bool skip,
String skipReason,
PlatformSelector testOn,
bool pauseAfterLoad,
bool color,
String packageRoot,
String reporter,
int pubServePort,
int concurrency,
Timeout timeout,
Pattern pattern,
Iterable<TestPlatform> platforms,
Iterable<String> paths,
Glob filename,
Iterable<String> chosenPresets,
BooleanSelector includeTags,
BooleanSelector excludeTags,
Iterable addTags,
Map<BooleanSelector, Configuration> tags,
Map<PlatformSelector, Configuration> onPlatform,
Map<String, Configuration> presets}) {
return new Configuration(
help: help ?? _help,
version: version ?? _version,
verboseTrace: verboseTrace ?? _verboseTrace,
jsTrace: jsTrace ?? _jsTrace,
skip: skip ?? _skip,
skipReason: skipReason ?? this.skipReason,
testOn: testOn ?? this.testOn,
pauseAfterLoad: pauseAfterLoad ?? _pauseAfterLoad,
color: color ?? _color,
packageRoot: packageRoot ?? _packageRoot,
reporter: reporter ?? _reporter,
pubServePort: pubServePort ?? pubServeUrl?.port,
concurrency: concurrency ?? _concurrency,
timeout: timeout ?? this.timeout,
pattern: pattern ?? this.pattern,
platforms: platforms ?? _platforms,
paths: paths ?? _paths,
filename: filename ?? _filename,
chosenPresets: chosenPresets ?? this.chosenPresets,
includeTags: includeTags ?? this.includeTags,
excludeTags: excludeTags ?? this.excludeTags,
addTags: addTags ?? this.addTags,
tags: tags ?? this.tags,
onPlatform: onPlatform ?? this.onPlatform,
presets: presets ?? this.presets);
}
/// Merges two maps whose values are [Configuration]s.
///
/// Any overlapping keys in the maps have their configurations merged in the
/// returned map.
Map<Object, Configuration> _mergeConfigMaps(Map<Object, Configuration> map1,
Map<Object, Configuration> map2) =>
mergeMaps(map1, map2,
value: (config1, config2) => config1.merge(config2));
}