| // 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 'package:boolean_selector/boolean_selector.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:source_span/source_span.dart'; | |
| import 'package:test_api/scaffolding.dart' show Timeout; | |
| import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports | |
| import 'compiler_selection.dart'; | |
| import 'runtime_selection.dart'; | |
| /// A filter on tests cases to run within a test suite. | |
| /// | |
| /// The tests that match a test selection should match all available criteria. A | |
| /// suite run may include multiple test selections, and a test should run if it | |
| /// matches any test selection. | |
| /// | |
| /// An empty [TestSelection()] will run all tests in the suite. | |
| final class TestSelection { | |
| /// The patterns to check against test case names. | |
| /// | |
| /// Only run tests which match all the patterns. | |
| final Set<Pattern> testPatterns; | |
| /// Only run tests that originate from this line in the test suite. | |
| final int? line; | |
| /// Only run tests that originate from this column in the test suite. | |
| final int? col; | |
| const TestSelection({this.testPatterns = const {}, this.line, this.col}); | |
| } | |
| /// Suite-level configuration. | |
| /// | |
| /// This tracks configuration that can differ from suite to suite. | |
| final class SuiteConfiguration { | |
| /// Empty configuration with only default values. | |
| /// | |
| /// Using this is slightly more efficient than manually constructing a new | |
| /// configuration with no arguments. | |
| static final empty = SuiteConfiguration._( | |
| allowDuplicateTestNames: null, | |
| allowTestRandomization: null, | |
| jsTrace: null, | |
| runSkipped: null, | |
| dart2jsArgs: null, | |
| testSelections: const {}, | |
| precompiledPath: null, | |
| runtimes: null, | |
| compilerSelections: null, | |
| tags: null, | |
| onPlatform: null, | |
| metadata: null, | |
| ignoreTimeouts: null, | |
| suiteLoadTimeout: null, | |
| ); | |
| /// Whether or not duplicate test (or group) names are allowed within the same | |
| /// test suite. | |
| // | |
| // TODO: Change the default https://github.com/dart-lang/test/issues/1571 | |
| bool get allowDuplicateTestNames => _allowDuplicateTestNames ?? true; | |
| final bool? _allowDuplicateTestNames; | |
| /// Whether test randomization should be allowed for this test. | |
| bool get allowTestRandomization => _allowTestRandomization ?? true; | |
| final bool? _allowTestRandomization; | |
| /// Whether JavaScript stack traces should be left as-is or converted to | |
| /// Dart-like traces. | |
| bool get jsTrace => _jsTrace ?? false; | |
| final bool? _jsTrace; | |
| /// Whether skipped tests should be run. | |
| bool get runSkipped => _runSkipped ?? false; | |
| final bool? _runSkipped; | |
| /// The path to a mirror of this package containing HTML that points to | |
| /// precompiled JS. | |
| /// | |
| /// This is used by the internal Google test runner so that test compilation | |
| /// can more effectively make use of Google's build tools. | |
| final String? precompiledPath; | |
| /// Additional arguments to pass to dart2js. | |
| /// | |
| /// Note that this if multiple suites run the same JavaScript on different | |
| /// runtimes, and they have different [dart2jsArgs], only one (undefined) | |
| /// suite's arguments will be used. | |
| final List<String> dart2jsArgs; | |
| /// The selections for which tests to run in a suite. | |
| /// | |
| /// If empty, no tests have been selected for this suite. | |
| /// Tests must be selected once for a suite before the suite can run. | |
| /// When merging suite configurations, only one, or neither, should have tests | |
| /// already selected. | |
| /// | |
| /// A test should run within a suite if it matches any selection in | |
| /// [testSelections]. | |
| final Set<TestSelection> testSelections; | |
| /// The set of compiler selections for running tests. | |
| final List<CompilerSelection>? compilerSelections; | |
| /// The set of runtimes on which to run tests. | |
| List<String> get runtimes => _runtimes == null | |
| ? const ['vm'] | |
| : List.unmodifiable(_runtimes.map((runtime) => runtime.name)); | |
| final List<RuntimeSelection>? _runtimes; | |
| /// Configuration for particular tags. | |
| /// | |
| /// The keys are tag selectors, and the values are configurations for tests | |
| /// whose tags match those selectors. | |
| final Map<BooleanSelector, SuiteConfiguration> tags; | |
| /// 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, SuiteConfiguration> onPlatform; | |
| /// The global test metadata derived from this configuration. | |
| Metadata get metadata { | |
| if (tags.isEmpty && onPlatform.isEmpty) return _metadata; | |
| return _metadata.change( | |
| forTag: tags.map((key, config) => MapEntry(key, config.metadata)), | |
| onPlatform: onPlatform.map( | |
| (key, config) => MapEntry(key, config.metadata), | |
| ), | |
| ); | |
| } | |
| final Metadata _metadata; | |
| /// The set of tags that have been declared in any way in this configuration. | |
| late final Set<String> knownTags = UnmodifiableSetView({ | |
| ..._metadata.tags, | |
| for (var selector in tags.keys) ...selector.variables, | |
| for (var configuration in tags.values) ...configuration.knownTags, | |
| for (var configuration in onPlatform.values) ...configuration.knownTags, | |
| }); | |
| /// Whether or not timeouts should be ignored. | |
| final bool? _ignoreTimeouts; | |
| bool get ignoreTimeouts => _ignoreTimeouts ?? false; | |
| /// The timeout for loading a test suite. | |
| final Timeout? _suiteLoadTimeout; | |
| Timeout get suiteLoadTimeout => _suiteLoadTimeout ?? Timeout.none; | |
| factory SuiteConfiguration({ | |
| required bool? allowDuplicateTestNames, | |
| required bool? allowTestRandomization, | |
| required bool? jsTrace, | |
| required bool? runSkipped, | |
| required Iterable<String>? dart2jsArgs, | |
| required String? precompiledPath, | |
| required Iterable<CompilerSelection>? compilerSelections, | |
| required Iterable<RuntimeSelection>? runtimes, | |
| required Map<BooleanSelector, SuiteConfiguration>? tags, | |
| required Map<PlatformSelector, SuiteConfiguration>? onPlatform, | |
| required bool? ignoreTimeouts, | |
| required Timeout? suiteLoadTimeout, | |
| // Test-level configuration | |
| required Timeout? timeout, | |
| required bool? verboseTrace, | |
| required bool? chainStackTraces, | |
| required bool? skip, | |
| required int? retry, | |
| required String? skipReason, | |
| required PlatformSelector? testOn, | |
| required Iterable<String>? addTags, | |
| }) { | |
| var config = SuiteConfiguration._( | |
| allowDuplicateTestNames: allowDuplicateTestNames, | |
| allowTestRandomization: allowTestRandomization, | |
| jsTrace: jsTrace, | |
| runSkipped: runSkipped, | |
| dart2jsArgs: dart2jsArgs, | |
| testSelections: const {}, | |
| precompiledPath: precompiledPath, | |
| compilerSelections: compilerSelections, | |
| runtimes: runtimes, | |
| tags: tags, | |
| onPlatform: onPlatform, | |
| ignoreTimeouts: ignoreTimeouts, | |
| suiteLoadTimeout: suiteLoadTimeout, | |
| metadata: Metadata( | |
| timeout: timeout, | |
| verboseTrace: verboseTrace, | |
| chainStackTraces: chainStackTraces, | |
| skip: skip, | |
| retry: retry, | |
| skipReason: skipReason, | |
| testOn: testOn, | |
| tags: addTags, | |
| ), | |
| ); | |
| return config._resolveTags(); | |
| } | |
| /// A constructor that doesn't require all of its options to be passed. | |
| /// | |
| /// This should only be used in situations where you really only want to | |
| /// configure a specific restricted set of options. | |
| factory SuiteConfiguration._unsafe({ | |
| bool? allowDuplicateTestNames, | |
| bool? allowTestRandomization, | |
| bool? jsTrace, | |
| bool? runSkipped, | |
| Iterable<String>? dart2jsArgs, | |
| String? precompiledPath, | |
| Iterable<CompilerSelection>? compilerSelections, | |
| Iterable<RuntimeSelection>? runtimes, | |
| Map<BooleanSelector, SuiteConfiguration>? tags, | |
| Map<PlatformSelector, SuiteConfiguration>? onPlatform, | |
| bool? ignoreTimeouts, | |
| Timeout? suiteLoadTimeout, | |
| // Test-level configuration | |
| Timeout? timeout, | |
| bool? verboseTrace, | |
| bool? chainStackTraces, | |
| bool? skip, | |
| int? retry, | |
| String? skipReason, | |
| PlatformSelector? testOn, | |
| Iterable<String>? addTags, | |
| }) => SuiteConfiguration( | |
| allowDuplicateTestNames: allowDuplicateTestNames, | |
| allowTestRandomization: allowTestRandomization, | |
| jsTrace: jsTrace, | |
| runSkipped: runSkipped, | |
| dart2jsArgs: dart2jsArgs, | |
| precompiledPath: precompiledPath, | |
| compilerSelections: compilerSelections, | |
| runtimes: runtimes, | |
| tags: tags, | |
| onPlatform: onPlatform, | |
| ignoreTimeouts: ignoreTimeouts, | |
| timeout: timeout, | |
| suiteLoadTimeout: suiteLoadTimeout, | |
| verboseTrace: verboseTrace, | |
| chainStackTraces: chainStackTraces, | |
| skip: skip, | |
| retry: retry, | |
| skipReason: skipReason, | |
| testOn: testOn, | |
| addTags: addTags, | |
| ); | |
| /// A specialized constructor for only configuring the runtimes. | |
| factory SuiteConfiguration.runtimes(Iterable<RuntimeSelection> runtimes) => | |
| SuiteConfiguration._unsafe(runtimes: runtimes); | |
| /// A specialized constructor for only configuring runSkipped. | |
| factory SuiteConfiguration.runSkipped(bool runSkipped) => | |
| SuiteConfiguration._unsafe(runSkipped: runSkipped); | |
| /// A specialized constructor for only configuring the timeout. | |
| factory SuiteConfiguration.timeout(Timeout timeout) => | |
| SuiteConfiguration._unsafe(timeout: timeout); | |
| /// Creates new SuiteConfiguration. | |
| /// | |
| /// Unlike [SuiteConfiguration.new], this assumes [tags] is already | |
| /// resolved. | |
| SuiteConfiguration._({ | |
| required bool? allowDuplicateTestNames, | |
| required bool? allowTestRandomization, | |
| required bool? jsTrace, | |
| required bool? runSkipped, | |
| required Iterable<String>? dart2jsArgs, | |
| required this.testSelections, | |
| required this.precompiledPath, | |
| required Iterable<CompilerSelection>? compilerSelections, | |
| required Iterable<RuntimeSelection>? runtimes, | |
| required Map<BooleanSelector, SuiteConfiguration>? tags, | |
| required Map<PlatformSelector, SuiteConfiguration>? onPlatform, | |
| required Metadata? metadata, | |
| required bool? ignoreTimeouts, | |
| required Timeout? suiteLoadTimeout, | |
| }) : _allowDuplicateTestNames = allowDuplicateTestNames, | |
| _allowTestRandomization = allowTestRandomization, | |
| _jsTrace = jsTrace, | |
| _runSkipped = runSkipped, | |
| dart2jsArgs = _list(dart2jsArgs) ?? const [], | |
| _runtimes = _list(runtimes), | |
| compilerSelections = _list(compilerSelections), | |
| tags = _map(tags), | |
| onPlatform = _map(onPlatform), | |
| _ignoreTimeouts = ignoreTimeouts, | |
| _metadata = metadata ?? Metadata.empty, | |
| _suiteLoadTimeout = suiteLoadTimeout; | |
| /// Creates a new [SuiteConfiguration] that takes its configuration from | |
| /// [metadata]. | |
| factory SuiteConfiguration.fromMetadata(Metadata metadata) => | |
| SuiteConfiguration._( | |
| tags: metadata.forTag.map( | |
| (key, child) => MapEntry(key, SuiteConfiguration.fromMetadata(child)), | |
| ), | |
| onPlatform: metadata.onPlatform.map( | |
| (key, child) => MapEntry(key, SuiteConfiguration.fromMetadata(child)), | |
| ), | |
| metadata: metadata.change(forTag: {}, onPlatform: {}), | |
| allowDuplicateTestNames: null, | |
| allowTestRandomization: null, | |
| jsTrace: null, | |
| runSkipped: null, | |
| dart2jsArgs: null, | |
| testSelections: const {}, | |
| precompiledPath: null, | |
| runtimes: null, | |
| compilerSelections: null, | |
| ignoreTimeouts: null, | |
| suiteLoadTimeout: null, | |
| ); | |
| /// Returns an unmodifiable copy of [input]. | |
| /// | |
| /// If [input] is `null` or empty, this returns `null`. | |
| static List<T>? _list<T>(Iterable<T>? input) { | |
| if (input == null) return null; | |
| var list = List<T>.unmodifiable(input); | |
| if (list.isEmpty) return null; | |
| return list; | |
| } | |
| /// Returns an unmodifiable copy of [input] or an empty unmodifiable map. | |
| static Map<K, V> _map<K, V>(Map<K, V>? input) { | |
| if (input == null || input.isEmpty) return const <Never, Never>{}; | |
| return 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. | |
| SuiteConfiguration merge(SuiteConfiguration other) { | |
| if (this == SuiteConfiguration.empty) return other; | |
| if (other == SuiteConfiguration.empty) return this; | |
| assert(testSelections.isEmpty || other.testSelections.isEmpty); | |
| var config = SuiteConfiguration._( | |
| allowDuplicateTestNames: | |
| other._allowDuplicateTestNames ?? _allowDuplicateTestNames, | |
| allowTestRandomization: | |
| other._allowTestRandomization ?? _allowTestRandomization, | |
| jsTrace: other._jsTrace ?? _jsTrace, | |
| runSkipped: other._runSkipped ?? _runSkipped, | |
| dart2jsArgs: dart2jsArgs.toList()..addAll(other.dart2jsArgs), | |
| testSelections: testSelections.isEmpty | |
| ? other.testSelections | |
| : testSelections, | |
| precompiledPath: other.precompiledPath ?? precompiledPath, | |
| compilerSelections: other.compilerSelections ?? compilerSelections, | |
| runtimes: other._runtimes ?? _runtimes, | |
| tags: _mergeConfigMaps(tags, other.tags), | |
| onPlatform: _mergeConfigMaps(onPlatform, other.onPlatform), | |
| ignoreTimeouts: other._ignoreTimeouts ?? _ignoreTimeouts, | |
| suiteLoadTimeout: other._suiteLoadTimeout ?? _suiteLoadTimeout, | |
| metadata: metadata.merge(other.metadata), | |
| ); | |
| return config._resolveTags(); | |
| } | |
| /// 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. | |
| SuiteConfiguration change({ | |
| bool? allowDuplicateTestNames, | |
| bool? allowTestRandomization, | |
| bool? jsTrace, | |
| bool? runSkipped, | |
| Iterable<String>? dart2jsArgs, | |
| String? precompiledPath, | |
| Iterable<CompilerSelection>? compilerSelections, | |
| Iterable<RuntimeSelection>? runtimes, | |
| Map<BooleanSelector, SuiteConfiguration>? tags, | |
| Map<PlatformSelector, SuiteConfiguration>? onPlatform, | |
| bool? ignoreTimeouts, | |
| Timeout? suiteLoadTimeout, | |
| // Test-level configuration | |
| Timeout? timeout, | |
| bool? verboseTrace, | |
| bool? chainStackTraces, | |
| bool? skip, | |
| int? retry, | |
| String? skipReason, | |
| PlatformSelector? testOn, | |
| Iterable<String>? addTags, | |
| }) { | |
| var config = SuiteConfiguration._( | |
| allowDuplicateTestNames: | |
| allowDuplicateTestNames ?? _allowDuplicateTestNames, | |
| allowTestRandomization: allowTestRandomization ?? _allowTestRandomization, | |
| jsTrace: jsTrace ?? _jsTrace, | |
| runSkipped: runSkipped ?? _runSkipped, | |
| dart2jsArgs: dart2jsArgs?.toList() ?? this.dart2jsArgs, | |
| testSelections: testSelections, | |
| precompiledPath: precompiledPath ?? this.precompiledPath, | |
| compilerSelections: compilerSelections ?? this.compilerSelections, | |
| runtimes: runtimes ?? _runtimes, | |
| tags: tags ?? this.tags, | |
| onPlatform: onPlatform ?? this.onPlatform, | |
| ignoreTimeouts: ignoreTimeouts ?? _ignoreTimeouts, | |
| suiteLoadTimeout: suiteLoadTimeout ?? _suiteLoadTimeout, | |
| metadata: _metadata.change( | |
| timeout: timeout, | |
| verboseTrace: verboseTrace, | |
| chainStackTraces: chainStackTraces, | |
| skip: skip, | |
| retry: retry, | |
| skipReason: skipReason, | |
| testOn: testOn, | |
| tags: addTags?.toSet(), | |
| ), | |
| ); | |
| return config._resolveTags(); | |
| } | |
| /// Assign the selection of tests that should run for this suite. | |
| /// | |
| /// Test selections must be chosen only once per suite, once a | |
| /// SuiteConfiguration has test slections this method should not be called | |
| /// again. | |
| /// | |
| /// [testSelections] must not be empty. | |
| SuiteConfiguration selectTests(Set<TestSelection> testSelections) { | |
| assert(this.testSelections.isEmpty); | |
| assert(testSelections.isNotEmpty); | |
| return SuiteConfiguration._( | |
| testSelections: testSelections, | |
| allowDuplicateTestNames: _allowDuplicateTestNames, | |
| allowTestRandomization: _allowTestRandomization, | |
| jsTrace: _jsTrace, | |
| runSkipped: _runSkipped, | |
| dart2jsArgs: dart2jsArgs, | |
| precompiledPath: precompiledPath, | |
| compilerSelections: compilerSelections, | |
| runtimes: _runtimes, | |
| tags: tags, | |
| onPlatform: onPlatform, | |
| ignoreTimeouts: _ignoreTimeouts, | |
| suiteLoadTimeout: _suiteLoadTimeout, | |
| metadata: _metadata, | |
| ); | |
| } | |
| /// Throws a [FormatException] if this refers to any undefined runtimes. | |
| void validateRuntimes(List<Runtime> allRuntimes) { | |
| var validVariables = allRuntimes | |
| .map((runtime) => runtime.identifier) | |
| .toSet(); | |
| _metadata.validatePlatformSelectors(validVariables); | |
| var runtimes = _runtimes; | |
| if (runtimes != null) { | |
| for (var selection in runtimes) { | |
| if (!allRuntimes.any( | |
| (runtime) => runtime.identifier == selection.name, | |
| )) { | |
| if (selection.span != null) { | |
| throw SourceSpanFormatException( | |
| 'Unknown platform "${selection.name}".', | |
| selection.span, | |
| ); | |
| } else { | |
| throw FormatException('Unknown platform "${selection.name}".'); | |
| } | |
| } | |
| } | |
| } | |
| onPlatform.forEach((selector, config) { | |
| selector.validate(validVariables); | |
| config.validateRuntimes(allRuntimes); | |
| }); | |
| } | |
| /// Returns a copy of this with all platform-specific configuration from | |
| /// [onPlatform] resolved. | |
| SuiteConfiguration forPlatform(SuitePlatform platform) { | |
| if (onPlatform.isEmpty) return this; | |
| var config = this; | |
| onPlatform.forEach((platformSelector, platformConfig) { | |
| if (!platformSelector.evaluate(platform)) return; | |
| config = config.merge(platformConfig); | |
| }); | |
| return config.change(onPlatform: {}); | |
| } | |
| /// Merges two maps whose values are [SuiteConfiguration]s. | |
| /// | |
| /// Any overlapping keys in the maps have their configurations merged in the | |
| /// returned map. | |
| Map<T, SuiteConfiguration> _mergeConfigMaps<T>( | |
| Map<T, SuiteConfiguration> map1, | |
| Map<T, SuiteConfiguration> map2, | |
| ) => mergeMaps( | |
| map1, | |
| map2, | |
| value: (config1, config2) => config1.merge(config2), | |
| ); | |
| SuiteConfiguration _resolveTags() { | |
| // If there's no tag-specific configuration, or if none of it applies, just | |
| // return the configuration as-is. | |
| if (_metadata.tags.isEmpty || tags.isEmpty) return this; | |
| // Otherwise, resolve the tag-specific components. | |
| var newTags = Map<BooleanSelector, SuiteConfiguration>.from(tags); | |
| var merged = tags.keys.fold(empty, (SuiteConfiguration merged, selector) { | |
| if (!selector.evaluate(_metadata.tags.contains)) return merged; | |
| return merged.merge(newTags.remove(selector)!); | |
| }); | |
| if (merged == empty) return this; | |
| return change(tags: newTags).merge(merged); | |
| } | |
| } |