Skip to content

Commit 4a4174e

Browse files
committed
Don't use TestPlatform.all in PlatformSelector
We want users to be able to dynamically define new platforms, which means we need infrastructure in place for piping those platforms to places that previously assumed TestPlatform.all was a full list of available platforms. PlatformSelector is the trickiest example, since it's parsed in a number of different places and needs to provide useful feedback to users when they use an undefined platform. This splits parsing and platform validation into two separate steps. Validation will be done immediately after parsing when the selectors come from top-level annotations or parameters passed to test() or group(), but selectors defined in configuration files are now parsed only after all configuration is parsed. This will allow new platforms to be defined *and* referenced in configuration files. See #99 See #391
1 parent bfd24ca commit 4a4174e

20 files changed

+354
-135
lines changed

lib/src/backend/declarer.dart

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'group_entry.dart';
1313
import 'invoker.dart';
1414
import 'metadata.dart';
1515
import 'test.dart';
16+
import 'test_platform.dart';
1617

1718
/// A class that manages the state of tests as they're declared.
1819
///
@@ -22,6 +23,9 @@ import 'test.dart';
2223
/// for a block using [Declarer.declare], and it can be accessed using
2324
/// [Declarer.current].
2425
class Declarer {
26+
/// All test plaforms that are defined for the current test run.
27+
final List<TestPlatform> _allPlatforms;
28+
2529
/// The parent declarer, or `null` if this corresponds to the root group.
2630
final Declarer _parent;
2731

@@ -89,12 +93,13 @@ class Declarer {
8993
/// thousands of tests are being declared (see #457).
9094
///
9195
/// If [noRetry] is `true` tests will be run at most once.
92-
Declarer({Metadata metadata, bool collectTraces: false, bool noRetry: false})
93-
: this._(null, null, metadata ?? new Metadata(), collectTraces, null,
94-
noRetry);
96+
Declarer(List<TestPlatform> allPlatforms,
97+
{Metadata metadata, bool collectTraces: false, bool noRetry: false})
98+
: this._(allPlatforms, null, null, metadata ?? new Metadata(),
99+
collectTraces, null, noRetry);
95100

96-
Declarer._(this._parent, this._name, this._metadata, this._collectTraces,
97-
this._trace, this._noRetry);
101+
Declarer._(this._allPlatforms, this._parent, this._name, this._metadata,
102+
this._collectTraces, this._trace, this._noRetry);
98103

99104
/// Runs [body] with this declarer as [Declarer.current].
100105
///
@@ -111,13 +116,15 @@ class Declarer {
111116
int retry}) {
112117
_checkNotBuilt("test");
113118

114-
var metadata = _metadata.merge(new Metadata.parse(
119+
var newMetadata = new Metadata.parse(
115120
testOn: testOn,
116121
timeout: timeout,
117122
skip: skip,
118123
onPlatform: onPlatform,
119124
tags: tags,
120-
retry: _noRetry ? 0 : retry));
125+
retry: _noRetry ? 0 : retry);
126+
newMetadata.validatePlatformSelectors(_allPlatforms);
127+
var metadata = _metadata.merge(newMetadata);
121128

122129
_entries.add(new LocalTest(_prefix(name), metadata, () async {
123130
var parents = <Declarer>[];
@@ -151,17 +158,19 @@ class Declarer {
151158
int retry}) {
152159
_checkNotBuilt("group");
153160

154-
var metadata = _metadata.merge(new Metadata.parse(
161+
var newMetadata = new Metadata.parse(
155162
testOn: testOn,
156163
timeout: timeout,
157164
skip: skip,
158165
onPlatform: onPlatform,
159166
tags: tags,
160-
retry: retry));
167+
retry: _noRetry ? 0 : retry);
168+
newMetadata.validatePlatformSelectors(_allPlatforms);
169+
var metadata = _metadata.merge(newMetadata);
161170
var trace = _collectTraces ? new Trace.current(2) : null;
162171

163-
var declarer = new Declarer._(
164-
this, _prefix(name), metadata, _collectTraces, trace, _noRetry);
172+
var declarer = new Declarer._(_allPlatforms, this, _prefix(name), metadata,
173+
_collectTraces, trace, _noRetry);
165174
declarer.declare(() {
166175
// Cast to dynamic to avoid the analyzer complaining about us using the
167176
// result of a void method.

lib/src/backend/metadata.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ class Metadata {
286286
"Dart identifiers.");
287287
}
288288

289+
/// Throws a [FormatException] if any [PlatformSelector]s use variables that
290+
/// are undefined in [allPlatforms] or other sources of valid variables.
291+
void validatePlatformSelectors(List<TestPlatform> allPlatforms) {
292+
testOn.validate(allPlatforms);
293+
onPlatform.forEach((selector, metadata) {
294+
selector.validate(allPlatforms);
295+
metadata.validatePlatformSelectors(allPlatforms);
296+
});
297+
}
298+
289299
/// Return a new [Metadata] that merges [this] with [other].
290300
///
291301
/// If the two [Metadata]s have conflicting properties, [other] wins. If

lib/src/backend/platform_selector.dart

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ import 'package:source_span/source_span.dart';
88
import 'operating_system.dart';
99
import 'test_platform.dart';
1010

11-
/// The set of all valid variable names.
11+
/// The set of statically-known valid variable names.
1212
final _validVariables =
1313
new Set<String>.from(["posix", "dart-vm", "browser", "js", "blink"])
14-
..addAll(TestPlatform.all.map((platform) => platform.identifier))
1514
..addAll(OperatingSystem.all.map((os) => os.identifier));
1615

1716
/// An expression for selecting certain platforms, including operating systems
@@ -27,16 +26,46 @@ class PlatformSelector {
2726
/// The boolean selector used to implement this selector.
2827
final BooleanSelector _inner;
2928

29+
/// The source span from which this selector was parsed.
30+
final SourceSpan _span;
31+
3032
/// Parses [selector].
3133
///
32-
/// This will throw a [SourceSpanFormatException] if the selector is
33-
/// malformed or if it uses an undefined variable.
34-
PlatformSelector.parse(String selector)
35-
: _inner = new BooleanSelector.parse(selector) {
36-
_inner.validate(_validVariables.contains);
34+
/// If [span] is passed, it indicates the location of the text for [selector]
35+
/// in a larger document. It's used for error reporting.
36+
PlatformSelector.parse(String selector, [SourceSpan span])
37+
: _inner = _wrapFormatException(
38+
() => new BooleanSelector.parse(selector), span),
39+
_span = span;
40+
41+
const PlatformSelector._(this._inner) : _span = null;
42+
43+
/// Runs [body] and wraps any [FormatException] it throws in a
44+
/// [SourceSpanFormatException] using [span].
45+
///
46+
/// If [span] is `null`, runs [body] as-is.
47+
static T _wrapFormatException<T>(T body(), SourceSpan span) {
48+
if (span == null) return body();
49+
50+
try {
51+
return body();
52+
} on FormatException catch (error) {
53+
throw new SourceSpanFormatException(error.message, span);
54+
}
3755
}
3856

39-
const PlatformSelector._(this._inner);
57+
/// Throws a [FormatException] if any variables are undefined in
58+
/// [allPlatforms] or other sources of valid variables.
59+
void validate(Iterable<TestPlatform> allPlatforms) {
60+
if (identical(this, all)) return;
61+
62+
_wrapFormatException(() {
63+
_inner.validate((name) {
64+
if (_validVariables.contains(name)) return true;
65+
return allPlatforms.any((platform) => name == platform.identifier);
66+
});
67+
}, _span);
68+
}
4069

4170
/// Returns whether the selector matches the given [platform] and [os].
4271
///

lib/src/executable.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ transformers:
172172
} on ApplicationException catch (error) {
173173
stderr.writeln(error.message);
174174
exitCode = exit_codes.data;
175+
} on SourceSpanFormatException catch (error) {
176+
stderr.writeln(error.toString(color: configuration.color));
177+
exitCode = exit_codes.data;
178+
} on FormatException catch (error) {
179+
stderr.writeln(error.message);
180+
exitCode = exit_codes.data;
175181
} catch (error, stackTrace) {
176182
stderr.writeln(getErrorMessage(error));
177183
stderr.writeln(new Trace.from(stackTrace).terse);

lib/src/runner.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class Runner {
8080
/// This starts running tests and printing their progress. It returns whether
8181
/// or not they ran successfully.
8282
Future<bool> run() => _config.asCurrent(() async {
83+
_config.validatePlatforms(_loader.allPlatforms);
84+
8385
if (_closed) {
8486
throw new StateError("run() may not be called on a closed Runner.");
8587
}

lib/src/runner/configuration.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import 'package:glob/glob.dart';
1010
import 'package:path/path.dart' as p;
1111

1212
import '../backend/platform_selector.dart';
13+
import '../backend/test_platform.dart';
1314
import '../frontend/timeout.dart';
1415
import '../util/io.dart';
1516
import 'configuration/args.dart' as args;
1617
import 'configuration/load.dart';
18+
import 'configuration/platform_selection.dart';
1719
import 'configuration/reporters.dart';
1820
import 'configuration/suite.dart';
1921
import 'configuration/values.dart';
@@ -219,7 +221,7 @@ class Configuration {
219221
Iterable<String> dart2jsArgs,
220222
String precompiledPath,
221223
Iterable<Pattern> patterns,
222-
Iterable<String> platforms,
224+
Iterable<PlatformSelection> platforms,
223225
BooleanSelector includeTags,
224226
BooleanSelector excludeTags,
225227
Map<BooleanSelector, SuiteConfiguration> tags,
@@ -383,6 +385,14 @@ class Configuration {
383385
/// asynchronous callbacks transitively created by [body].
384386
T asCurrent<T>(T body()) => runZoned(body, zoneValues: {_currentKey: this});
385387

388+
/// Throws a [FormatException] if [this] refers to any undefined platforms.
389+
void validatePlatforms(List<TestPlatform> allPlatforms) {
390+
suiteDefaults.validatePlatforms(allPlatforms);
391+
for (var config in presets.values) {
392+
config.validatePlatforms(allPlatforms);
393+
}
394+
}
395+
386396
/// Merges this with [other].
387397
///
388398
/// For most fields, if both configurations have values set, [other]'s value
@@ -466,7 +476,7 @@ class Configuration {
466476
Iterable<String> dart2jsArgs,
467477
String precompiledPath,
468478
Iterable<Pattern> patterns,
469-
Iterable<String> platforms,
479+
Iterable<PlatformSelection> platforms,
470480
BooleanSelector includeTags,
471481
BooleanSelector excludeTags,
472482
Map<BooleanSelector, SuiteConfiguration> tags,

lib/src/runner/configuration/args.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import 'package:boolean_selector/boolean_selector.dart';
1010
import '../../backend/test_platform.dart';
1111
import '../../frontend/timeout.dart';
1212
import '../configuration.dart';
13+
import 'platform_selection.dart';
1314
import 'reporters.dart';
1415
import 'values.dart';
1516

1617
/// The parser used to parse the command-line arguments.
1718
final ArgParser _parser = (() {
1819
var parser = new ArgParser(allowTrailingOptions: true);
1920

20-
var allPlatforms = TestPlatform.all.toList();
21+
var allPlatforms = TestPlatform.all.toList()..remove(TestPlatform.vm);
2122
if (!Platform.isMacOS) allPlatforms.remove(TestPlatform.safari);
2223
if (!Platform.isWindows) allPlatforms.remove(TestPlatform.internetExplorer);
2324

@@ -62,9 +63,9 @@ final ArgParser _parser = (() {
6263
parser.addSeparator("======== Running Tests");
6364
parser.addOption("platform",
6465
abbr: 'p',
65-
help: 'The platform(s) on which to run the tests.',
66-
defaultsTo: 'vm',
67-
allowed: allPlatforms.map((platform) => platform.identifier).toList(),
66+
help: 'The platform(s) on which to run the tests.\n'
67+
'[vm (default), '
68+
'${allPlatforms.map((platform) => platform.identifier).join(", ")}]',
6869
allowMultiple: true);
6970
parser.addOption("preset",
7071
abbr: 'P',
@@ -220,7 +221,9 @@ class _Parser {
220221
totalShards: totalShards,
221222
timeout: _parseOption('timeout', (value) => new Timeout.parse(value)),
222223
patterns: patterns,
223-
platforms: _ifParsed('platform') as List<String>,
224+
platforms: (_ifParsed('platform') as List<String>)
225+
?.map((platform) => new PlatformSelection(platform))
226+
?.toList(),
224227
runSkipped: _ifParsed('run-skipped'),
225228
chosenPresets: _ifParsed('preset') as List<String>,
226229
paths: _options.rest.isEmpty ? null : _options.rest,

lib/src/runner/configuration/load.dart

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import 'package:yaml/yaml.dart';
1313

1414
import '../../backend/operating_system.dart';
1515
import '../../backend/platform_selector.dart';
16-
import '../../backend/test_platform.dart';
1716
import '../../frontend/timeout.dart';
1817
import '../../util/io.dart';
1918
import '../../utils.dart';
2019
import '../configuration.dart';
2120
import '../configuration/suite.dart';
21+
import 'platform_selection.dart';
2222
import 'reporters.dart';
2323

2424
/// A regular expression matching a Dart identifier.
@@ -94,7 +94,7 @@ class _ConfigurationLoader {
9494

9595
var onPlatform = _getMap("on_platform",
9696
key: (keyNode) => _parseNode(keyNode, "on_platform key",
97-
(value) => new PlatformSelector.parse(value)),
97+
(value) => new PlatformSelector.parse(value, keyNode.span)),
9898
value: (valueNode) =>
9999
_nestedConfig(valueNode, "on_platform value", runnerConfig: false));
100100

@@ -155,8 +155,7 @@ class _ConfigurationLoader {
155155
skip = true;
156156
}
157157

158-
var testOn =
159-
_parseValue("test_on", (value) => new PlatformSelector.parse(value));
158+
var testOn = _parsePlatformSelector("test_on");
160159

161160
var addTags = _getList(
162161
"add_tags", (tagNode) => _parseIdentifierLike(tagNode, "Tag name"));
@@ -206,14 +205,11 @@ class _ConfigurationLoader {
206205

207206
var concurrency = _getInt("concurrency");
208207

209-
var platforms = _getList("platforms", (platformNode) {
210-
var name = _parseIdentifierLike(platformNode, "Platform name");
211-
if (!TestPlatform.all.any((platform) => platform.identifier == name)) {
212-
throw new SourceSpanFormatException(
213-
'Unknown platform "$name"', platformNode.span, _source);
214-
}
215-
return name;
216-
});
208+
var platforms = _getList(
209+
"platforms",
210+
(platformNode) => new PlatformSelection(
211+
_parseIdentifierLike(platformNode, "Platform name"),
212+
platformNode.span));
217213

218214
var chosenPresets = _getList("add_presets",
219215
(presetNode) => _parseIdentifierLike(presetNode, "Preset name"));
@@ -409,6 +405,14 @@ class _ConfigurationLoader {
409405
BooleanSelector _parseBooleanSelector(String name) =>
410406
_parseValue(name, (value) => new BooleanSelector.parse(value));
411407

408+
/// Parses [node]'s value as a platform selector.
409+
PlatformSelector _parsePlatformSelector(String field) {
410+
var node = _document.nodes[field];
411+
if (node == null) return null;
412+
return _parseNode(
413+
node, field, (value) => new PlatformSelector.parse(value, node.span));
414+
}
415+
412416
/// Asserts that [node] is a string, passes its value to [parse], and returns
413417
/// the result.
414418
///
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2017, 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:source_span/source_span.dart';
6+
7+
/// A platform on which the user has chosen to run tests.
8+
class PlatformSelection {
9+
/// The name of the platform.
10+
final String name;
11+
12+
/// The location in the configuration file of this platform string, or `null`
13+
/// if it was defined outside a configuration file (for example, on the
14+
/// command line).
15+
final SourceSpan span;
16+
17+
PlatformSelection(this.name, [this.span]);
18+
19+
bool operator ==(other) => other is PlatformSelection && other.name == name;
20+
21+
int get hashCode => name.hashCode;
22+
}

0 commit comments

Comments
 (0)