| // 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)); |
| } |