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