diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart index 216865b9f..5b5e743ee 100644 --- a/lib/src/backend/declarer.dart +++ b/lib/src/backend/declarer.dart @@ -69,6 +69,9 @@ class Declarer { /// Whether [build] has been called for this declarer. bool _built = false; + /// Whether a solo test or solo group has been encountered. + bool _soloSeen = false; + /// The current zone-scoped declarer. static Declarer get current => Zone.current[#test.declarer]; @@ -88,12 +91,25 @@ class Declarer { declare(body()) => runZoned(body, zoneValues: {#test.declarer: this}); /// Defines a test case with the given name and body. - void test(String name, body(), {String testOn, Timeout timeout, skip, - Map onPlatform, tags}) { + void test(String name, body(), + {String testOn, + Timeout timeout, + skip, + solo, + Map onPlatform, + tags}) { _checkNotBuilt("test"); + if (solo) { + _soloSeen = true; + } + var metadata = _metadata.merge(new Metadata.parse( - testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform, + testOn: testOn, + timeout: timeout, + skip: skip, + solo: solo, + onPlatform: onPlatform, tags: tags)); _entries.add(new LocalTest(_prefix(name), metadata, () async { @@ -110,18 +126,34 @@ class Declarer { } /// Creates a group of tests. - void group(String name, void body(), {String testOn, Timeout timeout, skip, - Map onPlatform, tags}) { + void group(String name, void body(), + {String testOn, + Timeout timeout, + skip, + solo, + Map onPlatform, + tags}) { _checkNotBuilt("group"); + if (solo) { + _soloSeen = true; + } + var metadata = _metadata.merge(new Metadata.parse( - testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform, + testOn: testOn, + timeout: timeout, + skip: skip, + solo: solo, + onPlatform: onPlatform, tags: tags)); var trace = new Trace.current(2); var declarer = new Declarer._(this, _prefix(name), metadata, trace); declarer.declare(body); - _entries.add(declarer.build()); + if (declarer._soloSeen) { + _soloSeen = true; + } + _entries.add(declarer.build(soloSeenInParent: _soloSeen)); } /// Returns [name] prefixed with this declarer's group name. @@ -154,9 +186,13 @@ class Declarer { } /// Finalizes and returns the group being declared. - Group build() { + Group build({bool soloSeenInParent: false}) { _checkNotBuilt("build"); + if (_soloSeen || soloSeenInParent) { + _skipNonSoloEntries(_entries); + } + _built = true; return new Group(_name, _entries.toList(), metadata: _metadata, @@ -165,6 +201,16 @@ class Declarer { tearDownAll: _tearDownAll); } + void _skipNonSoloEntries(List entries) { + entries.where((entry) => !entry.metadata.solo).forEach((entry) { + if (entry is Group) { + _skipNonSoloEntries(entry.entries); + } else { + entry.metadata.skip = true; + } + }); + } + /// Throws a [StateError] if [build] has been called. /// /// [name] should be the name of the method being called. diff --git a/lib/src/backend/metadata.dart b/lib/src/backend/metadata.dart index 77d69e035..95eada750 100644 --- a/lib/src/backend/metadata.dart +++ b/lib/src/backend/metadata.dart @@ -26,7 +26,10 @@ class Metadata { final Timeout timeout; /// Whether the test or suite should be skipped. - final bool skip; + bool skip; + + /// Whether this is a solo test or suite. + bool solo; /// Whether to use verbose stack traces. final bool verboseTrace; @@ -123,14 +126,15 @@ class Metadata { /// included inline in the returned value. The values directly passed to the /// constructor take precedence over tag-specific metadata. factory Metadata({PlatformSelector testOn, Timeout timeout, bool skip: false, - bool verboseTrace: false, String skipReason, Iterable tags, - Map onPlatform, + bool solo: false, bool verboseTrace: false, String skipReason, + Iterable tags, Map onPlatform, Map forTag}) { // Returns metadata without forTag resolved at all. _unresolved() => new Metadata._( testOn: testOn, timeout: timeout, skip: skip, + solo: solo, verboseTrace: verboseTrace, skipReason: skipReason, tags: tags, @@ -160,8 +164,8 @@ class Metadata { /// /// Unlike [new Metadata], this assumes [forTag] is already resolved. Metadata._({PlatformSelector testOn, Timeout timeout, bool skip: false, - this.verboseTrace: false, this.skipReason, Iterable tags, - Map onPlatform, + this.solo: false, this.verboseTrace: false, this.skipReason, + Iterable tags, Map onPlatform, Map forTag}) : testOn = testOn == null ? PlatformSelector.all : testOn, timeout = timeout == null ? const Timeout.factor(1) : timeout, @@ -181,7 +185,7 @@ class Metadata { /// where applicable. /// /// Throws a [FormatException] if any field is invalid. - Metadata.parse({String testOn, Timeout timeout, skip, + Metadata.parse({String testOn, Timeout timeout, skip, solo, this.verboseTrace: false, Map onPlatform, tags}) : testOn = testOn == null @@ -190,6 +194,7 @@ class Metadata { timeout = timeout == null ? const Timeout.factor(1) : timeout, skip = skip != null && skip != false, skipReason = skip is String ? skip : null, + solo = solo != null && solo != false, onPlatform = _parseOnPlatform(onPlatform), tags = _parseTags(tags), forTag = const {} { @@ -209,6 +214,7 @@ class Metadata { timeout = _deserializeTimeout(serialized['timeout']), skip = serialized['skip'], skipReason = serialized['skipReason'], + solo = serialized['solo'], verboseTrace = serialized['verboseTrace'], tags = new Set.from(serialized['tags']), onPlatform = new Map.fromIterable(serialized['onPlatform'], @@ -254,6 +260,7 @@ class Metadata { timeout: timeout.merge(other.timeout), skip: skip || other.skip, skipReason: other.skipReason == null ? skipReason : other.skipReason, + solo: solo || other.solo, verboseTrace: verboseTrace || other.verboseTrace, tags: tags.union(other.tags), onPlatform: mergeMaps(onPlatform, other.onPlatform, @@ -263,16 +270,17 @@ class Metadata { /// Returns a copy of [this] with the given fields changed. Metadata change({PlatformSelector testOn, Timeout timeout, bool skip, - bool verboseTrace, String skipReason, + bool solo, bool verboseTrace, String skipReason, Map onPlatform}) { if (testOn == null) testOn = this.testOn; if (timeout == null) timeout = this.timeout; if (skip == null) skip = this.skip; + if (solo == null) solo = this.solo; if (verboseTrace == null) verboseTrace = this.verboseTrace; if (skipReason == null) skipReason = this.skipReason; if (onPlatform == null) onPlatform = this.onPlatform; return new Metadata(testOn: testOn, timeout: timeout, skip: skip, - verboseTrace: verboseTrace, skipReason: skipReason, + solo: solo, verboseTrace: verboseTrace, skipReason: skipReason, onPlatform: onPlatform); } @@ -303,6 +311,7 @@ class Metadata { 'timeout': _serializeTimeout(timeout), 'skip': skip, 'skipReason': skipReason, + 'solo': solo, 'verboseTrace': verboseTrace, 'tags': tags.toList(), 'onPlatform': serializedOnPlatform, diff --git a/lib/test.dart b/lib/test.dart index 564f9363f..aa0efbaa4 100644 --- a/lib/test.dart +++ b/lib/test.dart @@ -122,12 +122,14 @@ void test(description, body(), {String testOn, Timeout timeout, skip, + solo, tags, Map onPlatform}) { _declarer.test(description.toString(), body, testOn: testOn, timeout: timeout, skip: skip, + solo: solo, onPlatform: onPlatform, tags: tags); @@ -138,6 +140,21 @@ void test(description, body(), return; } +void solo_test(description, body(), + {String testOn, + Timeout timeout, + skip, + tags, + Map onPlatform}) { + test(description.toString(), body, + testOn: testOn, + timeout: timeout, + skip: skip, + solo: true, + onPlatform: onPlatform, + tags: tags); +} + /// Creates a group of tests. /// /// A group's description (converted to a string) is included in the descriptions @@ -189,10 +206,11 @@ void group(description, body(), {String testOn, Timeout timeout, skip, + solo, tags, Map onPlatform}) { _declarer.group(description.toString(), body, - testOn: testOn, timeout: timeout, skip: skip, tags: tags); + testOn: testOn, timeout: timeout, skip: skip, solo: solo, tags: tags); // Force dart2js not to inline this function. We need it to be separate from // `main()` in JS stack traces in order to properly determine the line and @@ -201,6 +219,21 @@ void group(description, body(), return; } +void solo_group(description, body(), + {String testOn, + Timeout timeout, + skip, + tags, + Map onPlatform}) { + group(description, body, + testOn: testOn, + timeout: timeout, + skip: skip, + solo: true, + tags: tags, + onPlatform: onPlatform); +} + /// Registers a function to be run before tests. /// /// This function will be called before each test is run. [callback] may be diff --git a/test/backend/declarer_test.dart b/test/backend/declarer_test.dart index 3dc1bc803..3cbc0ccfb 100644 --- a/test/backend/declarer_test.dart +++ b/test/backend/declarer_test.dart @@ -581,6 +581,224 @@ void main() { }); }); }); + + group(".solo_test()", () { + test("skips all other tests", () { + var tests = declare(() { + test("description 1", () {}); + solo_test("description 2", () {}); + }); + + var notSkippedTests = tests.where((t) => !t.metadata.skip).toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.single.name, equals("description 2")); + }); + + test("does not skip other solo tests", () { + var tests = declare(() { + test("description 1", () {}); + solo_test("description 2", () {}); + test("description 3", () {}); + solo_test("description 4", () {}); + }); + + var notSkippedTests = tests.where((t) => !t.metadata.skip).toList(); + expect(notSkippedTests, hasLength(2)); + expect(notSkippedTests[0].name, equals("description 2")); + expect(notSkippedTests[1].name, equals("description 4")); + }); + + test("skips tests declared alongside containing group", () { + var tests = declare(() { + test("description 1", () {}); + group("group 1", () { + solo_test("description 2", () {}); + }); + test("description 3", () {}); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.single.name, equals("group 1 description 2")); + }); + + test("skips tests declared in groups declared alongside test", () { + var tests = declare(() { + group("group 1", () { + test("description 1", () {}); + }); + solo_test("description 2", () {}); + group("group 2", () { + test("description 3", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.first.name, equals("description 2")); + }); + + test("skips tests declared in groups declared alongside containing group", + () { + var tests = declare(() { + group("group 1", () { + test("description 1", () {}); + }); + group("group 2", () { + solo_test("description 2", () {}); + }); + group("group 3", () { + test("description 3", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.single.name, equals("group 2 description 2")); + }); + }); + + group(".solo_group()", () { + test("skips all other tests", () { + var tests = declare(() { + test("description 1", () {}); + solo_group("group 1", () { + test("description 2", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.single.name, equals("group 1 description 2")); + }); + + test("does not skip other solo groups", () { + var tests = declare(() { + group("group 1", () { + test("description 1", () {}); + }); + solo_group("group 2", () { + test("description 2", () {}); + }); + group("group 3", () { + test("description 3", () {}); + }); + solo_group("group 4", () { + test("description 4", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(2)); + expect(notSkippedTests[0].name, equals("group 2 description 2")); + expect(notSkippedTests[1].name, equals("group 4 description 4")); + }); + + test("skips tests declared in groups declared alongside solo group", () { + var tests = declare(() { + group("group 1", () { + test("description 1", () {}); + }); + solo_group("group 2", () { + test("description 2", () {}); + }); + group("group 3", () { + test("description 3", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect(notSkippedTests.first.name, equals("group 2 description 2")); + }); + + test("skips tests declared in groups declared alongside containing group", + () { + var tests = declare(() { + group("group 1", () { + test("description 1", () {}); + }); + group("group 2", () { + solo_group("group 3", () { + test("description 2", () {}); + }); + }); + group("group 4", () { + test("description 3", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(1)); + expect( + notSkippedTests.single.name, equals("group 2 group 3 description 2")); + }); + + test("does not skip tests in other solo groups", () { + var tests = declare(() { + solo_group("group 1", () { + test("description 1", () {}); + }); + group("group 2", () { + test("description 2", () {}); + solo_group("group 3", () { + test("description 3", () {}); + }); + solo_group("group 4", () { + test("description 4", () {}); + solo_group("group 5", () { + test("description 5", () {}); + }); + }); + }); + group("group 6", () { + test("description 6", () {}); + }); + }); + + var notSkippedTests = tests + .fold([], _flattenEntries) + .where((e) => !e.metadata.skip) + .toList(); + expect(notSkippedTests, hasLength(4)); + expect(notSkippedTests[0].name, equals("group 1 description 1")); + expect(notSkippedTests[1].name, equals("group 2 group 3 description 3")); + expect(notSkippedTests[2].name, equals("group 2 group 4 description 4")); + expect(notSkippedTests[3].name, + equals("group 2 group 4 group 5 description 5")); + }); + }); +} + +List _flattenEntries(List memo, dynamic entry) { + var result = new List.from(memo); + if (entry is Group) { + result.addAll(entry.entries.fold([], _flattenEntries)); + } else { + result.add(entry); + } + return result; } /// Runs [test].