Skip to content

Commit 4c82f75

Browse files
authored
Add implied features... (#1598)
Features can now imply or be implied by other features. Implied features are executed as long as at least one implying feature is executed, preventing their removal from the execution plan, through things such as `PostDiscoveryFilters`. Implying features does not affect the execution order in any way.
1 parent 3d7aa63 commit 4c82f75

8 files changed

Lines changed: 396 additions & 14 deletions

File tree

spock-core/src/main/java/org/spockframework/runtime/FeatureNode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ public FeatureNode(UniqueId uniqueId, String displayName, TestSource source, Run
1616
super(uniqueId, displayName, source, configuration, featureInfo);
1717
}
1818

19+
protected FeatureNode(FeatureNode other) {
20+
super(other);
21+
}
22+
1923
@Override
2024
public Type getType() {
2125
return Type.CONTAINER;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.spockframework.runtime;
2+
3+
import org.junit.platform.engine.TestDescriptor;
4+
import org.junit.platform.engine.TestExecutionResult;
5+
import org.spockframework.runtime.model.FeatureInfo;
6+
7+
import java.util.List;
8+
import java.util.Objects;
9+
import java.util.stream.Stream;
10+
11+
import static java.util.stream.Collectors.collectingAndThen;
12+
import static java.util.stream.Collectors.toList;
13+
14+
/**
15+
* A feature that is implied by one or multiple other features within the same specification,
16+
* implies one or multiple other features within the same specification,
17+
* or both.
18+
* The actual test work is delegated to another feature node.
19+
* This node makes sure the node is not removed from hierarchy e.g. due to filtering as long as one implying feature is still present.
20+
*/
21+
public class ImplicationFeatureNode extends FeatureNode {
22+
23+
private final FeatureNode delegate;
24+
25+
private boolean removedFromHierarchy;
26+
27+
public ImplicationFeatureNode(FeatureNode delegate) {
28+
super(delegate);
29+
this.delegate = delegate;
30+
}
31+
32+
@Override
33+
public Type getType() {
34+
return delegate.getType();
35+
}
36+
37+
@Override
38+
public SpockExecutionContext prepare(SpockExecutionContext context) throws Exception {
39+
return delegate.prepare(context);
40+
}
41+
42+
@Override
43+
public SpockExecutionContext before(SpockExecutionContext context) throws Exception {
44+
return delegate.before(context);
45+
}
46+
47+
@Override
48+
public void around(SpockExecutionContext context, Invocation<SpockExecutionContext> invocation) {
49+
delegate.around(context, invocation);
50+
}
51+
52+
@Override
53+
public SpockExecutionContext execute(SpockExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception {
54+
return delegate.execute(context, dynamicTestExecutor);
55+
}
56+
57+
@Override
58+
public void after(SpockExecutionContext context) throws Exception {
59+
delegate.after(context);
60+
}
61+
62+
@Override
63+
public void nodeFinished(SpockExecutionContext context, TestDescriptor testDescriptor, TestExecutionResult result) {
64+
delegate.nodeFinished(context, testDescriptor, result);
65+
}
66+
67+
@Override
68+
public void nodeSkipped(SpockExecutionContext context, TestDescriptor testDescriptor, SkipResult result) {
69+
delegate.nodeSkipped(context, testDescriptor, result);
70+
}
71+
72+
@Override
73+
public SkipResult shouldBeSkipped(SpockExecutionContext context) throws Exception {
74+
return delegate.shouldBeSkipped(context);
75+
}
76+
77+
private boolean allImplyingFeaturesAreRemoved() {
78+
return !findImplicationFeatureNodes(getNodeInfo().getImplyingFeatures())
79+
.findAny()
80+
.isPresent();
81+
}
82+
83+
@Override
84+
public void removeFromHierarchy() {
85+
if (getNodeInfo().getImplyingFeatures().isEmpty() || allImplyingFeaturesAreRemoved()) {
86+
Stream<ImplicationFeatureNode> impliedFeatureNodes = findImplicationFeatureNodes(getNodeInfo().getImpliedFeatures());
87+
super.removeFromHierarchy();
88+
// retry to remove implied features that were just preserved for this implying feature
89+
impliedFeatureNodes
90+
.filter(impliedFeature -> impliedFeature.removedFromHierarchy)
91+
.forEach(TestDescriptor::removeFromHierarchy);
92+
} else {
93+
// do not remove from hierarchy but remember it should have been removed
94+
removedFromHierarchy = true;
95+
}
96+
}
97+
98+
private Stream<ImplicationFeatureNode> findImplicationFeatureNodes(List<FeatureInfo> features) {
99+
SpecNode specNode = getParent()
100+
.map(SpecNode.class::cast)
101+
.orElseThrow(AssertionError::new);
102+
103+
return features
104+
.stream()
105+
.map(featureInfo -> specNode
106+
.getChildren()
107+
.stream()
108+
.map(FeatureNode.class::cast)
109+
.filter(featureNode -> featureNode.getNodeInfo().equals(featureInfo))
110+
.map(ImplicationFeatureNode.class::cast)
111+
.collect(collectingAndThen(toList(), featureNodes -> {
112+
switch (featureNodes.size()) {
113+
case 0:
114+
return null;
115+
116+
case 1:
117+
return featureNodes.get(0);
118+
119+
default:
120+
throw new AssertionError("Expected to find 0 or 1 node, but found multiple");
121+
}
122+
}))
123+
)
124+
.filter(Objects::nonNull);
125+
}
126+
}

spock-core/src/main/java/org/spockframework/runtime/SpockEngineDiscoveryPostProcessor.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,25 @@ SpockEngineDescriptor postProcessEngineDescriptor(UniqueId uniqueId, RunContext
1818
return processedEngineDescriptor;
1919
}
2020

21-
private SpockNode createNode(SpecNode specNode, FeatureInfo feature, RunnerConfiguration configuration) {
21+
private FeatureNode createNode(SpecNode specNode, FeatureInfo feature, RunnerConfiguration configuration) {
22+
FeatureNode result;
2223
if (feature.isParameterized()) {
23-
return describeParameterizedFeature(specNode.getUniqueId(), feature, configuration);
24+
result = describeParameterizedFeature(specNode.getUniqueId(), feature, configuration);
2425
} else {
25-
return describeSimpleFeature(specNode.getUniqueId(), feature, configuration);
26+
result = describeSimpleFeature(specNode.getUniqueId(), feature, configuration);
2627
}
28+
if (!feature.getImplyingFeatures().isEmpty() || !feature.getImpliedFeatures().isEmpty()) {
29+
result = new ImplicationFeatureNode(result);
30+
}
31+
return result;
2732
}
2833

2934
private FeatureNode describeParameterizedFeature(UniqueId parentId, FeatureInfo feature,
3035
RunnerConfiguration configuration) {
3136
return new ParameterizedFeatureNode(toUniqueId(parentId, feature), configuration, feature);
3237
}
3338

34-
private SpockNode describeSimpleFeature(UniqueId parentId, FeatureInfo feature, RunnerConfiguration configuration) {
39+
private FeatureNode describeSimpleFeature(UniqueId parentId, FeatureInfo feature, RunnerConfiguration configuration) {
3540
IterationInfo iterationInfo = new IterationInfo(feature, 0, EMPTY_ARGS, 1);
3641
iterationInfo.setName(feature.getName());
3742
UniqueId uniqueId = toUniqueId(parentId, feature);

spock-core/src/main/java/org/spockframework/runtime/SpockNode.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ protected SpockNode(UniqueId uniqueId, String displayName, TestSource source,
2727
this.nodeInfo = nodeInfo;
2828
}
2929

30+
protected SpockNode(SpockNode<T> other) {
31+
super(other.getUniqueId(), other.getDisplayName(), other.getSource().orElse(null));
32+
this.configuration = other.configuration;
33+
this.nodeInfo = other.nodeInfo;
34+
}
35+
3036
public T getNodeInfo() {
3137
return nodeInfo;
3238
}

spock-core/src/main/java/org/spockframework/runtime/extension/builtin/StepwiseExtension.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ private void includeFeaturesBeforeLastIncludedFeature(SpecInfo spec) {
7171
FeatureInfo feature = features.get(i);
7272
if (includeRemaining) feature.setExcluded(false);
7373
else if (!feature.isExcluded()) includeRemaining = true;
74+
75+
if (i > 0) {
76+
feature.addImpliedFeature(features.get(i - 1));
77+
}
7478
}
7579
}
7680

spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public class FeatureInfo extends SpecElementInfo<SpecInfo, AnnotatedElement> imp
3434
private final List<DataProviderInfo> dataProviders = new ArrayList<>();
3535
private final IterationFilter iterationFilter = new IterationFilter();
3636

37+
private final List<FeatureInfo> implyingFeatures = new ArrayList<>();
38+
39+
private final List<FeatureInfo> impliedFeatures = new ArrayList<>();
40+
3741
private boolean reportIterations = true;
3842

3943
private boolean forceParameterized = false;
@@ -119,6 +123,71 @@ public void addDataProvider(DataProviderInfo dataProvider) {
119123
dataProviders.add(dataProvider);
120124
}
121125

126+
/**
127+
* Returns the features that imply this feature.
128+
* All features are within the same specification hierarchy as this feature.
129+
* If one of the returned features is going to be executed, this feature is also going to be executed,
130+
* even if for example a post discovery filter would have filtered out this feature, like IDEs and build
131+
* tools do if a specific test or a pattern of tests is executed.
132+
*
133+
* <p><b>NOTE:</b> This relationship does not imply any ordering constraint. According to configured
134+
* run order and parallel execution settings the features can run in any order or even
135+
* concurrently.
136+
*
137+
* @return the features that imply this feature
138+
*/
139+
@Beta
140+
public List<FeatureInfo> getImplyingFeatures() {
141+
return implyingFeatures;
142+
}
143+
144+
/**
145+
* Returns the features this feature implies.
146+
* All features are within the same specification hierarchy as this feature.
147+
* If this feature is going to be executed, the returned features are also going to be executed,
148+
* even if for example a post discovery filter would have filtered them out, like IDEs and build
149+
* tools do if a specific test or a pattern of tests is executed.
150+
*
151+
* <p><b>NOTE:</b> This relationship does not imply any ordering constraint. According to configured
152+
* run order and parallel execution settings the features can run in any order or even
153+
* concurrently.
154+
*
155+
* @return the features this feature implies
156+
*/
157+
@Beta
158+
public List<FeatureInfo> getImpliedFeatures() {
159+
return impliedFeatures;
160+
}
161+
162+
/**
163+
* Adds the given feature as implied by this feature.
164+
* The given feature must be within the same specification hierarchy as this feature.
165+
* If this feature is going to be executed, the given feature is also going to be executed,
166+
* even if for example a post discovery filter would have filtered out the given feature,
167+
* like IDEs and build tools do if a specific test or a pattern of tests is executed.
168+
*
169+
* <p><b>NOTE:</b> This relationship does not imply any ordering constraint. According to configured
170+
* run order and parallel execution settings the features can run in any order or even
171+
* concurrently.
172+
*
173+
* @param feature a feature that should be implied by this feature
174+
*/
175+
@Beta
176+
public void addImpliedFeature(FeatureInfo feature) {
177+
if (equals(feature)) {
178+
throw new IllegalArgumentException("Features cannot imply themselves");
179+
}
180+
181+
Class<?> otherClass = feature.getParent().getReflection();
182+
Class<?> clazz = getParent().getReflection();
183+
if (!otherClass.isAssignableFrom(clazz) && !clazz.isAssignableFrom(otherClass)) {
184+
throw new IllegalArgumentException("Features can only imply features within the same specification hierarchy");
185+
}
186+
187+
impliedFeatures.add(feature);
188+
feature.implyingFeatures.add(this);
189+
}
190+
122191
@Override
123192
public void addExclusiveResource(ExclusiveResource exclusiveResource) {
124193
exclusiveResources.add(exclusiveResource);

0 commit comments

Comments
 (0)