Skip to content

Commit 59a159d

Browse files
authored
Merge pull request greghaskins#119 from ashleyfrieze/Timeout
Spectrum to allow spec timeout to be specified throughout the hierarchy
2 parents 92e0d36 + 27822de commit 59a159d

22 files changed

+490
-79
lines changed

docs/Configuration.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
## Configuration
2+
3+
Metadata to influence the execution can be added anywhere in the test hierarchy. Wherever you can add a block containing suites, tests, or a test body, you can also tag that block with configuration to apply from that point in the hierarchy downwards.
4+
5+
The common use case for this would be selective running, where tagging with `ignore` or `tags` would control which parts of the hierarchy are executed in a given test run. See [selective running](FocusingAndIgnoring.md) for more information.
6+
7+
### Adding configuration to blocks
8+
9+
The basic format is to take the block inside a `describe` or `it` block (and others) and wrap it inside a `with`. E.g.
10+
11+
```java
12+
describe("Some parent", () -> {
13+
it("tests something", () -> {});
14+
});
15+
16+
// might be tagged at parent level and become
17+
describe("Some parent", with(focus(), () -> {
18+
it("tests something", () -> {});
19+
}));
20+
```
21+
22+
Configurations can be chained by using the `and` function, for example this spec has both tags and a timeout:
23+
24+
```java
25+
describe("A suite", with(tags("someTag").and(timeout(10, TimeUnit.SECONDS)), () -> {
26+
it("will test something in time", () -> {
27+
...
28+
});
29+
}));
30+
```
31+
32+
`and` can be chained further:
33+
34+
```java
35+
describe("A suite", with(
36+
tags("someTag")
37+
.and(timeout(ofSeconds(10)))
38+
.and(focus()),
39+
() -> {
40+
...
41+
}
42+
));
43+
```
44+
45+
### Scope of configuration
46+
47+
A configuration applies to the current node in the hierarchy and its children. With `ignore`, the effect of ignoring a parent is to ignore all children. With `tag`, the tagging is cumulative. With `timeout` the timeout settings in a parent propagate down to all descendants but can be superseded by a child's own configuration.
48+
49+
The general rule is that the configuration applies to the whole hierarchy and can only be added to or superseded by children that are allowed to execute.
50+
51+
### Configurations available
52+
53+
In addition to:
54+
55+
- `ignore()` - ignore this test
56+
- `ignore(String reason)` - ignore this test with a reason
57+
- `focus()` - run focused specs only, especially this
58+
- `tags(String ... tags)` - tag the spec with labels
59+
60+
there is also:
61+
62+
- `timeout(Duration timeout)` - make the test fail if it takes too long - see [Timeout](Timeout.md)

docs/FocusingAndIgnoring.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,18 @@ describe("Ignored specs", () -> {
100100
```
101101
#### Ignoring Specs Spectrum style
102102

103-
Spectrum allows you to specify preconditions on a block. These preconditions can include tagging.
104-
The `with(BlockConfigurationChain,Block)` function is used to annotate a block with preconditions. Preconditions include:
103+
Spectrum allows you to specify preconditions for execution as part of the configuration of a block. These can include tagging.
104+
The `with(BlockConfigurationChain,Block)` function is used to annotate a block, for example:
105+
106+
```java
107+
describe("an ignored suite", with(ignore(), () -> {
108+
...
109+
}));
110+
```
111+
112+
For the full capabilities of adding configuration to blocks see [Configuration](Configuration.md).
113+
114+
Available tagging for ignoring:
105115

106116
- `ignore()` - ignore this test
107117
- `ignore(String reason)` - ignore this test and provide a reason to the reader

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Spectrum also supports:
1515
- Rigorous error handling and reporting when something unexpected goes wrong
1616
- Compatibility with most existing JUnit tools; no configuration required
1717
- Plugging in familiar JUnit-friendly libraries like `MockitoJUnit` or `SpringJUnit` [via JUnit `@Rule`s handling](JunitRules.md).
18-
- Tagging specs for [selective running](FocusingAndIgnoring.md)
18+
- Tagging specs for [selective running](FocusingAndIgnoring.md) or adding [configuration](Configuration.md) including [timeouts](Timeout.md)
1919
- Mixing Spectrum tests and normal JUnit tests in the same project suite
2020
- RSpec-style `aroundEach` and `aroundAll` hooks for advanced users and plugin authors
2121

@@ -25,4 +25,4 @@ Unlike some BDD-style frameworks, Spectrum is _only_ a test runner. Assertions,
2525

2626
## Getting Started
2727

28-
See the [quickstart waklthrough](QuickstartWalkthrough.md) and the docs for writing [Specification-style](SpecificationDSL.md) or [Gherkin-style](GherkinDSL.md) tests.
28+
See the [quickstart walkthrough](QuickstartWalkthrough.md) and the docs for writing [Specification-style](SpecificationDSL.md) or [Gherkin-style](GherkinDSL.md) tests.

docs/Timeout.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Test Timeouts
2+
3+
## JUnit Timeouts
4+
5+
In JUnit, you usually specify test timeout in the `@Test` annotation:
6+
7+
```java
8+
@Test(timeout=123)
9+
public void myTest() {
10+
...
11+
}
12+
13+
@Test(timeout=123)
14+
public void myOtherTest() {
15+
...
16+
}
17+
```
18+
19+
There is also a `Timeout` rule - [see here](https://github.com/junit-team/junit4/wiki/timeout-for-tests) for more information.
20+
21+
## Timeouts in Spectrum
22+
23+
Spectrum's timeout can be applied at the level of each _leaf node_ in the hierachy like the above:
24+
25+
```java
26+
describe("some suite", () -> {
27+
it("does one thing under timeout", with(timeout(ofMillis(123)), () -> {
28+
...
29+
}));
30+
31+
it("does another under timeout", with(timeout(ofMillis(123)), () -> {
32+
...
33+
}));
34+
35+
});
36+
```
37+
The timeout comes inside the configuration block using the `with` syntax. The duration of the timeout is a Java 8 `Duration` object, constructed
38+
(as in the above example) using a static method from the `Duration` class like `ofMillis` or `ofSeconds`.
39+
40+
The major difference between JUnit and Spectrum timeouts is the ability to apply timeout rules for the family of tests inside a `describe` or `context`:
41+
42+
```java
43+
// NOTE: the timeout applies to each test, not the sum of all
44+
describe("some suite", with(timeout(ofMillis(123)), () -> {
45+
it("does one thing under timeout", () -> {
46+
...
47+
});
48+
49+
it("does another under timeout", () -> {
50+
...
51+
});
52+
53+
}));
54+
```
55+
56+
> See also [Configuration](Configuration.md)

src/main/java/com/greghaskins/spectrum/Configure.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import com.greghaskins.spectrum.internal.configuration.BlockFocused;
55
import com.greghaskins.spectrum.internal.configuration.BlockIgnore;
66
import com.greghaskins.spectrum.internal.configuration.BlockTagging;
7+
import com.greghaskins.spectrum.internal.configuration.BlockTimeout;
78
import com.greghaskins.spectrum.internal.configuration.ConfiguredBlock;
89
import com.greghaskins.spectrum.internal.configuration.ExcludeTags;
910
import com.greghaskins.spectrum.internal.configuration.IncludeTags;
1011
import com.greghaskins.spectrum.internal.junit.Rules;
1112

13+
import java.time.Duration;
14+
import java.util.concurrent.TimeUnit;
1215
import java.util.function.Supplier;
1316

1417
public interface Configure {
@@ -18,8 +21,8 @@ public interface Configure {
1821

1922
/**
2023
* Surround a {@link Block} with the {@code with} statement to add
21-
* preconditions and metadata to it. E.g. <code>with(tags("foo"), () -&gt; {})</code>.<br>
22-
* Note: preconditions and metadata can be chained using the
24+
* configuration and metadata to it. E.g. <code>with(tags("foo"), () -&gt; {})</code>.<br>
25+
* Note: configuration metadata can be chained using the
2326
* {@link BlockConfigurationChain#and(BlockConfigurationChain)} method. E.g.
2427
* <code>with(tags("foo").and(ignore()), () -&gt; {})</code>
2528
*
@@ -30,6 +33,7 @@ public interface Configure {
3033
* @see #ignore()
3134
* @see #focus()
3235
* @see #tags(String...)
36+
* @see #timeout(Duration)
3337
*/
3438
static Block with(final BlockConfigurationChain configuration, final Block block) {
3539
return ConfiguredBlock.with(configuration.getBlockConfiguration(), block);
@@ -95,6 +99,16 @@ static BlockConfigurationChain focus() {
9599
return new BlockConfigurationChain().with(new BlockFocused());
96100
}
97101

102+
/**
103+
* Apply timeout to all leaf nodes from this level down. Can be superseded by a lower level having its
104+
* own timeout.
105+
* @param timeout the amount of the timeout
106+
* @return a chainable configuration that will apply a timeout to all leaf nodes below
107+
*/
108+
static BlockConfigurationChain timeout(Duration timeout) {
109+
return new BlockConfigurationChain().with(new BlockTimeout(timeout));
110+
}
111+
98112
/**
99113
* Filter which tests in the current suite will run.
100114
*

src/main/java/com/greghaskins/spectrum/dsl/specification/Specification.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ static void describe(final String context, final Block block) {
3434
final Suite suite = DeclarationState.instance()
3535
.getCurrentSuiteBeingDeclared()
3636
.addSuite(context);
37-
suite.applyPreconditions(block);
37+
suite.applyConfigurationFromBlock(block);
3838
DeclarationState.instance().beginDeclaration(suite, block);
3939
}
4040

src/main/java/com/greghaskins/spectrum/internal/Child.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.greghaskins.spectrum.Block;
44
import com.greghaskins.spectrum.internal.configuration.ConfiguredBlock;
55
import com.greghaskins.spectrum.internal.configuration.TaggingFilterCriteria;
6+
import com.greghaskins.spectrum.internal.hooks.Hook;
67

78
import org.junit.runner.Description;
89
import org.junit.runner.notification.Failure;
@@ -40,19 +41,4 @@ default boolean isAtomic() {
4041
default boolean isLeaf() {
4142
return false;
4243
}
43-
44-
/**
45-
* Gets the object to be filtered appropriately with its preconditions.
46-
* @param block the block that will be executed by the child - this may be of
47-
* type {@link ConfiguredBlock}.
48-
* @param taggingFilterCriteria the tagging state in the parent of this suite or spec.
49-
* This is used to determine what filters apply to the block
50-
* @return the child itself for fluent calling
51-
*/
52-
default Child applyPreconditions(final Block block,
53-
final TaggingFilterCriteria taggingFilterCriteria) {
54-
ConfiguredBlock.findApplicablePreconditions(block).applyTo(this, taggingFilterCriteria);
55-
56-
return this;
57-
}
5844
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.greghaskins.spectrum.internal;
2+
3+
import com.greghaskins.spectrum.internal.hooks.HookContext;
4+
import com.greghaskins.spectrum.internal.hooks.NonReportingHook;
5+
6+
/**
7+
* Interface for a child that is also a leaf node in the test hierarchy.
8+
*/
9+
public interface LeafChild extends Child {
10+
/**
11+
* Add an additional hook directly to a leaf of the hierarchy.
12+
* @param leafHook hook to add
13+
* @param precedence precedence, for sorting hooks into order
14+
*/
15+
void addLeafHook(NonReportingHook leafHook, HookContext.Precedence precedence);
16+
}

src/main/java/com/greghaskins/spectrum/internal/Spec.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
package com.greghaskins.spectrum.internal;
22

3-
import com.greghaskins.spectrum.internal.blocks.NotifyingBlock;
3+
4+
import com.greghaskins.spectrum.Block;
5+
import com.greghaskins.spectrum.internal.hooks.Hook;
6+
import com.greghaskins.spectrum.internal.hooks.HookContext;
7+
import com.greghaskins.spectrum.internal.hooks.Hooks;
8+
import com.greghaskins.spectrum.internal.hooks.NonReportingHook;
49

510
import org.junit.runner.Description;
611
import org.junit.runner.notification.Failure;
712

8-
final class Spec implements Child {
13+
final class Spec implements LeafChild {
914

10-
private final NotifyingBlock block;
15+
private final Block block;
1116
private final Description description;
1217
private final Parent parent;
1318
private boolean ignored = false;
19+
private Hooks leafHooks = new Hooks();
1420

15-
Spec(final Description description, final NotifyingBlock block, final Parent parent) {
21+
Spec(final Description description, final Block block, final Parent parent) {
1622
this.description = description;
1723
this.block = block;
1824
this.parent = parent;
@@ -31,8 +37,8 @@ public void run(final RunReporting<Description, Failure> notifier) {
3137
return;
3238
}
3339

34-
this.block.run(this.description, notifier);
35-
40+
// apply leaf hooks around the inner block
41+
leafHooks.sorted().runAround(this.description, notifier, block);
3642
}
3743

3844
@Override
@@ -68,4 +74,10 @@ public boolean isLeaf() {
6874
public boolean isEffectivelyIgnored() {
6975
return ignored;
7076
}
77+
78+
@Override
79+
public void addLeafHook(NonReportingHook leafHook, HookContext.Precedence precedence) {
80+
// hooks at this level are always at the same point in the hierarchy and applying to each child
81+
leafHooks.add(new HookContext(leafHook, 0, HookContext.AppliesTo.EACH_CHILD, precedence));
82+
}
7183
}

src/main/java/com/greghaskins/spectrum/internal/Suite.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.greghaskins.spectrum.internal;
22

3+
import static com.greghaskins.spectrum.internal.configuration.BlockConfiguration.merge;
4+
35
import com.greghaskins.spectrum.Block;
4-
import com.greghaskins.spectrum.internal.blocks.NotifyingBlock;
56
import com.greghaskins.spectrum.internal.configuration.BlockConfiguration;
67
import com.greghaskins.spectrum.internal.configuration.ConfiguredBlock;
78
import com.greghaskins.spectrum.internal.configuration.TaggingFilterCriteria;
@@ -30,7 +31,7 @@ public class Suite implements Parent, Child {
3031
private boolean ignored;
3132

3233
private final TaggingFilterCriteria tagging;
33-
private BlockConfiguration preconditions = BlockConfiguration.defaultConfiguration();
34+
private BlockConfiguration configuration = BlockConfiguration.defaultConfiguration();
3435
private NameSanitiser nameSanitiser = new NameSanitiser();
3536

3637
/**
@@ -70,9 +71,10 @@ public Suite addSuite(final String name) {
7071
}
7172

7273
private Suite addSuite(final String name, final ChildRunner childRunner) {
73-
final Suite suite =
74-
new Suite(Description.createSuiteDescription(sanitise(name)), this, childRunner,
75-
this.tagging.clone());
74+
final Suite suite = new Suite(Description.createSuiteDescription(sanitise(name)), this, childRunner,
75+
this.tagging.clone());
76+
77+
suite.inheritConfigurationFromParent(configuration.forChild());
7678

7779
return addedToThis(suite);
7880
}
@@ -102,13 +104,23 @@ private Child createSpec(final String name, final Block block) {
102104
final Description specDescription =
103105
Description.createTestDescription(this.description.getClassName(), sanitise(name));
104106

105-
final NotifyingBlock specBlockInContext = NotifyingBlock.wrap(block);
107+
return configuredChild(new Spec(specDescription, block, this), block);
108+
}
106109

107-
ConfiguredBlock configuredBlock =
108-
ConfiguredBlock.with(this.preconditions.forChild(), block);
110+
private void inheritConfigurationFromParent(final BlockConfiguration fromParent) {
111+
configuration = merge(fromParent, configuration);
112+
}
109113

110-
return new Spec(specDescription, specBlockInContext, this).applyPreconditions(configuredBlock,
111-
this.tagging);
114+
private Child configuredChild(final Child child, final Block block) {
115+
merge(this.configuration.forChild(), ConfiguredBlock.configurationFromBlock(block))
116+
.applyTo(child, this.tagging);
117+
118+
return child;
119+
}
120+
121+
public void applyConfigurationFromBlock(Block block) {
122+
this.configuration = merge(this.configuration, ConfiguredBlock.configurationFromBlock(block));
123+
this.configuration.applyTo(this, this.tagging);
112124
}
113125

114126
private void addChild(final Child child) {
@@ -158,11 +170,6 @@ public void excludeTags(final String... tags) {
158170
this.tagging.exclude(tags);
159171
}
160172

161-
public void applyPreconditions(Block block) {
162-
this.preconditions = ConfiguredBlock.findApplicablePreconditions(block);
163-
applyPreconditions(block, this.tagging);
164-
}
165-
166173
@Override
167174
public void focus(final Child child) {
168175
this.focusedChildren.add(child);

0 commit comments

Comments
 (0)