Skip to content

Commit 806612a

Browse files
Merge branch 'main' into renovate/jackson-monorepo
2 parents 62fe920 + e36b1a9 commit 806612a

6 files changed

Lines changed: 63 additions & 19 deletions

File tree

src/main/java/de/tum/cit/ase/ares/api/internal/TimeoutUtils.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,20 @@ private static <T> T executeWithTimeout(Duration timeout, Callable<T> action, Te
8585
}
8686
}
8787

88-
private static void terminateTimedOutExecution(Future<?> future, ExecutorService executorService,
88+
static void terminateTimedOutExecution(Future<?> future, ExecutorService executorService,
8989
Duration terminationGracePeriod, IntConsumer fatalProcessTerminator) {
90+
executorService.shutdown();
9091
future.cancel(true);
91-
executorService.shutdownNow();
9292
/*
93-
* Give interruption-aware code time to finish before the owning thread
94-
* continues. If it ignores interruption, the fork is already contaminated:
95-
* returning would let untrusted code outlive its security, IO and reporting
96-
* lifecycle and affect later tests in a reused JVM. Thread.stop() cannot repair
97-
* that safely or reliably, so fail closed by terminating the complete worker
98-
* process and let Maven, Gradle or the IDE report the crashed test fork.
93+
* Future.cancel(true) delivers the single interruption that asks the timed-out
94+
* execution to stop. Calling shutdownNow() here would interrupt the worker a
95+
* second time; that interruption can race with interruption-aware code leaving
96+
* its body. Give it time to finish before the owning thread continues. If it
97+
* ignores interruption, the fork is already contaminated: returning would let
98+
* untrusted code outlive its security, IO and reporting lifecycle and affect
99+
* later tests in a reused JVM. Thread.stop() cannot repair that safely or
100+
* reliably, so fail closed by terminating the complete worker process and let
101+
* Maven, Gradle or the IDE report the crashed test fork.
99102
*/
100103
if (!awaitTermination(executorService, terminationGracePeriod.toMillis())) {
101104
fatalProcessTerminator.accept(UNRESPONSIVE_TIMEOUT_EXIT_CODE);

src/main/java/de/tum/cit/ase/ares/api/jqwik/JqwikContext.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import org.apiguardian.api.API;
77
import org.apiguardian.api.API.Status;
88

9-
import net.jqwik.api.lifecycle.PropertyLifecycleContext;
9+
import net.jqwik.api.lifecycle.*;
1010

1111
import de.tum.cit.ase.ares.api.context.*;
1212

1313
@API(status = Status.INTERNAL)
1414
public class JqwikContext extends TestContext {
15-
private final PropertyLifecycleContext lifecycleContext;
15+
private final MethodLifecycleContext lifecycleContext;
1616

17-
JqwikContext(PropertyLifecycleContext lifecycleContext) {
17+
JqwikContext(MethodLifecycleContext lifecycleContext) {
1818
this.lifecycleContext = lifecycleContext;
1919
}
2020

@@ -44,13 +44,20 @@ public Optional<String> displayName() {
4444
}
4545

4646
public PropertyLifecycleContext getPropertyLifecycleContext() {
47-
return lifecycleContext;
47+
if (lifecycleContext instanceof PropertyLifecycleContext propertyLifecycleContext) {
48+
return propertyLifecycleContext;
49+
}
50+
throw new IllegalStateException("This context represents a jqwik try, not a property"); //$NON-NLS-1$
4851
}
4952

5053
public static JqwikContext of(PropertyLifecycleContext lifecycleContext) {
5154
return new JqwikContext(lifecycleContext);
5255
}
5356

57+
public static JqwikContext of(TryLifecycleContext lifecycleContext) {
58+
return new JqwikContext(lifecycleContext);
59+
}
60+
5461
@Override
5562
public Optional<TestType> findTestType() {
5663
return TestContextUtils.findAnnotationIn(this, JqwikAresTest.class).map(JqwikAresTest::value);

src/main/java/de/tum/cit/ase/ares/api/jqwik/JqwikStrictTimeoutExtension.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package de.tum.cit.ase.ares.api.jqwik;
22

33
import java.time.Duration;
4+
import java.util.List;
45

56
import org.apiguardian.api.API;
67
import org.apiguardian.api.API.Status;
@@ -29,27 +30,28 @@
2930
* @author Christian Femers
3031
*/
3132
@API(status = Status.MAINTAINED)
32-
public class JqwikStrictTimeoutExtension implements AroundPropertyHook {
33+
public class JqwikStrictTimeoutExtension implements AroundTryHook {
3334
private static final Duration TERMINATION_GRACE_PERIOD = Duration.ofSeconds(1);
3435

3536
@Override
36-
public int aroundPropertyProximity() {
37+
public int aroundTryProximity() {
3738
/*
38-
* Keep the timeout inside Ares's security, IO and reporting hooks. Their setup
39-
* and cleanup mutate engine-wide state and must remain on jqwik's owning
40-
* thread; only the actual property execution belongs on the timeout worker.
39+
* Keep the timeout inside jqwik's try lifecycle and Ares's security, IO and
40+
* reporting hooks. Their setup and cleanup mutate engine-wide state and must
41+
* remain on jqwik's owning thread; only one invocation of the property method
42+
* belongs on the timeout worker.
4143
*/
4244
return 40;
4345
}
4446

4547
@Override
46-
public PropertyExecutionResult aroundProperty(PropertyLifecycleContext context, PropertyExecutor property)
48+
public TryExecutionResult aroundTry(TryLifecycleContext context, TryExecutor aTry, List<Object> parameters)
4749
throws Throwable {
4850
DomainContext domainContext = CurrentDomainContext.get();
4951
TestDescriptor desc = CurrentTestDescriptor.get();
5052
return TimeoutUtils.performTimeoutExecution(
5153
() -> CurrentDomainContext.runWithContext(domainContext,
52-
() -> CurrentTestDescriptor.runWithDescriptor(desc, property::execute)),
54+
() -> CurrentTestDescriptor.runWithDescriptor(desc, () -> aTry.execute(parameters))),
5355
JqwikContext.of(context), TERMINATION_GRACE_PERIOD);
5456
}
5557
}

src/test/java/de/tum/cit/ase/ares/api/internal/TimeoutUtilsTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,41 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.fail;
56
import static org.mockito.Mockito.*;
67

78
import java.lang.reflect.Method;
89
import java.nio.file.Path;
10+
import java.time.Duration;
911
import java.util.Optional;
12+
import java.util.concurrent.ExecutorService;
13+
import java.util.concurrent.Future;
1014
import java.util.concurrent.TimeUnit;
1115
import java.util.concurrent.atomic.AtomicBoolean;
1216
import java.util.concurrent.atomic.AtomicInteger;
1317

1418
import org.junit.jupiter.api.Test;
19+
import org.mockito.InOrder;
1520
import org.opentest4j.AssertionFailedError;
1621

1722
import de.tum.cit.ase.ares.api.StrictTimeout;
1823
import de.tum.cit.ase.ares.api.context.TestContext;
1924

2025
class TimeoutUtilsTest {
26+
@Test
27+
void timedOutExecutionUsesOneCancellationInterrupt() {
28+
Future<?> future = mock(Future.class);
29+
ExecutorService executorService = mock(ExecutorService.class);
30+
when(executorService.isTerminated()).thenReturn(true);
31+
32+
TimeoutUtils.terminateTimedOutExecution(future, executorService, Duration.ofSeconds(1),
33+
exitCode -> fail("Interruption-aware execution must not request fatal termination")); //$NON-NLS-1$
34+
35+
InOrder cancellationOrder = inOrder(executorService, future);
36+
cancellationOrder.verify(executorService).shutdown();
37+
cancellationOrder.verify(future).cancel(true);
38+
verify(executorService, never()).shutdownNow();
39+
}
2140

2241
@Test
2342
void interruptionAwareExecutionFinishesBeforeControlReturns() throws Exception {

src/test/java/de/tum/cit/ase/ares/integration/JqwickTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class JqwickTest {
3636
private final String provokeTimeoutSleepExample = "provokeTimeoutSleepExample";
3737
private final String provokeTimeoutSleepProperty = "provokeTimeoutSleepProperty";
3838
private final String provokeTimeoutSleepTries = "provokeTimeoutSleepTries";
39+
private final String strictTimeoutAppliesPerTry = "strictTimeoutAppliesPerTry";
3940
private final String testHiddenIncomplete = "testHiddenIncomplete";
4041
private final String testLocaleDe = "testLocaleDe";
4142
private final String testPublicIncomplete = "testPublicIncomplete";
@@ -125,6 +126,11 @@ void test_provokeTimeoutSleepTries() {
125126
tests.assertThatEvents().haveExactly(1, testFailedWith(provokeTimeoutSleepTries, AssertionFailedError.class));
126127
}
127128

129+
@TestTest
130+
void test_strictTimeoutAppliesPerTry() {
131+
tests.assertThatEvents().haveExactly(1, finishedSuccessfully(strictTimeoutAppliesPerTry));
132+
}
133+
128134
@TestTest
129135
void test_testHiddenIncomplete() {
130136
tests.assertThatEvents().doNotHave(event(test(testHiddenIncomplete)));

src/test/java/de/tum/cit/ase/ares/integration/testuser/JqwickUser.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ void provokeTimeoutSleepTries(@SuppressWarnings("unused") @ForAll @Positive int
138138
sleepUntilInterrupted();
139139
}
140140

141+
@Public
142+
@Property(tries = 5)
143+
@StrictTimeout(value = 250, unit = TimeUnit.MILLISECONDS)
144+
void strictTimeoutAppliesPerTry(@SuppressWarnings("unused") @ForAll int x) throws InterruptedException {
145+
Thread.sleep(100);
146+
}
147+
141148
private void sleepUntilInterrupted() throws InterruptedException {
142149
try {
143150
Thread.sleep(300);

0 commit comments

Comments
 (0)