Line data Source code
1 : // Copyright (c) 2015, 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 :
8 : import 'configuration/skip.dart';
9 : import 'configuration/timeout.dart';
10 : import 'platform_selector.dart';
11 : import 'suite_platform.dart';
12 : import 'util/identifier_regex.dart';
13 : import 'util/pretty_print.dart';
14 :
15 : /// Metadata for a test or test suite.
16 : ///
17 : /// This metadata comes from declarations on the test itself; it doesn't include
18 : /// configuration from the user.
19 : class Metadata {
20 : /// Empty metadata with only default values.
21 : ///
22 : /// Using this is slightly more efficient than manually constructing a new
23 : /// metadata with no arguments.
24 0 : static final empty = Metadata._();
25 :
26 : /// The selector indicating which platforms the suite supports.
27 : final PlatformSelector testOn;
28 :
29 : /// The modification to the timeout for the test or suite.
30 : final Timeout timeout;
31 :
32 : /// Whether the test or suite should be skipped.
33 0 : bool get skip => _skip ?? false;
34 : final bool? _skip;
35 :
36 : /// The reason the test or suite should be skipped, if given.
37 : final String? skipReason;
38 :
39 : /// Whether to use verbose stack traces.
40 22 : bool get verboseTrace => _verboseTrace ?? false;
41 : final bool? _verboseTrace;
42 :
43 : /// Whether to chain stack traces.
44 33 : bool get chainStackTraces => _chainStackTraces ?? _verboseTrace ?? false;
45 : final bool? _chainStackTraces;
46 :
47 : /// The user-defined tags attached to the test or suite.
48 : final Set<String> tags;
49 :
50 : /// The number of times to re-run a test before being marked as a failure.
51 0 : int get retry => _retry ?? 0;
52 : final int? _retry;
53 :
54 : /// Platform-specific metadata.
55 : ///
56 : /// Each key identifies a platform, and its value identifies the specific
57 : /// metadata for that platform. These can be applied by calling [forPlatform].
58 : final Map<PlatformSelector, Metadata> onPlatform;
59 :
60 : /// Metadata that applies only when specific tags are applied.
61 : ///
62 : /// Tag-specific metadata is applied when merging this with other metadata.
63 : /// Note that unlike [onPlatform], the base metadata takes precedence over any
64 : /// tag-specific metadata.
65 : ///
66 : /// This is guaranteed not to have any keys that match [tags]; those are
67 : /// resolved when the metadata is constructed.
68 : final Map<BooleanSelector, Metadata> forTag;
69 :
70 : /// The language version comment, if one is present.
71 : ///
72 : /// Only available for test suites and not individual tests.
73 : final String? languageVersionComment;
74 :
75 : /// Parses a user-provided map into the value for [onPlatform].
76 11 : static Map<PlatformSelector, Metadata> _parseOnPlatform(
77 : Map<String, dynamic>? onPlatform) {
78 11 : if (onPlatform == null) return {};
79 :
80 0 : var result = <PlatformSelector, Metadata>{};
81 0 : onPlatform.forEach((platform, metadata) {
82 0 : if (metadata is Timeout || metadata is Skip) {
83 0 : metadata = [metadata];
84 0 : } else if (metadata is! List) {
85 0 : throw ArgumentError('Metadata for platform "$platform" must be a '
86 : 'Timeout, Skip, or List of those; was "$metadata".');
87 : }
88 :
89 0 : var selector = PlatformSelector.parse(platform);
90 :
91 : Timeout? timeout;
92 : dynamic skip;
93 0 : for (var metadatum in metadata) {
94 0 : if (metadatum is Timeout) {
95 : if (timeout != null) {
96 0 : throw ArgumentError('Only a single Timeout may be declared for '
97 : '"$platform".');
98 : }
99 :
100 : timeout = metadatum;
101 0 : } else if (metadatum is Skip) {
102 : if (skip != null) {
103 0 : throw ArgumentError('Only a single Skip may be declared for '
104 : '"$platform".');
105 : }
106 :
107 0 : skip = metadatum.reason ?? true;
108 : } else {
109 0 : throw ArgumentError('Metadata for platform "$platform" must be a '
110 : 'Timeout, Skip, or List of those; was "$metadata".');
111 : }
112 : }
113 :
114 0 : result[selector] = Metadata.parse(timeout: timeout, skip: skip);
115 : });
116 : return result;
117 : }
118 :
119 : /// Parses a user-provided [String] or [Iterable] into the value for [tags].
120 : ///
121 : /// Throws an [ArgumentError] if [tags] is not a [String] or an [Iterable].
122 11 : static Set<String> _parseTags(tags) {
123 : if (tags == null) return {};
124 0 : if (tags is String) return {tags};
125 0 : if (tags is! Iterable) {
126 0 : throw ArgumentError.value(
127 : tags, 'tags', 'must be either a String or an Iterable.');
128 : }
129 :
130 0 : if (tags.any((tag) => tag is! String)) {
131 0 : throw ArgumentError.value(tags, 'tags', 'must contain only Strings.');
132 : }
133 :
134 0 : return Set.from(tags);
135 : }
136 :
137 : /// Creates new Metadata.
138 : ///
139 : /// [testOn] defaults to [PlatformSelector.all].
140 : ///
141 : /// If [forTag] contains metadata that applies to [tags], that metadata is
142 : /// included inline in the returned value. The values directly passed to the
143 : /// constructor take precedence over tag-specific metadata.
144 11 : factory Metadata(
145 : {PlatformSelector? testOn,
146 : Timeout? timeout,
147 : bool? skip,
148 : bool? verboseTrace,
149 : bool? chainStackTraces,
150 : int? retry,
151 : String? skipReason,
152 : Iterable<String>? tags,
153 : Map<PlatformSelector, Metadata>? onPlatform,
154 : Map<BooleanSelector, Metadata>? forTag,
155 : String? languageVersionComment}) {
156 : // Returns metadata without forTag resolved at all.
157 22 : Metadata _unresolved() => Metadata._(
158 : testOn: testOn,
159 : timeout: timeout,
160 : skip: skip,
161 : verboseTrace: verboseTrace,
162 : chainStackTraces: chainStackTraces,
163 : retry: retry,
164 : skipReason: skipReason,
165 : tags: tags,
166 : onPlatform: onPlatform,
167 : forTag: forTag,
168 : languageVersionComment: languageVersionComment);
169 :
170 : // If there's no tag-specific metadata, or if none of it applies, just
171 : // return the metadata as-is.
172 : if (forTag == null || tags == null) return _unresolved();
173 11 : tags = Set.from(tags);
174 11 : forTag = Map.from(forTag);
175 :
176 : // Otherwise, resolve the tag-specific components. Doing this eagerly means
177 : // we only have to resolve suite- or group-level tags once, rather than
178 : // doing it for every test individually.
179 11 : var empty = Metadata._();
180 33 : var merged = forTag.keys.toList().fold(empty, (Metadata merged, selector) {
181 0 : if (!selector.evaluate(tags!.contains)) return merged;
182 0 : return merged.merge(forTag!.remove(selector)!);
183 : });
184 :
185 11 : if (merged == empty) return _unresolved();
186 0 : return merged.merge(_unresolved());
187 : }
188 :
189 : /// Creates new Metadata.
190 : ///
191 : /// Unlike [new Metadata], this assumes [forTag] is already resolved.
192 11 : Metadata._({
193 : PlatformSelector? testOn,
194 : Timeout? timeout,
195 : bool? skip,
196 : this.skipReason,
197 : bool? verboseTrace,
198 : bool? chainStackTraces,
199 : int? retry,
200 : Iterable<String>? tags,
201 : Map<PlatformSelector, Metadata>? onPlatform,
202 : Map<BooleanSelector, Metadata>? forTag,
203 : this.languageVersionComment,
204 : }) : testOn = testOn ?? PlatformSelector.all,
205 : timeout = timeout ?? const Timeout.factor(1),
206 : _skip = skip,
207 : _verboseTrace = verboseTrace,
208 : _chainStackTraces = chainStackTraces,
209 : _retry = retry,
210 22 : tags = UnmodifiableSetView(tags == null ? {} : tags.toSet()),
211 : onPlatform =
212 11 : onPlatform == null ? const {} : UnmodifiableMapView(onPlatform),
213 11 : forTag = forTag == null ? const {} : UnmodifiableMapView(forTag) {
214 0 : if (retry != null) RangeError.checkNotNegative(retry, 'retry');
215 11 : _validateTags();
216 : }
217 :
218 : /// Creates a new Metadata, but with fields parsed from caller-friendly values
219 : /// where applicable.
220 : ///
221 : /// Throws a [FormatException] if any field is invalid.
222 11 : Metadata.parse(
223 : {String? testOn,
224 : Timeout? timeout,
225 : dynamic skip,
226 : bool? verboseTrace,
227 : bool? chainStackTraces,
228 : int? retry,
229 : Map<String, dynamic>? onPlatform,
230 : tags,
231 : this.languageVersionComment})
232 : : testOn = testOn == null
233 : ? PlatformSelector.all
234 0 : : PlatformSelector.parse(testOn),
235 : timeout = timeout ?? const Timeout.factor(1),
236 0 : _skip = skip == null ? null : skip != false,
237 : _verboseTrace = verboseTrace,
238 : _chainStackTraces = chainStackTraces,
239 : _retry = retry,
240 11 : skipReason = skip is String ? skip : null,
241 11 : onPlatform = _parseOnPlatform(onPlatform),
242 11 : tags = _parseTags(tags),
243 : forTag = const {} {
244 0 : if (skip != null && skip is! String && skip is! bool) {
245 0 : throw ArgumentError('"skip" must be a String or a bool, was "$skip".');
246 : }
247 :
248 0 : if (retry != null) RangeError.checkNotNegative(retry, 'retry');
249 :
250 11 : _validateTags();
251 : }
252 :
253 : /// Deserializes the result of [Metadata.serialize] into a new [Metadata].
254 11 : Metadata.deserialize(serialized)
255 11 : : testOn = serialized['testOn'] == null
256 : ? PlatformSelector.all
257 10 : : PlatformSelector.parse(serialized['testOn'] as String),
258 22 : timeout = _deserializeTimeout(serialized['timeout']),
259 11 : _skip = serialized['skip'] as bool?,
260 11 : skipReason = serialized['skipReason'] as String?,
261 11 : _verboseTrace = serialized['verboseTrace'] as bool?,
262 11 : _chainStackTraces = serialized['chainStackTraces'] as bool?,
263 11 : _retry = serialized['retry'] as int?,
264 22 : tags = Set.from(serialized['tags'] as Iterable),
265 11 : onPlatform = {
266 11 : for (var pair in serialized['onPlatform'])
267 0 : PlatformSelector.parse(pair.first as String):
268 0 : Metadata.deserialize(pair.last)
269 : },
270 22 : forTag = (serialized['forTag'] as Map).map((key, nested) => MapEntry(
271 0 : BooleanSelector.parse(key as String),
272 0 : Metadata.deserialize(nested))),
273 : languageVersionComment =
274 11 : serialized['languageVersionComment'] as String?;
275 :
276 : /// Deserializes timeout from the format returned by [_serializeTimeout].
277 11 : static Timeout _deserializeTimeout(serialized) {
278 11 : if (serialized == 'none') return Timeout.none;
279 11 : var scaleFactor = serialized['scaleFactor'];
280 11 : if (scaleFactor != null) return Timeout.factor(scaleFactor as num);
281 0 : return Timeout(Duration(microseconds: serialized['duration'] as int));
282 : }
283 :
284 : /// Throws an [ArgumentError] if any tags in [tags] aren't hyphenated
285 : /// identifiers.
286 11 : void _validateTags() {
287 11 : var invalidTags = tags
288 11 : .where((tag) => !tag.contains(anchoredHyphenatedIdentifier))
289 11 : .map((tag) => '"$tag"')
290 11 : .toList();
291 :
292 11 : if (invalidTags.isEmpty) return;
293 :
294 0 : throw ArgumentError("Invalid ${pluralize('tag', invalidTags.length)} "
295 0 : '${toSentence(invalidTags)}. Tags must be (optionally hyphenated) '
296 : 'Dart identifiers.');
297 : }
298 :
299 : /// Throws a [FormatException] if any [PlatformSelector]s use any variables
300 : /// that don't appear either in [validVariables] or in the set of variables
301 : /// that are known to be valid for all selectors.
302 11 : void validatePlatformSelectors(Set<String> validVariables) {
303 22 : testOn.validate(validVariables);
304 22 : onPlatform.forEach((selector, metadata) {
305 0 : selector.validate(validVariables);
306 0 : metadata.validatePlatformSelectors(validVariables);
307 : });
308 : }
309 :
310 : /// Return a new [Metadata] that merges [this] with [other].
311 : ///
312 : /// If the two [Metadata]s have conflicting properties, [other] wins. If
313 : /// either has a [forTag] metadata for one of the other's tags, that metadata
314 : /// is merged as well.
315 22 : Metadata merge(Metadata other) => Metadata(
316 33 : testOn: testOn.intersection(other.testOn),
317 33 : timeout: timeout.merge(other.timeout),
318 22 : skip: other._skip ?? _skip,
319 22 : skipReason: other.skipReason ?? skipReason,
320 22 : verboseTrace: other._verboseTrace ?? _verboseTrace,
321 22 : chainStackTraces: other._chainStackTraces ?? _chainStackTraces,
322 22 : retry: other._retry ?? _retry,
323 33 : tags: tags.union(other.tags),
324 33 : onPlatform: mergeMaps(onPlatform, other.onPlatform,
325 0 : value: (metadata1, metadata2) => metadata1.merge(metadata2)),
326 33 : forTag: mergeMaps(forTag, other.forTag,
327 0 : value: (metadata1, metadata2) => metadata1.merge(metadata2)),
328 : languageVersionComment:
329 22 : other.languageVersionComment ?? languageVersionComment);
330 :
331 : /// Returns a copy of [this] with the given fields changed.
332 0 : Metadata change(
333 : {PlatformSelector? testOn,
334 : Timeout? timeout,
335 : bool? skip,
336 : bool? verboseTrace,
337 : bool? chainStackTraces,
338 : int? retry,
339 : String? skipReason,
340 : Map<PlatformSelector, Metadata>? onPlatform,
341 : Set<String>? tags,
342 : Map<BooleanSelector, Metadata>? forTag,
343 : String? languageVersionComment}) {
344 0 : testOn ??= this.testOn;
345 0 : timeout ??= this.timeout;
346 0 : skip ??= _skip;
347 0 : verboseTrace ??= _verboseTrace;
348 0 : chainStackTraces ??= _chainStackTraces;
349 0 : retry ??= _retry;
350 0 : skipReason ??= this.skipReason;
351 0 : onPlatform ??= this.onPlatform;
352 0 : tags ??= this.tags;
353 0 : forTag ??= this.forTag;
354 0 : languageVersionComment ??= this.languageVersionComment;
355 0 : return Metadata(
356 : testOn: testOn,
357 : timeout: timeout,
358 : skip: skip,
359 : verboseTrace: verboseTrace,
360 : chainStackTraces: chainStackTraces,
361 : skipReason: skipReason,
362 : onPlatform: onPlatform,
363 : tags: tags,
364 : forTag: forTag,
365 : retry: retry,
366 : languageVersionComment: languageVersionComment);
367 : }
368 :
369 : /// Returns a copy of [this] with all platform-specific metadata from
370 : /// [onPlatform] resolved.
371 11 : Metadata forPlatform(SuitePlatform platform) {
372 22 : if (onPlatform.isEmpty) return this;
373 :
374 : var metadata = this;
375 0 : onPlatform.forEach((platformSelector, platformMetadata) {
376 0 : if (!platformSelector.evaluate(platform)) return;
377 0 : metadata = metadata.merge(platformMetadata);
378 : });
379 0 : return metadata.change(onPlatform: {});
380 : }
381 :
382 : /// Serializes [this] into a JSON-safe object that can be deserialized using
383 : /// [Metadata.deserialize].
384 11 : Map<String, dynamic> serialize() {
385 : // Make this a list to guarantee that the order is preserved.
386 11 : var serializedOnPlatform = [];
387 22 : onPlatform.forEach((key, value) {
388 0 : serializedOnPlatform.add([key.toString(), value.serialize()]);
389 : });
390 :
391 11 : return {
392 32 : 'testOn': testOn == PlatformSelector.all ? null : testOn.toString(),
393 22 : 'timeout': _serializeTimeout(timeout),
394 11 : 'skip': _skip,
395 11 : 'skipReason': skipReason,
396 11 : 'verboseTrace': _verboseTrace,
397 11 : 'chainStackTraces': _chainStackTraces,
398 11 : 'retry': _retry,
399 22 : 'tags': tags.toList(),
400 : 'onPlatform': serializedOnPlatform,
401 22 : 'forTag': forTag.map((selector, metadata) =>
402 0 : MapEntry(selector.toString(), metadata.serialize())),
403 11 : 'languageVersionComment': languageVersionComment,
404 : };
405 : }
406 :
407 : /// Serializes timeout into a JSON-safe object.
408 11 : dynamic _serializeTimeout(Timeout timeout) {
409 11 : if (timeout == Timeout.none) return 'none';
410 11 : return {
411 11 : 'duration': timeout.duration?.inMicroseconds,
412 11 : 'scaleFactor': timeout.scaleFactor
413 : };
414 : }
415 : }
|