This is an automated email from the ASF dual-hosted git repository. ifesdjeen pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/cassandra-simulator.git
commit 3f0abfbff5e8b4f15a90f94d357c65a4f216eac6 Author: Alex Petrov <[email protected]> AuthorDate: Fri Jul 24 14:00:19 2026 +0000 Use instrumented AQS for JDK CountDownLatch --- README.md | 10 +- .../simulator_test/SchedulerIntegrationTest.java | 19 +-- .../org/apache/cassandra/simulator/Simulator.java | 10 +- .../cassandra/simulator/step/ObservableAction.java | 24 ++++ .../apache/cassandra/simulator/step/Session.java | 22 +-- .../simulator_test/CountDownLatchSimTest.java | 160 +++++++++++++++------ .../simulator_test/InstrumentedPrimitivesTest.java | 12 +- .../cassandra/simulator_test/ParkUnparkTest.java | 25 ++-- 8 files changed, 187 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 0b81abd..5201270 100644 --- a/README.md +++ b/README.md @@ -151,13 +151,17 @@ package matches the `GLOBAL_METHODS` or `MONITORS` patterns: |---|---| | `new Thread(...).start()` | `InterceptorOfGlobalMethods$Global.startThread()` — scheduled as a simulator action | -### JDK synchronizers (constructor redirect) +### JDK synchronizers and constructor redirects -Constructors are rewritten at call sites so the simulator's subclass is instantiated instead: +JDK synchronizers built on `AbstractQueuedSynchronizer`, including `CountDownLatch`, retain their +JDK implementations. The agent instruments AQS calls to `LockSupport.park/unpark`, bringing their +blocking and wakeup behavior under scheduler control. A caller can opt into a specialized latch +with `sim.intercept(CountDownLatch.class, Replacement.class)`. + +The remaining default constructor redirects are: | Original | Simulator replacement | |---|---| -| `new CountDownLatch(n)` | `InterceptingCountDownLatch` | | `new ConcurrentHashMap(...)` | `InterceptibleConcurrentHashMap` (deterministic hash codes) | | `new IdentityHashMap(...)` | `InterceptedIdentityHashMap` (deterministic hash codes) | diff --git a/integration-test/src/test/java/org/apache/cassandra/simulator_test/SchedulerIntegrationTest.java b/integration-test/src/test/java/org/apache/cassandra/simulator_test/SchedulerIntegrationTest.java index 1295e2a..9de6588 100644 --- a/integration-test/src/test/java/org/apache/cassandra/simulator_test/SchedulerIntegrationTest.java +++ b/integration-test/src/test/java/org/apache/cassandra/simulator_test/SchedulerIntegrationTest.java @@ -47,8 +47,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; * <ol> * <li>A brand-new user-defined interface annotated with {@code @Intercept} can be registered * and produces genuine scheduling points inside the simulator. - * <li>The built-in JDK {@code CountDownLatch} registration can be replaced with a custom - * implementation via {@link Simulator#intercept(Class, Class)}. + * <li>A JDK {@code CountDownLatch} can be explicitly replaced with a custom implementation + * via {@link Simulator#intercept(Class, Class)}. * <li>A Cassandra factory-method interface can be overridden with a custom implementation. * <li>The validator throws {@link IllegalArgumentException} when a custom implementation * does not override every {@code @Intercept}-annotated instance method. @@ -144,9 +144,8 @@ public class SchedulerIntegrationTest } // =========================================================================== - // Custom JDK CountDownLatch replacement - overrides the built-in default so - // that the simulator uses MyJdkCountDownLatch instead of - // InterceptingCountDownLatch whenever simulation code calls new CountDownLatch(n). + // Custom JDK CountDownLatch replacement. JDK CountDownLatch uses instrumented + // AQS by default; this demonstrates opting into a specialized implementation. // =========================================================================== @PerClassLoader @@ -259,13 +258,13 @@ public class SchedulerIntegrationTest } // =========================================================================== - // 2. JDK CountDownLatch override - replaces InterceptingCountDownLatch - // with MyJdkCountDownLatch. The test verifies causal ordering to confirm - // the custom class is functioning as a genuine scheduling point. + // 2. JDK CountDownLatch replacement - installs MyJdkCountDownLatch explicitly. + // The test verifies causal ordering to confirm the custom class functions + // as a genuine scheduling point. // =========================================================================== @Test - void jdkCDLOverride_replacesDefaultAndPreservesScheduling() + void jdkCDLReplacement_installsCustomImplementationAndPreservesScheduling() { try (Simulator sim = new Simulator(42L, 1.0f)) { @@ -276,6 +275,8 @@ public class SchedulerIntegrationTest // new CountDownLatch(1) is redirected to new MyJdkCountDownLatch(1) var latch = new CountDownLatch(1); var done = cdl(2); + if (!(latch instanceof MyJdkCountDownLatch)) + throw new AssertionError("custom CountDownLatch replacement was not installed: " + latch.getClass()); new Thread(() -> { log.add("A-before-await"); diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java index 2954e39..8787b9e 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java @@ -217,8 +217,6 @@ public class Simulator implements AutoCloseable // TODO: there are quite a few JDK primitives that are still not instrumented // Default JDK primitive redirects; can be overridden or supplemented by intercept() calls. - customRules.add(new InterceptRule.Constructor("java/util/concurrent/CountDownLatch", - "org/apache/cassandra/simulator/systems/InterceptingCountDownLatch")); customRules.add(new InterceptRule.Constructor("java/util/IdentityHashMap", "org/apache/cassandra/simulator/systems/InterceptedIdentityHashMap")); customRules.add(new InterceptRule.Constructor("java/util/concurrent/ConcurrentHashMap", @@ -484,7 +482,7 @@ public class Simulator implements AutoCloseable * <p>Inside the bodies, standard Java concurrency primitives are transparently * intercepted by the ASM transformer: * <ul> - * <li>{@code new CountDownLatch(n)} -> simulator-controlled latch</li> + * <li>{@code CountDownLatch.await/countDown} -> scheduler-controlled through instrumented AQS</li> * <li>{@code thread.start()} -> scheduler-controlled thread start</li> * <li>{@code synchronized} blocks -> simulator-controlled monitor acquire/release</li> * <li>{@code LockSupport.park/unpark} -> scheduler-controlled park/unpark</li> @@ -607,17 +605,17 @@ public class Simulator implements AutoCloseable } @Override - public boolean stepUntil(Predicate<ObservableAction> predicate) + public Optional<ObservableAction> stepUntil(Predicate<ObservableAction> predicate) { while (step()) { for (ObservableAction action : drainObservedActions()) { if (predicate.test(action)) - return true; + return Optional.of(action); } } - return false; + return Optional.empty(); } @Override diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/step/ObservableAction.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/step/ObservableAction.java index ad5f0c7..f9f8330 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/step/ObservableAction.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/step/ObservableAction.java @@ -19,6 +19,8 @@ package org.apache.cassandra.simulator.step; import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; import javax.annotation.Nullable; @@ -52,6 +54,28 @@ public interface ObservableAction CUSTOM } + /** Match actions of the supplied kind emitted by a thread whose name contains the fragment. */ + static Predicate<ObservableAction> matcher(String threadNameFragment, Kind kind) + { + Objects.requireNonNull(threadNameFragment, "threadNameFragment"); + Objects.requireNonNull(kind, "kind"); + return action -> action.kind() == kind && action.threadName().contains(threadNameFragment); + } + + /** Match actions targeting the thread that emitted the supplied action. */ + static Predicate<ObservableAction> targetMatcher(ObservableAction targetThread) + { + long targetThreadId = Objects.requireNonNull(targetThread, "targetThread").threadId(); + return action -> action.targetThreadId() != null && action.targetThreadId() == targetThreadId; + } + + /** Match actions of the supplied kind targeting the thread that emitted the supplied action. */ + static Predicate<ObservableAction> targetMatcher(ObservableAction targetThread, Kind kind) + { + Objects.requireNonNull(kind, "kind"); + return targetMatcher(targetThread).and(action -> action.kind() == kind); + } + /** Sequence number within the session, starting at zero. */ long sequence(); diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/step/Session.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/step/Session.java index 48d918e..6c630b3 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/step/Session.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/step/Session.java @@ -63,9 +63,9 @@ public interface Session extends AutoCloseable { * Step until a captured action matches the supplied predicate. * Debug capture must be enabled before calling this method. * - * @return true on a match, or false if the schedule was exhausted first + * @return the matched action, or empty if the schedule was exhausted first */ - boolean stepUntil(Predicate<ObservableAction> predicate); + Optional<ObservableAction> stepUntil(Predicate<ObservableAction> predicate); /** * Step until an action of the supplied kind is captured. @@ -75,27 +75,13 @@ public interface Session extends AutoCloseable { */ default Optional<ObservableAction> stepUntil(ObservableAction.Kind kind) { - ObservableAction[] matched = new ObservableAction[1]; - boolean found = stepUntil(action -> { - if (action.kind() != kind) - return false; - matched[0] = action; - return true; - }); - return found ? Optional.of(matched[0]) : Optional.empty(); + return stepUntil(action -> action.kind() == kind); } /** Step until a custom action with the supplied stable name is captured. */ default Optional<ObservableAction> stepUntil(String name) { - ObservableAction[] matched = new ObservableAction[1]; - boolean found = stepUntil(action -> { - if (action.kind() != ObservableAction.Kind.CUSTOM || !name.equals(action.name())) - return false; - matched[0] = action; - return true; - }); - return found ? Optional.of(matched[0]) : Optional.empty(); + return stepUntil(action -> action.kind() == ObservableAction.Kind.CUSTOM && name.equals(action.name())); } /** diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/CountDownLatchSimTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/CountDownLatchSimTest.java index 0444d38..328b2ac 100644 --- a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/CountDownLatchSimTest.java +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/CountDownLatchSimTest.java @@ -20,60 +20,134 @@ package org.apache.cassandra.simulator_test; import java.util.concurrent.CountDownLatch; -import org.apache.cassandra.simulator.Simulator; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.apache.cassandra.simulator.Simulator; import org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable; +import org.apache.cassandra.simulator.context.SharedTestState; +import org.apache.cassandra.simulator.step.ObservableAction; +import org.apache.cassandra.simulator.step.Session; +import org.apache.cassandra.simulator.systems.InterceptingCountDownLatch; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.LATCH_AWAIT_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.LATCH_COUNTDOWN_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.PARK_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.UNPARK_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.WAKEUP_FIRED; +import static org.apache.cassandra.simulator.step.ObservableAction.matcher; +import static org.apache.cassandra.simulator.step.ObservableAction.targetMatcher; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Verifies that JDK {@link CountDownLatch} is instrumented. + * Verifies both supported JDK {@link CountDownLatch} paths: explicit constructor + * replacement and the unmodified JDK implementation running through instrumented AQS. */ public class CountDownLatchSimTest { @Test - void countDownLatchWakesWaiter() + void countDownLatchCanBeExplicitlyReplaced() + { + try (Simulator sim = new Simulator(42L)) + { + sim.intercept(CountDownLatch.class, InterceptingCountDownLatch.class); + try (Session session = sim.byStep((SerializableRunnable) () -> { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(1); + boolean[] waiterRan = { false }; + + assertTrue(latch instanceof InterceptingCountDownLatch, + "the explicit constructor rule should install the requested replacement"); + + new Thread(() -> { + try + { + latch.await(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + waiterRan[0] = true; + done.countDown(); + }, "waiter").start(); + + new Thread(latch::countDown, "decrementer").start(); + + try + { + done.await(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + + assertTrue(waiterRan[0], "the replacement should wake the waiter"); + }).enableDebug()) + { + ObservableAction await = session.stepUntil(matcher("waiter", LATCH_AWAIT_CAPTURED)) + .orElseThrow(() -> new AssertionError("the replacement should expose its await operation")); + assertEquals("1", await.detail()); + + ObservableAction countDown = session.stepUntil(matcher("decrementer", LATCH_COUNTDOWN_CAPTURED)) + .orElseThrow(() -> new AssertionError("the replacement should expose its countDown operation")); + assertEquals("1", countDown.detail()); + assertTrue(await.sequence() < countDown.sequence()); + + session.runToEnd(); + } + } + } + + @Test + void jdkCountDownLatchBlocksThroughInstrumentedAqs() { - int steps = Simulator.simulate(42L, (SerializableRunnable) () -> { - CountDownLatch latch = new CountDownLatch(1); - CountDownLatch done = new CountDownLatch(1); - boolean[] waiterRan = { false }; - - new Thread(() -> { - try { latch.await(); } - catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } - waiterRan[0] = true; - done.countDown(); - }, "waiter").start(); - - new Thread(latch::countDown, "decrementer").start(); - - try { done.await(); } - catch (InterruptedException e) { Thread.currentThread().interrupt(); } - - assertTrue(waiterRan[0], "waiter should have been woken by countDown() and completed"); - }); - // The step count is deterministic because buildSchedule() uses RunnableActionScheduler.Sequential, - // so actions are dequeued FIFO: waiter.start() is called before decrementer.start(), so the - // waiter always parks on the latch before the decrementer runs. With a random scheduler the - // decrementer could run first, the latch would already be 0 when the waiter called await(), - // and the whole thing would complete in 4 steps instead of 5. - Assertions.assertEquals(5, steps); - // Execution contains exactly 5 steps: - // * main thread starts and parks on done.await() - // - [t]Thread[thread-0] - // (its consequences: invoke waiter thread, invoke decrementer thread; - // the thread itself suspends on done.await() — UNBOUNDED_WAIT adds no scheduled action, - // so main will be woken later by a direct interceptWakeup from done.countDown()) - // * waiter thread starts and parks on latch.await() - // - [t]Invoke Thread[waiter,5,sim-896126964341291] with Thread[sim-896126964341291_waiter:1,5,sim-896126964341291] - // * decrementer thread starts, calls latch.countDown() (signals waiter), and exits - // - [t]Invoke Thread[decrementer,5,sim-896126964341291] with Thread[sim-896126964341291_decrementer:1,5,sim-896126964341291] - // * waiter resumes, sets waiterRan, calls done.countDown() (signals main), and exits - // - [tw]Wakeup Thread[sim-896126964341291_waiter:1,5,sim-896126964341291] parkedAt[org.apache.cassandra.simulator_test.CountDownLatchSimTest.lambda$countDownLatchWakesWaiter$0(CountDownLatchSimTest.java:45)] - // * main thread resumes and asserts - // - [tw]Wakeup Thread[sim-896126964341291_thread-0:1,5,sim-896126964341291] parkedAt[org.apache.cassandra.simulator_test.CountDownLatchSimTest.lambda$countDownLatchWakesWaiter$1f753cc0$1(CountDownLatchSimTest.java:53)] + SharedTestState.reset(); + try (Simulator sim = new Simulator(42L); + Session session = sim.byStep((SerializableRunnable) () -> { + CountDownLatch latch = new CountDownLatch(1); + if (latch.getClass() != CountDownLatch.class) + throw new AssertionError("JDK CountDownLatch was unexpectedly replaced by " + latch.getClass()); + + new Thread(() -> { + try + { + latch.await(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + SharedTestState.threadRan.incrementAndGet(); + }, "jdk-cdl-waiter").start(); + + new Thread(latch::countDown, "jdk-cdl-decrementer").start(); + }).enableDebug()) + { + ObservableAction park = session.stepUntil(matcher("jdk-cdl-waiter", PARK_CAPTURED)) + .orElseThrow(() -> new AssertionError("CountDownLatch.await() should park in AQS")); + assertTrue(park.stackTrace().stream() + .anyMatch(frame -> frame.getClassName().equals("java.util.concurrent.locks.AbstractQueuedSynchronizer")), + "the intercepted park should originate in AQS"); + assertEquals(0, SharedTestState.threadRan.get(), "the waiter must remain suspended"); + + ObservableAction unpark = session.stepUntil(matcher("jdk-cdl-decrementer", UNPARK_CAPTURED)) + .orElseThrow(() -> new AssertionError("CountDownLatch.countDown() should unpark through AQS")); + assertTrue(unpark.stackTrace().stream() + .anyMatch(frame -> frame.getClassName().equals("java.util.concurrent.locks.AbstractQueuedSynchronizer")), + "the intercepted unpark should originate in AQS"); + assertTrue(park.sequence() < unpark.sequence()); + assertEquals(0, SharedTestState.threadRan.get(), "the wakeup has not run yet"); + + ObservableAction wakeup = session.stepUntil(targetMatcher(park, WAKEUP_FIRED)).orElseThrow(); + assertEquals(Long.valueOf(park.threadId()), wakeup.targetThreadId()); + assertTrue(unpark.sequence() < wakeup.sequence()); + + session.runToEnd(); + assertEquals(1, SharedTestState.threadRan.get(), "the waiter should resume after countDown()"); + } } } diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InstrumentedPrimitivesTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InstrumentedPrimitivesTest.java index 77da2a5..b9fa202 100644 --- a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InstrumentedPrimitivesTest.java +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InstrumentedPrimitivesTest.java @@ -82,8 +82,8 @@ import org.junit.jupiter.api.Test; * <li>{@link Object#wait()} -- via ASM to {@code InterceptorOfSystemMethods.Global.wait}</li> * <li>{@link Object#notify()} -- via ASM to {@code InterceptorOfSystemMethods.Global.notify}</li> * <li>{@link Object#notifyAll()} -- via ASM to {@code InterceptorOfSystemMethods.Global.notifyAll}</li> - * <li>JDK {@link java.util.concurrent.CountDownLatch} (constructor rewritten to - * {@link org.apache.cassandra.simulator.systems.InterceptingCountDownLatch})</li> + * <li>JDK {@link java.util.concurrent.CountDownLatch} -- blocking controlled through + * instrumented AQS {@code LockSupport.park/unpark} calls</li> * <li>{@link LockSupport#park()} -- via ASM to {@code InterceptibleThread.park}</li> * <li>{@link LockSupport#parkNanos(long)} -- same</li> * <li>{@link Thread#sleep(long)} -- via ASM to {@code InterceptorOfSystemMethods.Global.sleep}</li> @@ -100,7 +100,7 @@ public class InstrumentedPrimitivesTest { // ---- Helpers -------------------------------------------------------------- - /** JDK CDL -- constructor is rewritten to InterceptingCountDownLatch by default. */ + /** JDK CDL -- its AQS park/unpark path is controlled by the simulator agent. */ private static CountDownLatch cdl(int n) { return new CountDownLatch(n); @@ -290,9 +290,9 @@ public class InstrumentedPrimitivesTest } // =========================================================================== - // 5. JDK CountDownLatch (NEW + <init> rewritten to InterceptingCountDownLatch) - // The JDK type is transparently replaced with a simulator-controlled subclass. - // No sim.intercept() call needed -- default Constructor rule fires automatically. + // 5. JDK CountDownLatch + // The JDK implementation is retained. Its AQS blocking path reaches the + // agent-instrumented LockSupport.park/unpark calls. // =========================================================================== @Test diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java index 51e8184..4dcaaca 100644 --- a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java @@ -33,6 +33,11 @@ import org.junit.jupiter.api.Test; import org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable; import org.apache.cassandra.simulator.context.Shared; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.PARK_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.UNPARK_CAPTURED; +import static org.apache.cassandra.simulator.step.ObservableAction.Kind.WAKEUP_FIRED; +import static org.apache.cassandra.simulator.step.ObservableAction.matcher; +import static org.apache.cassandra.simulator.step.ObservableAction.targetMatcher; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -136,16 +141,16 @@ public class ParkUnparkTest session.enableDebug(); assertEquals(Set.of(), SharedState.events); - assertTrue(session.stepUntil(ObservableAction.Kind.PARK_CAPTURED).isPresent()); + ObservableAction park = session.stepUntil(matcher("parker", PARK_CAPTURED)).orElseThrow(); assertEquals(Set.of("parking"), SharedState.events, "park() was captured and the parker is suspended"); - assertTrue(session.stepUntil(ObservableAction.Kind.UNPARK_CAPTURED).isPresent()); + session.stepUntil(matcher("unparker", UNPARK_CAPTURED).and(targetMatcher(park))).orElseThrow(); assertEquals(Set.of("parking"), SharedState.events, "unpark() was captured but its wakeup has not fired"); - assertTrue(session.stepUntil(ObservableAction.Kind.WAKEUP_FIRED).isPresent()); + session.stepUntil(targetMatcher(park, WAKEUP_FIRED)).orElseThrow(); assertEquals(Set.of("parking", "unparked"), SharedState.events, "the scheduled wakeup resumed the parker"); - assertFalse(session.stepUntil(ObservableAction.Kind.PARK_CAPTURED).isPresent()); + session.runToEnd(); assertFalse(session.hasNext()); } } @@ -183,15 +188,15 @@ public class ParkUnparkTest { session.enableDebug(); - assertTrue(session.stepUntil(ObservableAction.Kind.PARK_CAPTURED).isPresent()); - assertEquals(ObservableAction.Kind.PARK_CAPTURED, session.lastAction().orElseThrow().kind()); + ObservableAction park = session.stepUntil(matcher("parker", PARK_CAPTURED)).orElseThrow(); + assertEquals(PARK_CAPTURED, park.kind()); - assertTrue(session.stepUntil(ObservableAction.Kind.UNPARK_CAPTURED).isPresent()); - assertNotEquals(ObservableAction.Kind.WAKEUP_FIRED, session.lastAction().orElseThrow().kind(), + ObservableAction unpark = session.stepUntil(matcher("unparker", UNPARK_CAPTURED).and(targetMatcher(park))).orElseThrow(); + assertNotEquals(WAKEUP_FIRED, unpark.kind(), "capturing unpark should not collapse with wakeup delivery"); - assertTrue(session.stepUntil(ObservableAction.Kind.WAKEUP_FIRED).isPresent()); - assertFalse(session.stepUntil(ObservableAction.Kind.PARK_CAPTURED).isPresent()); + session.stepUntil(targetMatcher(park, WAKEUP_FIRED)).orElseThrow(); + session.runToEnd(); } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
