Skip to content

Commit 38cd14d

Browse files
authored
[JUnit] Fix swallowed exception (#2714)
1 parent 70da332 commit 38cd14d

File tree

7 files changed

+128
-119
lines changed

7 files changed

+128
-119
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Fixed
2020
- [Pico] Improve performance ([#2724](https://github.com/cucumber/cucumber-jvm/issues/2724) Julien Kronegg)
21+
- [JUnit 4] Fix swallowed exception ([#2714](https://github.com/cucumber/cucumber-jvm/issues/2714) M.P. Korstanje)
2122

2223
## [7.11.2] - 2023-03-23
2324
### Fixed

cucumber-core/src/main/java/io/cucumber/core/runtime/CucumberExecutionContext.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import static io.cucumber.cienvironment.DetectCiEnvironment.detectCiEnvironment;
2727
import static io.cucumber.core.exception.ExceptionUtils.printStackTrace;
28+
import static io.cucumber.core.exception.ExceptionUtils.throwAsUncheckedException;
29+
import static io.cucumber.core.exception.UnrecoverableExceptions.rethrowIfUnrecoverable;
2830
import static io.cucumber.messages.Convertor.toMessage;
2931
import static java.util.Collections.singletonList;
3032

@@ -46,6 +48,11 @@ public CucumberExecutionContext(EventBus bus, ExitStatus exitStatus, RunnerSuppl
4648
this.runnerSupplier = runnerSupplier;
4749
}
4850

51+
@FunctionalInterface
52+
public interface ThrowingRunnable {
53+
void run() throws Throwable;
54+
}
55+
4956
public void startTestRun() {
5057
emitMeta();
5158
emitTestRunStarted();
@@ -134,4 +141,30 @@ private Runner getRunner() {
134141
return collector.executeAndThrow(runnerSupplier::get);
135142
}
136143

144+
public void runFeatures(ThrowingRunnable executeFeatures) {
145+
startTestRun();
146+
execute(() -> {
147+
runBeforeAllHooks();
148+
executeFeatures.run();
149+
});
150+
try {
151+
execute(this::runAfterAllHooks);
152+
} finally {
153+
finishTestRun();
154+
}
155+
Throwable throwable = getThrowable();
156+
if (throwable != null) {
157+
throwAsUncheckedException(throwable);
158+
}
159+
}
160+
161+
private void execute(ThrowingRunnable runnable) {
162+
try {
163+
runnable.run();
164+
} catch (Throwable t) {
165+
// Collected in CucumberExecutionContext
166+
rethrowIfUnrecoverable(t);
167+
}
168+
}
169+
137170
}

cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import java.util.function.Predicate;
3030
import java.util.function.Supplier;
3131

32-
import static io.cucumber.core.exception.ExceptionUtils.throwAsUncheckedException;
33-
import static io.cucumber.core.exception.UnrecoverableExceptions.rethrowIfUnrecoverable;
3432
import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
3533
import static java.util.Collections.emptyList;
3634
import static java.util.stream.Collectors.collectingAndThen;
@@ -77,29 +75,7 @@ public static Builder builder() {
7775
public void run() {
7876
// Parse the features early. Don't proceed when there are lexer errors
7977
List<Feature> features = featureSupplier.get();
80-
context.startTestRun();
81-
execute(() -> {
82-
context.runBeforeAllHooks();
83-
runFeatures(features);
84-
});
85-
try {
86-
execute(context::runAfterAllHooks);
87-
} finally {
88-
context.finishTestRun();
89-
}
90-
Throwable exception = context.getThrowable();
91-
if (exception != null) {
92-
throwAsUncheckedException(exception);
93-
}
94-
}
95-
96-
private void execute(Runnable runnable) {
97-
try {
98-
runnable.run();
99-
} catch (Throwable t) {
100-
// Collected in CucumberExecutionContext
101-
rethrowIfUnrecoverable(t);
102-
}
78+
context.runFeatures(() -> runFeatures(features));
10379
}
10480

10581
private void runFeatures(List<Feature> features) {

cucumber-core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package io.cucumber.core.runtime;
22

3+
import io.cucumber.core.backend.CucumberBackendException;
34
import io.cucumber.core.backend.Glue;
45
import io.cucumber.core.backend.HookDefinition;
56
import io.cucumber.core.backend.ParameterInfo;
67
import io.cucumber.core.backend.ScenarioScoped;
8+
import io.cucumber.core.backend.StaticHookDefinition;
79
import io.cucumber.core.backend.StubStepDefinition;
810
import io.cucumber.core.backend.TestCaseState;
911
import io.cucumber.core.eventbus.EventBus;
1012
import io.cucumber.core.exception.CompositeCucumberException;
13+
import io.cucumber.core.exception.CucumberException;
1114
import io.cucumber.core.feature.TestFeatureParser;
1215
import io.cucumber.core.gherkin.Feature;
1316
import io.cucumber.core.gherkin.FeatureParserException;
@@ -31,7 +34,6 @@
3134
import io.cucumber.plugin.event.TestRunStarted;
3235
import io.cucumber.plugin.event.TestStepFinished;
3336
import io.cucumber.plugin.event.TestStepStarted;
34-
import org.hamcrest.CoreMatchers;
3537
import org.junit.jupiter.api.Test;
3638
import org.junit.jupiter.api.function.Executable;
3739
import org.mockito.ArgumentCaptor;
@@ -401,6 +403,30 @@ void should_fail_on_event_listener_exception_at_test_run_finished() {
401403
assertThat(actualThrown, equalTo(expectedException));
402404
}
403405

406+
@Test
407+
void should_fail_on_exception_invoking_after_all_hook() {
408+
RuntimeException expectedException = new RuntimeException("This exception is expected");
409+
CucumberBackendException backendException = new CucumberBackendException("failed", expectedException);
410+
MockedStaticHookDefinition mockedStaticHookDefinition = new MockedStaticHookDefinition(() -> {
411+
throw backendException;
412+
});
413+
414+
BackendSupplier backendSupplier = new TestBackendSupplier() {
415+
@Override
416+
public void loadGlue(Glue glue, List<URI> gluePaths) {
417+
glue.addAfterAllHook(mockedStaticHookDefinition);
418+
}
419+
};
420+
421+
Executable testMethod = () -> Runtime.builder()
422+
.withFeatureSupplier(new StubFeatureSupplier())
423+
.withBackendSupplier(backendSupplier)
424+
.build()
425+
.run();
426+
CucumberException actualThrown = assertThrows(CucumberException.class, testMethod);
427+
assertThat(actualThrown.getCause(), equalTo(backendException));
428+
}
429+
404430
@Test
405431
void should_interrupt_waiting_plugins() throws InterruptedException {
406432
final Feature feature1 = TestFeatureParser.parse("path/test.feature", "" +
@@ -574,6 +600,35 @@ public String getLocation() {
574600

575601
}
576602

603+
private static class MockedStaticHookDefinition implements StaticHookDefinition {
604+
605+
private final Runnable runnable;
606+
607+
private MockedStaticHookDefinition(Runnable runnable) {
608+
this.runnable = runnable;
609+
}
610+
611+
@Override
612+
public void execute() {
613+
runnable.run();
614+
}
615+
616+
@Override
617+
public int getOrder() {
618+
return 0;
619+
}
620+
621+
@Override
622+
public boolean isDefinedAt(StackTraceElement stackTraceElement) {
623+
return false;
624+
}
625+
626+
@Override
627+
public String getLocation() {
628+
return "mocked hook definition definition";
629+
}
630+
}
631+
577632
private static class FormatterSpy implements EventListener {
578633

579634
private final StringBuilder calls = new StringBuilder();

cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.junit.platform.engine.support.hierarchical.Node;
77

88
import java.util.Optional;
9+
import java.util.function.Consumer;
910

1011
class CucumberEngineDescriptor extends EngineDescriptor implements Node<CucumberEngineExecutionContext> {
1112

@@ -15,53 +16,55 @@ class CucumberEngineDescriptor extends EngineDescriptor implements Node<Cucumber
1516
super(uniqueId, "Cucumber");
1617
}
1718

18-
private static void recursivelyMerge(TestDescriptor descriptor, TestDescriptor parent) {
19-
Optional<? extends TestDescriptor> byUniqueId = parent.findByUniqueId(descriptor.getUniqueId());
20-
if (!byUniqueId.isPresent()) {
21-
parent.addChild(descriptor);
22-
} else {
23-
byUniqueId.ifPresent(
24-
existingParent -> descriptor.getChildren()
25-
.forEach(child -> recursivelyMerge(child, existingParent)));
26-
}
27-
}
28-
2919
@Override
3020
public CucumberEngineExecutionContext prepare(CucumberEngineExecutionContext context) {
31-
if (getChildren().isEmpty()) {
32-
return context;
33-
}
34-
context.startTestRun();
35-
return context;
21+
return ifChildren(context, CucumberEngineExecutionContext::startTestRun);
3622
}
3723

3824
@Override
3925
public CucumberEngineExecutionContext before(CucumberEngineExecutionContext context) {
40-
if (getChildren().isEmpty()) {
41-
return context;
42-
}
43-
context.runBeforeAllHooks();
44-
return context;
26+
return ifChildren(context, CucumberEngineExecutionContext::runBeforeAllHooks);
4527
}
4628

4729
@Override
4830
public void after(CucumberEngineExecutionContext context) {
49-
if (getChildren().isEmpty()) {
50-
return;
51-
}
52-
context.runAfterAllHooks();
31+
ifChildren(context, CucumberEngineExecutionContext::runAfterAllHooks);
5332
}
5433

5534
@Override
5635
public void cleanUp(CucumberEngineExecutionContext context) {
57-
if (getChildren().isEmpty()) {
58-
return;
36+
ifChildren(context, CucumberEngineExecutionContext::finishTestRun);
37+
}
38+
39+
/*
40+
* Problem: The JUnit Platform will always execute all engines that
41+
* participated in discovery. In combination with the JUnit Platform Suite
42+
* Engine this may result in CucumberEngine being executed multiple times.
43+
* To ensure Cucumber only performs works if/when there are tests to run we
44+
* don't do anything unless there are tests. I.e. only when this test
45+
* descriptor has children.
46+
*/
47+
private CucumberEngineExecutionContext ifChildren(
48+
CucumberEngineExecutionContext context, Consumer<CucumberEngineExecutionContext> action
49+
) {
50+
if (!getChildren().isEmpty()) {
51+
action.accept(context);
5952
}
60-
context.finishTestRun();
53+
return context;
6154
}
6255

6356
void mergeFeature(FeatureDescriptor descriptor) {
6457
recursivelyMerge(descriptor, this);
6558
}
6659

60+
private static void recursivelyMerge(TestDescriptor descriptor, TestDescriptor parent) {
61+
Optional<? extends TestDescriptor> byUniqueId = parent.findByUniqueId(descriptor.getUniqueId());
62+
if (!byUniqueId.isPresent()) {
63+
parent.addChild(descriptor);
64+
} else {
65+
byUniqueId.ifPresent(
66+
existingParent -> descriptor.getChildren()
67+
.forEach(child -> recursivelyMerge(child, existingParent)));
68+
}
69+
}
6770
}

cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void startTestRun() {
7676
// Problem: The JUnit Platform will always execute all engines that
7777
// participated in discovery. In combination with the JUnit Platform
7878
// Suite Engine this may result in CucumberEngine being executed
79-
// twice.
79+
// multiple times.
8080
//
8181
// One of these instances may not have discovered any tests and would
8282
// write empty reports. Therefor we do not invoke 'startTestRun' if

cucumber-junit/src/main/java/io/cucumber/junit/Cucumber.java

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,7 @@ protected void runChild(ParentRunner<?> child, RunNotifier notifier) {
199199
@Override
200200
protected Statement childrenInvoker(RunNotifier notifier) {
201201
Statement statement = super.childrenInvoker(notifier);
202-
203-
statement = new RunBeforeAllHooks(statement);
204-
statement = new RunAfterAllHooks(statement);
205-
206-
statement = new StartTestRun(statement);
207-
statement = new FinishTestRun(statement);
208-
202+
statement = new StartAndFinishTestRun(statement);
209203
return statement;
210204
}
211205

@@ -215,75 +209,22 @@ public void setScheduler(RunnerScheduler scheduler) {
215209
multiThreadingAssumed = true;
216210
}
217211

218-
private class StartTestRun extends Statement {
212+
private class StartAndFinishTestRun extends Statement {
219213
private final Statement next;
220214

221-
public StartTestRun(Statement next) {
215+
public StartAndFinishTestRun(Statement next) {
222216
this.next = next;
223217
}
224218

225219
@Override
226-
public void evaluate() throws Throwable {
220+
public void evaluate() {
227221
if (multiThreadingAssumed) {
228222
plugins.setSerialEventBusOnEventListenerPlugins(bus);
229223
} else {
230224
plugins.setEventBusOnEventListenerPlugins(bus);
231225
}
232-
context.startTestRun();
233-
next.evaluate();
234-
}
235-
236-
}
237-
238-
private class FinishTestRun extends Statement {
239-
private final Statement next;
240-
241-
public FinishTestRun(Statement next) {
242-
this.next = next;
243-
}
244-
245-
@Override
246-
public void evaluate() throws Throwable {
247-
try {
248-
next.evaluate();
249-
} finally {
250-
context.finishTestRun();
251-
}
252-
}
253-
254-
}
255-
256-
private class RunBeforeAllHooks extends Statement {
257-
private final Statement next;
258-
259-
public RunBeforeAllHooks(Statement next) {
260-
this.next = next;
261-
}
262-
263-
@Override
264-
public void evaluate() throws Throwable {
265-
context.runBeforeAllHooks();
266-
next.evaluate();
267-
}
268-
269-
}
270-
271-
private class RunAfterAllHooks extends Statement {
272-
private final Statement next;
273-
274-
public RunAfterAllHooks(Statement next) {
275-
this.next = next;
276-
}
277-
278-
@Override
279-
public void evaluate() throws Throwable {
280-
try {
281-
next.evaluate();
282-
} finally {
283-
context.runAfterAllHooks();
284-
}
226+
context.runFeatures(next::evaluate);
285227
}
286-
287228
}
288229

289230
}

0 commit comments

Comments
 (0)