Line data Source code
1 : // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'package:boolean_selector/boolean_selector.dart';
6 : import 'package:collection/collection.dart';
7 : import 'package:source_span/source_span.dart';
8 : import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
9 : show
10 : Timeout;
11 : import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
12 : import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
13 : import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
14 : import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
15 :
16 : import 'runtime_selection.dart';
17 :
18 : /// Suite-level configuration.
19 : ///
20 : /// This tracks configuration that can differ from suite to suite.
21 : class SuiteConfiguration {
22 : /// Empty configuration with only default values.
23 : ///
24 : /// Using this is slightly more efficient than manually constructing a new
25 : /// configuration with no arguments.
26 0 : static final empty = SuiteConfiguration._(
27 : allowDuplicateTestNames: null,
28 : allowTestRandomization: null,
29 : jsTrace: null,
30 : runSkipped: null,
31 : dart2jsArgs: null,
32 : precompiledPath: null,
33 : patterns: null,
34 : runtimes: null,
35 : includeTags: null,
36 : excludeTags: null,
37 : tags: null,
38 : onPlatform: null,
39 : metadata: null,
40 : line: null,
41 : col: null);
42 :
43 : /// Whether or not duplicate test (or group) names are allowed within the same
44 : /// test suite.
45 : //
46 : // TODO: Change the default https://github.com/dart-lang/test/issues/1571
47 0 : bool get allowDuplicateTestNames => _allowDuplicateTestNames ?? true;
48 : final bool? _allowDuplicateTestNames;
49 :
50 : /// Whether test randomization should be allowed for this test.
51 0 : bool get allowTestRandomization => _allowTestRandomization ?? true;
52 : final bool? _allowTestRandomization;
53 :
54 : /// Whether JavaScript stack traces should be left as-is or converted to
55 : /// Dart-like traces.
56 0 : bool get jsTrace => _jsTrace ?? false;
57 : final bool? _jsTrace;
58 :
59 : /// Whether skipped tests should be run.
60 0 : bool get runSkipped => _runSkipped ?? false;
61 : final bool? _runSkipped;
62 :
63 : /// The path to a mirror of this package containing HTML that points to
64 : /// precompiled JS.
65 : ///
66 : /// This is used by the internal Google test runner so that test compilation
67 : /// can more effectively make use of Google's build tools.
68 : final String? precompiledPath;
69 :
70 : /// Additional arguments to pass to dart2js.
71 : ///
72 : /// Note that this if multiple suites run the same JavaScript on different
73 : /// runtimes, and they have different [dart2jsArgs], only one (undefined)
74 : /// suite's arguments will be used.
75 : final List<String> dart2jsArgs;
76 :
77 : /// The patterns to match against test names to decide which to run.
78 : ///
79 : /// All patterns must match in order for a test to be run.
80 : ///
81 : /// If empty, all tests should be run.
82 : final Set<Pattern> patterns;
83 :
84 : /// The set of runtimes on which to run tests.
85 0 : List<String> get runtimes => _runtimes == null
86 : ? const ['vm']
87 0 : : List.unmodifiable(_runtimes!.map((runtime) => runtime.name));
88 : final List<RuntimeSelection>? _runtimes;
89 :
90 : /// Only run tests whose tags match this selector.
91 : ///
92 : /// When [merge]d, this is intersected with the other configuration's included
93 : /// tags.
94 : final BooleanSelector includeTags;
95 :
96 : /// Do not run tests whose tags match this selector.
97 : ///
98 : /// When [merge]d, this is unioned with the other configuration's
99 : /// excluded tags.
100 : final BooleanSelector excludeTags;
101 :
102 : /// Configuration for particular tags.
103 : ///
104 : /// The keys are tag selectors, and the values are configurations for tests
105 : /// whose tags match those selectors.
106 : final Map<BooleanSelector, SuiteConfiguration> tags;
107 :
108 : /// Configuration for particular platforms.
109 : ///
110 : /// The keys are platform selectors, and the values are configurations for
111 : /// those platforms. These configuration should only contain test-level
112 : /// configuration fields, but that isn't enforced.
113 : final Map<PlatformSelector, SuiteConfiguration> onPlatform;
114 :
115 : /// The global test metadata derived from this configuration.
116 0 : Metadata get metadata {
117 0 : if (tags.isEmpty && onPlatform.isEmpty) return _metadata;
118 0 : return _metadata.change(
119 0 : forTag: tags.map((key, config) => MapEntry(key, config.metadata)),
120 : onPlatform:
121 0 : onPlatform.map((key, config) => MapEntry(key, config.metadata)));
122 : }
123 :
124 : final Metadata _metadata;
125 :
126 : /// The set of tags that have been declared in any way in this configuration.
127 0 : Set<String> get knownTags => _knownTags ??= UnmodifiableSetView({
128 0 : ...includeTags.variables,
129 0 : ...excludeTags.variables,
130 0 : ..._metadata.tags,
131 0 : for (var selector in tags.keys) ...selector.variables,
132 0 : for (var configuration in tags.values) ...configuration.knownTags,
133 0 : for (var configuration in onPlatform.values) ...configuration.knownTags,
134 : });
135 : Set<String>? _knownTags;
136 :
137 : /// Only run tests that originate from this line in a test file.
138 : final int? line;
139 :
140 : /// Only run tests that original from this column in a test file.
141 : final int? col;
142 :
143 0 : factory SuiteConfiguration(
144 : {required bool? allowDuplicateTestNames,
145 : required bool? allowTestRandomization,
146 : required bool? jsTrace,
147 : required bool? runSkipped,
148 : required Iterable<String>? dart2jsArgs,
149 : required String? precompiledPath,
150 : required Iterable<Pattern>? patterns,
151 : required Iterable<RuntimeSelection>? runtimes,
152 : required BooleanSelector? includeTags,
153 : required BooleanSelector? excludeTags,
154 : required Map<BooleanSelector, SuiteConfiguration>? tags,
155 : required Map<PlatformSelector, SuiteConfiguration>? onPlatform,
156 : required int? line,
157 : required int? col,
158 :
159 : // Test-level configuration
160 : required Timeout? timeout,
161 : required bool? verboseTrace,
162 : required bool? chainStackTraces,
163 : required bool? skip,
164 : required int? retry,
165 : required String? skipReason,
166 : required PlatformSelector? testOn,
167 : required Iterable<String>? addTags}) {
168 0 : var config = SuiteConfiguration._(
169 : allowDuplicateTestNames: allowDuplicateTestNames,
170 : allowTestRandomization: allowTestRandomization,
171 : jsTrace: jsTrace,
172 : runSkipped: runSkipped,
173 : dart2jsArgs: dart2jsArgs,
174 : precompiledPath: precompiledPath,
175 : patterns: patterns,
176 : runtimes: runtimes,
177 : includeTags: includeTags,
178 : excludeTags: excludeTags,
179 : tags: tags,
180 : onPlatform: onPlatform,
181 : line: line,
182 : col: col,
183 0 : metadata: Metadata(
184 : timeout: timeout,
185 : verboseTrace: verboseTrace,
186 : chainStackTraces: chainStackTraces,
187 : skip: skip,
188 : retry: retry,
189 : skipReason: skipReason,
190 : testOn: testOn,
191 : tags: addTags));
192 0 : return config._resolveTags();
193 : }
194 :
195 : /// A constructor that doesn't require all of its options to be passed.
196 : ///
197 : /// This should only be used in situations where you really only want to
198 : /// configure a specific restricted set of options.
199 0 : factory SuiteConfiguration._unsafe(
200 : {bool? allowDuplicateTestNames,
201 : bool? allowTestRandomization,
202 : bool? jsTrace,
203 : bool? runSkipped,
204 : Iterable<String>? dart2jsArgs,
205 : String? precompiledPath,
206 : Iterable<Pattern>? patterns,
207 : Iterable<RuntimeSelection>? runtimes,
208 : BooleanSelector? includeTags,
209 : BooleanSelector? excludeTags,
210 : Map<BooleanSelector, SuiteConfiguration>? tags,
211 : Map<PlatformSelector, SuiteConfiguration>? onPlatform,
212 : int? line,
213 : int? col,
214 :
215 : // Test-level configuration
216 : Timeout? timeout,
217 : bool? verboseTrace,
218 : bool? chainStackTraces,
219 : bool? skip,
220 : int? retry,
221 : String? skipReason,
222 : PlatformSelector? testOn,
223 : Iterable<String>? addTags}) =>
224 0 : SuiteConfiguration(
225 : allowDuplicateTestNames: allowDuplicateTestNames,
226 : allowTestRandomization: allowTestRandomization,
227 : jsTrace: jsTrace,
228 : runSkipped: runSkipped,
229 : dart2jsArgs: dart2jsArgs,
230 : precompiledPath: precompiledPath,
231 : patterns: patterns,
232 : runtimes: runtimes,
233 : includeTags: includeTags,
234 : excludeTags: excludeTags,
235 : tags: tags,
236 : onPlatform: onPlatform,
237 : line: line,
238 : col: col,
239 : timeout: timeout,
240 : verboseTrace: verboseTrace,
241 : chainStackTraces: chainStackTraces,
242 : skip: skip,
243 : retry: retry,
244 : skipReason: skipReason,
245 : testOn: testOn,
246 : addTags: addTags);
247 :
248 : /// A specialized constructor for only configuring the runtimes.
249 0 : factory SuiteConfiguration.runtimes(Iterable<RuntimeSelection> runtimes) =>
250 0 : SuiteConfiguration._unsafe(runtimes: runtimes);
251 :
252 : /// A specialized constructor for only configuring runSkipped.
253 0 : factory SuiteConfiguration.runSkipped(bool runSkipped) =>
254 0 : SuiteConfiguration._unsafe(runSkipped: runSkipped);
255 :
256 : /// A specialized constructor for only configuring the timeout.
257 0 : factory SuiteConfiguration.timeout(Timeout timeout) =>
258 0 : SuiteConfiguration._unsafe(timeout: timeout);
259 :
260 : /// Creates new SuiteConfiguration.
261 : ///
262 : /// Unlike [new SuiteConfiguration], this assumes [tags] is already
263 : /// resolved.
264 0 : SuiteConfiguration._(
265 : {required bool? allowDuplicateTestNames,
266 : required bool? allowTestRandomization,
267 : required bool? jsTrace,
268 : required bool? runSkipped,
269 : required Iterable<String>? dart2jsArgs,
270 : required this.precompiledPath,
271 : required Iterable<Pattern>? patterns,
272 : required Iterable<RuntimeSelection>? runtimes,
273 : required BooleanSelector? includeTags,
274 : required BooleanSelector? excludeTags,
275 : required Map<BooleanSelector, SuiteConfiguration>? tags,
276 : required Map<PlatformSelector, SuiteConfiguration>? onPlatform,
277 : required Metadata? metadata,
278 : required this.line,
279 : required this.col})
280 : : _allowDuplicateTestNames = allowDuplicateTestNames,
281 : _allowTestRandomization = allowTestRandomization,
282 : _jsTrace = jsTrace,
283 : _runSkipped = runSkipped,
284 0 : dart2jsArgs = _list(dart2jsArgs) ?? const [],
285 0 : patterns = UnmodifiableSetView(patterns?.toSet() ?? {}),
286 0 : _runtimes = _list(runtimes),
287 : includeTags = includeTags ?? BooleanSelector.all,
288 : excludeTags = excludeTags ?? BooleanSelector.none,
289 0 : tags = _map(tags),
290 0 : onPlatform = _map(onPlatform),
291 0 : _metadata = metadata ?? Metadata.empty;
292 :
293 : /// Creates a new [SuiteConfiguration] that takes its configuration from
294 : /// [metadata].
295 0 : factory SuiteConfiguration.fromMetadata(Metadata metadata) =>
296 0 : SuiteConfiguration._(
297 0 : tags: metadata.forTag.map((key, child) =>
298 0 : MapEntry(key, SuiteConfiguration.fromMetadata(child))),
299 0 : onPlatform: metadata.onPlatform.map((key, child) =>
300 0 : MapEntry(key, SuiteConfiguration.fromMetadata(child))),
301 0 : metadata: metadata.change(forTag: {}, onPlatform: {}),
302 : allowDuplicateTestNames: null,
303 : allowTestRandomization: null,
304 : jsTrace: null,
305 : runSkipped: null,
306 : dart2jsArgs: null,
307 : precompiledPath: null,
308 : patterns: null,
309 : runtimes: null,
310 : includeTags: null,
311 : excludeTags: null,
312 : line: null,
313 : col: null,
314 : );
315 :
316 : /// Returns an unmodifiable copy of [input].
317 : ///
318 : /// If [input] is `null` or empty, this returns `null`.
319 0 : static List<T>? _list<T>(Iterable<T>? input) {
320 : if (input == null) return null;
321 0 : var list = List<T>.unmodifiable(input);
322 0 : if (list.isEmpty) return null;
323 : return list;
324 : }
325 :
326 : /// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
327 0 : static Map<K, V> _map<K, V>(Map<K, V>? input) {
328 0 : if (input == null || input.isEmpty) return const <Never, Never>{};
329 0 : return Map.unmodifiable(input);
330 : }
331 :
332 : /// Merges this with [other].
333 : ///
334 : /// For most fields, if both configurations have values set, [other]'s value
335 : /// takes precedence. However, certain fields are merged together instead.
336 : /// This is indicated in those fields' documentation.
337 0 : SuiteConfiguration merge(SuiteConfiguration other) {
338 0 : if (this == SuiteConfiguration.empty) return other;
339 0 : if (other == SuiteConfiguration.empty) return this;
340 :
341 0 : var config = SuiteConfiguration._(
342 : allowDuplicateTestNames:
343 0 : other._allowDuplicateTestNames ?? _allowDuplicateTestNames,
344 : allowTestRandomization:
345 0 : other._allowTestRandomization ?? _allowTestRandomization,
346 0 : jsTrace: other._jsTrace ?? _jsTrace,
347 0 : runSkipped: other._runSkipped ?? _runSkipped,
348 0 : dart2jsArgs: dart2jsArgs.toList()..addAll(other.dart2jsArgs),
349 0 : precompiledPath: other.precompiledPath ?? precompiledPath,
350 0 : patterns: patterns.union(other.patterns),
351 0 : runtimes: other._runtimes ?? _runtimes,
352 0 : includeTags: includeTags.intersection(other.includeTags),
353 0 : excludeTags: excludeTags.union(other.excludeTags),
354 0 : tags: _mergeConfigMaps(tags, other.tags),
355 0 : onPlatform: _mergeConfigMaps(onPlatform, other.onPlatform),
356 0 : line: other.line ?? line,
357 0 : col: other.col ?? col,
358 0 : metadata: metadata.merge(other.metadata));
359 0 : return config._resolveTags();
360 : }
361 :
362 : /// Returns a copy of this configuration with the given fields updated.
363 : ///
364 : /// Note that unlike [merge], this has no merging behavior—the old value is
365 : /// always replaced by the new one.
366 0 : SuiteConfiguration change(
367 : {bool? allowDuplicateTestNames,
368 : bool? allowTestRandomization,
369 : bool? jsTrace,
370 : bool? runSkipped,
371 : Iterable<String>? dart2jsArgs,
372 : String? precompiledPath,
373 : Iterable<Pattern>? patterns,
374 : Iterable<RuntimeSelection>? runtimes,
375 : BooleanSelector? includeTags,
376 : BooleanSelector? excludeTags,
377 : Map<BooleanSelector, SuiteConfiguration>? tags,
378 : Map<PlatformSelector, SuiteConfiguration>? onPlatform,
379 : int? line,
380 : int? col,
381 :
382 : // Test-level configuration
383 : Timeout? timeout,
384 : bool? verboseTrace,
385 : bool? chainStackTraces,
386 : bool? skip,
387 : int? retry,
388 : String? skipReason,
389 : PlatformSelector? testOn,
390 : Iterable<String>? addTags}) {
391 0 : var config = SuiteConfiguration._(
392 : allowDuplicateTestNames:
393 0 : allowDuplicateTestNames ?? _allowDuplicateTestNames,
394 : allowTestRandomization:
395 0 : allowTestRandomization ?? _allowTestRandomization,
396 0 : jsTrace: jsTrace ?? _jsTrace,
397 0 : runSkipped: runSkipped ?? _runSkipped,
398 0 : dart2jsArgs: dart2jsArgs?.toList() ?? this.dart2jsArgs,
399 0 : precompiledPath: precompiledPath ?? this.precompiledPath,
400 0 : patterns: patterns ?? this.patterns,
401 0 : runtimes: runtimes ?? _runtimes,
402 0 : includeTags: includeTags ?? this.includeTags,
403 0 : excludeTags: excludeTags ?? this.excludeTags,
404 0 : tags: tags ?? this.tags,
405 0 : onPlatform: onPlatform ?? this.onPlatform,
406 0 : line: line ?? this.line,
407 0 : col: col ?? this.col,
408 0 : metadata: _metadata.change(
409 : timeout: timeout,
410 : verboseTrace: verboseTrace,
411 : chainStackTraces: chainStackTraces,
412 : skip: skip,
413 : retry: retry,
414 : skipReason: skipReason,
415 : testOn: testOn,
416 0 : tags: addTags?.toSet()));
417 0 : return config._resolveTags();
418 : }
419 :
420 : /// Throws a [FormatException] if this refers to any undefined runtimes.
421 0 : void validateRuntimes(List<Runtime> allRuntimes) {
422 : var validVariables =
423 0 : allRuntimes.map((runtime) => runtime.identifier).toSet();
424 0 : _metadata.validatePlatformSelectors(validVariables);
425 :
426 0 : var runtimes = _runtimes;
427 : if (runtimes != null) {
428 0 : for (var selection in runtimes) {
429 : if (!allRuntimes
430 0 : .any((runtime) => runtime.identifier == selection.name)) {
431 0 : if (selection.span != null) {
432 0 : throw SourceSpanFormatException(
433 0 : 'Unknown platform "${selection.name}".', selection.span);
434 : } else {
435 0 : throw FormatException('Unknown platform "${selection.name}".');
436 : }
437 : }
438 : }
439 : }
440 :
441 0 : onPlatform.forEach((selector, config) {
442 0 : selector.validate(validVariables);
443 0 : config.validateRuntimes(allRuntimes);
444 : });
445 : }
446 :
447 : /// Returns a copy of this with all platform-specific configuration from
448 : /// [onPlatform] resolved.
449 0 : SuiteConfiguration forPlatform(SuitePlatform platform) {
450 0 : if (onPlatform.isEmpty) return this;
451 :
452 : var config = this;
453 0 : onPlatform.forEach((platformSelector, platformConfig) {
454 0 : if (!platformSelector.evaluate(platform)) return;
455 0 : config = config.merge(platformConfig);
456 : });
457 0 : return config.change(onPlatform: {});
458 : }
459 :
460 : /// Merges two maps whose values are [SuiteConfiguration]s.
461 : ///
462 : /// Any overlapping keys in the maps have their configurations merged in the
463 : /// returned map.
464 0 : Map<T, SuiteConfiguration> _mergeConfigMaps<T>(
465 : Map<T, SuiteConfiguration> map1, Map<T, SuiteConfiguration> map2) =>
466 0 : mergeMaps(map1, map2,
467 0 : value: (config1, config2) => config1.merge(config2));
468 :
469 0 : SuiteConfiguration _resolveTags() {
470 : // If there's no tag-specific configuration, or if none of it applies, just
471 : // return the configuration as-is.
472 0 : if (_metadata.tags.isEmpty || tags.isEmpty) return this;
473 :
474 : // Otherwise, resolve the tag-specific components.
475 0 : var newTags = Map<BooleanSelector, SuiteConfiguration>.from(tags);
476 0 : var merged = tags.keys.fold(empty, (SuiteConfiguration merged, selector) {
477 0 : if (!selector.evaluate(_metadata.tags.contains)) return merged;
478 0 : return merged.merge(newTags.remove(selector)!);
479 : });
480 :
481 0 : if (merged == empty) return this;
482 0 : return change(tags: newTags).merge(merged);
483 : }
484 : }
|