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 ae0d1c4c28d4dd9ffd4c9b43447915196561e50c
Author: Alex Petrov <[email protected]>
AuthorDate: Mon Jul 27 10:18:05 2026 +0200

    Improve tests
---
 AGENTS.md                                          |  11 +
 integration-test/build.gradle                      |   1 +
 .../test/CassandraCountDownLatchStepApiTest.java   |  90 ------
 ...grationTest.java => InterceptOverrideTest.java} |  20 +-
 .../cassandra/simulator/test/IoStepApiTest.java    | 353 ---------------------
 .../simulator/test/LatchChainSimTest.java          |  76 -----
 .../InstrumentedCassandraCountDownLatchTest.java   |   5 +-
 .../test/{ => step}/InstrumentedSemaphoreTest.java |   5 +-
 .../test/{ => step}/InstrumentedWaitQueueTest.java |   5 +-
 ...Test.java => LockSupportParkUnparkCDLTest.java} |  16 +-
 .../simulator/test/step/MonitorEnterTest.java      |  81 -----
 .../PluggablePrimitiveIntegrationTest.java         |   5 +-
 .../simulator/test/step/StepApiTestSupport.java    |   2 +-
 .../java/org/apache/cassandra/journal/Journal.java |  22 +-
 .../org/apache/cassandra/journal/KeySupport.java   |   1 -
 .../org/apache/cassandra/journal/OnDiskIndex.java  |   2 +-
 .../{UncheckedCloseable.java => Closeable.java}    |   0
 settings.gradle                                    |   1 +
 .../simulator/asm/GlobalMethodTransformer.java     |   4 +-
 .../simulator/context/IIsolatedExecutor.java       |  31 ++
 simulator-core/build.gradle                        |   1 +
 .../org/apache/cassandra/simulator/Action.java     |   4 +-
 .../org/apache/cassandra/simulator/Simulator.java  |  51 ++-
 .../cassandra/simulator/step/ObservableAction.java |   1 +
 .../apache/cassandra/simulator/step/Session.java   |   3 +-
 .../simulator/systems/InterceptUtils.java          |  41 +++
 .../systems/InterceptibleConcurrentHashMap.java    |  16 +-
 .../systems/InterceptingGlobalMethods.java         |  36 +--
 .../simulator/systems/InterceptingMonitors.java    |  25 +-
 .../cassandra/simulator/systems/SimulatedWait.java |  29 +-
 ...uleMethodCallOutsideConfiguredPackagesTest.java |   2 +-
 .../simulator/test/ConcurrentKVStoreSimTest.java   | 319 -------------------
 .../simulator/test/DeterministicCounterTest.java   |   4 +-
 .../test/ExecutorServiceInterceptTest.java         |   2 +-
 .../simulator/test/InstrumentedPrimitivesTest.java |  66 ++--
 .../test/InterceptRuleIntegrationTest.java         |   2 +-
 .../test/InterceptRuleMethodCallTest.java          |   2 +-
 ...entedTest.java => InterceptibleThreadTest.java} |   2 +-
 .../simulator/test/MonitorInvariantTest.java       |   2 +-
 .../simulator/test/TimeDeterminismTest.java        |   2 +-
 .../test/{ => step}/CountDownLatchSimTest.java     |   4 +-
 .../simulator/test/step/MonitorEnterTest.java      |  83 +++++
 .../simulator/test/step/NemesisYieldTest.java      |   2 +-
 .../simulator/test/step/ObjectWaitNotifyTest.java  |   2 +-
 .../simulator/test/step/StepApiTestSupport.java    |   2 +-
 .../simulator/test/step/ThreadSleepTest.java       |   2 +-
 .../simulator/test/step/ThreadStartTest.java       |   4 +-
 test-utils/build.gradle                            |   1 +
 .../simulator/testutils}/SharedTestState.java      |  20 +-
 49 files changed, 316 insertions(+), 1145 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..902f845
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,11 @@
+# AGENTS
+
+Project notes for code changes:
+
+- When writing comments and docstrings, optimize for readability in the IDE.
+- Prefer plain text over HTML-oriented Javadoc formatting.
+- Avoid tags like `<p>`, `<ol>`, `<li>`, or presentation-oriented markup 
unless they are strictly necessary.
+- Write comments so they read clearly in source form first.
+- Prefer imports and simple class names over fully qualified class names in 
ordinary source code and comments.
+- For commits requested by the user, use the user's Git author identity.
+- Prefix commit messages for work not yet reviewed by the user with 
`UNREVIEWED:`.
diff --git a/integration-test/build.gradle b/integration-test/build.gradle
index 80dd657..8d5edc4 100644
--- a/integration-test/build.gradle
+++ b/integration-test/build.gradle
@@ -21,6 +21,7 @@ test {
 dependencies {
     implementation project(':simulator-context')
     implementation project(':simulator-core')
+    testImplementation project(':test-utils')
     testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
     testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.36'
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/CassandraCountDownLatchStepApiTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/CassandraCountDownLatchStepApiTest.java
deleted file mode 100644
index 9d3a3c1..0000000
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/CassandraCountDownLatchStepApiTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.simulator.test;
-
-import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
-import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
-import org.apache.cassandra.simulator.step.ObservableAction;
-import org.apache.cassandra.simulator.step.Session;
-import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
-import org.apache.cassandra.simulator.test.step.StepApiTestSupport;
-import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * Verifies capture and wakeup ordering for CassandraCountDownLatch.
- */
-public class CassandraCountDownLatchStepApiTest extends StepApiTestSupport
-{
-    private static Simulator instrumentedSimulator(long seed)
-    {
-        Simulator sim = new Simulator(seed, 
"org/apache/cassandra/simulator/test/");
-        sim.intercept(CassandraCountDownLatch.class, 
InstrumentedCassandraCountDownLatch.class);
-        return sim;
-    }
-
-    @Test
-    void cassandraCountDownLatch_awaitSuspendsUntilDecrement()
-    {
-        try (Simulator sim = instrumentedSimulator(42L);
-             Session session = sim.byStep((SerializableRunnable) () ->
-        {
-            var latch = CassandraCountDownLatch.newCountDownLatch(1);
-            var done  = CassandraCountDownLatch.newCountDownLatch(2);
-
-            new Thread(() -> {
-                try { latch.await(); } catch (InterruptedException e) { 
return; }
-                SharedTestState.eventCount.incrementAndGet();
-                done.decrement();
-            }, "awaiter").start();
-
-            new Thread(() -> {
-                SharedTestState.threadRan.incrementAndGet();
-                latch.decrement();
-                done.decrement();
-            }, "decrementer").start();
-
-            try { done.await(); } catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-        }))
-        {
-            session.enableDebug();
-            assertEquals(0, SharedTestState.threadRan.get(),  "decrementer has 
not run yet");
-            assertEquals(0, SharedTestState.eventCount.get(), "awaiter has not 
resumed yet");
-
-            ObservableAction action;
-            action = 
session.stepUntil(InstrumentedCassandraCountDownLatch.AWAIT).get();
-            assertTrue(action.threadName().contains("entrypoint"));
-            assertEquals(0, SharedTestState.threadRan.get(),  "decrementer not 
yet run");
-            assertEquals(0, SharedTestState.eventCount.get(), "awaiter 
suspended");
-
-            action = 
session.stepUntil(InstrumentedCassandraCountDownLatch.DECREMENT).get();
-            assertTrue(action.threadName().contains("decrementer"));
-            assertEquals(1, SharedTestState.threadRan.get(),  "decrementer 
ran");
-            assertEquals(0, SharedTestState.eventCount.get(), "awaiter not yet 
resumed");
-
-            action = 
session.stepUntil(InstrumentedCassandraCountDownLatch.DECREMENT).get();
-            assertTrue(action.threadName().contains("awaiter"));
-            assertEquals(1, SharedTestState.eventCount.get(), "awaiter resumed 
after decrement");
-        }
-    }
-}
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/InterceptOverrideTest.java
similarity index 95%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/SchedulerIntegrationTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/InterceptOverrideTest.java
index 3983530..ebb3381 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/InterceptOverrideTest.java
@@ -44,18 +44,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
  * Verifies the pluggable scheduler integration contract via {@link 
Simulator#intercept} /
  * {@link Simulator#intercept}:
  *
- * <ol>
- *   <li>A brand-new user-defined interface annotated with {@code @Intercept} 
can be registered
- *       and produces genuine scheduling points inside the simulator.
- *   <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.
- *   <li>Methods marked {@code @NoSimulation} are exempt from the coverage 
check.
- * </ol>
+ * - A brand-new user-defined interface annotated with {@code @Intercept} can 
be registered
+ *   and produces genuine scheduling points inside the simulator.
+ * - A JDK {@code CountDownLatch} can be explicitly replaced with a custom 
implementation
+ *   via {@link Simulator#intercept(Class, Class)}.
+ * - A Cassandra factory-method interface can be overridden with a custom 
implementation.
+ * - The validator throws {@link IllegalArgumentException} when a custom 
implementation
+ *   does not override every {@code @Intercept}-annotated instance method.
+ * - Methods marked {@code @NoSimulation} are exempt from the coverage check.
  */
-public class SchedulerIntegrationTest
+public class InterceptOverrideTest
 {
     // 
===========================================================================
     // Custom Gate interface - a bespoke @Intercept interface that is unknown 
to
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/IoStepApiTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/IoStepApiTest.java
deleted file mode 100644
index ec7ff9c..0000000
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/IoStepApiTest.java
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.simulator.test;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.cassandra.simulator.io.InstrumentedFile;
-import org.apache.cassandra.simulator.io.IoOperationListener;
-import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
-import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
-import org.apache.cassandra.simulator.step.ObservableAction;
-import org.apache.cassandra.simulator.step.Session;
-import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
-import org.apache.cassandra.simulator.systems.InterceptorOfSystemMethods;
-import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class IoStepApiTest
-{
-    @BeforeEach
-    void resetSharedState()
-    {
-        SharedTestState.reset();
-        InstrumentedFile.setListener(new IoOperationListener()
-        {
-            @Override
-            public void beforePreIoPark()
-            {
-                SharedTestState.parkCount.incrementAndGet();
-            }
-
-            @Override
-            public void afterPreIoPark()
-            {
-                SharedTestState.unparkCount.incrementAndGet();
-            }
-        });
-    }
-
-    @AfterEach
-    void clearListener()
-    {
-        InstrumentedFile.setListener(null);
-    }
-
-    private static Simulator simulator(long seed)
-    {
-        return new Simulator(seed, "org/apache/cassandra/simulator/test/");
-    }
-
-    private static Simulator instrumentedSimulator(long seed)
-    {
-        Simulator sim = simulator(seed);
-        sim.intercept(CassandraCountDownLatch.class, 
InstrumentedCassandraCountDownLatch.class);
-        return sim;
-    }
-
-    @Test
-    void fileExists_isParkableAndResumableIoStep() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-exists", ".tmp");
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = instrumentedSimulator(42L);
-                 Session session = sim.byStep((SerializableRunnable) () -> {
-                var done = CassandraCountDownLatch.newCountDownLatch(2);
-
-                new Thread(() -> {
-                    boolean exists = new 
InstrumentedFile(Path.of(pathString)).exists();
-                    if (exists)
-                        SharedTestState.eventCount.incrementAndGet();
-                    done.decrement();
-                }, "exists").start();
-
-                new Thread(() -> {
-                    SharedTestState.threadRan.incrementAndGet();
-                    done.decrement();
-                }, "observer").start();
-
-                try { done.await(); } catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-            }).enableDebug())
-            {
-                ObservableAction preIo = 
session.stepUntil(ObservableAction.Kind.PRE_IO_CAPTURED).orElseThrow();
-                assertTrue(preIo.threadName().contains("exists"));
-                assertEquals(0, SharedTestState.eventCount.get(), "host I/O 
has not executed");
-
-                ObservableAction observer = 
session.stepUntil(InstrumentedCassandraCountDownLatch.DECREMENT).orElseThrow();
-                assertTrue(observer.threadName().contains("observer"));
-                assertEquals(0, SharedTestState.eventCount.get(), "another 
runnable executed before the continuation");
-
-                ObservableAction resumed = 
session.stepUntil(ObservableAction.Kind.WAKEUP_FIRED).orElseThrow();
-                assertEquals(Long.valueOf(preIo.threadId()), 
resumed.targetThreadId());
-                assertEquals(1, SharedTestState.eventCount.get(), "host I/O 
executed after the continuation fired");
-            }
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void immediateContinuationIsValid() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-immediate", ".tmp");
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = simulator(42L);
-                 Session session = sim.byStep((SerializableRunnable) () -> {
-                     if (new InstrumentedFile(Path.of(pathString)).exists())
-                         SharedTestState.eventCount.incrementAndGet();
-                 }).enableDebug())
-            {
-                ObservableAction preIo = 
session.stepUntil(ObservableAction.Kind.PRE_IO_CAPTURED).orElseThrow();
-                assertEquals(0, SharedTestState.eventCount.get());
-
-                ObservableAction resumed = 
session.stepUntil(ObservableAction.Kind.WAKEUP_FIRED).orElseThrow();
-                assertEquals(Long.valueOf(preIo.threadId()), 
resumed.targetThreadId());
-                assertEquals(1, SharedTestState.eventCount.get());
-                assertFalse(session.hasNext());
-            }
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void continuationPreservesThreadAndInterruptStatus() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-interrupt", ".tmp");
-        AtomicReference<Thread> before = new AtomicReference<>();
-        AtomicReference<Thread> after = new AtomicReference<>();
-        InstrumentedFile.setListener(new IoOperationListener()
-        {
-            @Override
-            public void beforePreIoPark()
-            {
-                before.set(Thread.currentThread());
-            }
-
-            @Override
-            public void afterPreIoPark()
-            {
-                after.set(Thread.currentThread());
-            }
-        });
-
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = simulator(42L);
-                 Session session = sim.byStep((SerializableRunnable) () -> {
-                     Thread.currentThread().interrupt();
-                     new InstrumentedFile(Path.of(pathString)).exists();
-                     if (Thread.currentThread().isInterrupted())
-                         SharedTestState.eventCount.incrementAndGet();
-                 }))
-            {
-                assertTrue(session.step());
-                assertNotNull(before.get());
-                assertNotSame(Thread.currentThread(), before.get());
-                assertNull(after.get());
-
-                assertTrue(session.step());
-                assertSame(before.get(), after.get());
-                assertEquals(1, SharedTestState.eventCount.get());
-            }
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void preIoConsumesNoRandomDecision() throws Exception
-    {
-        long seed = 42L;
-        try (Simulator sim = simulator(seed))
-        {
-            sim.simulate((SerializableRunnable) () -> {
-                UUID uuid = InterceptorOfSystemMethods.Global.randomUUID();
-                SharedTestState.value1.set(uuid.getMostSignificantBits());
-                SharedTestState.value2.set(uuid.getLeastSignificantBits());
-            });
-        }
-        UUID expected = new UUID(SharedTestState.value1.get(), 
SharedTestState.value2.get());
-
-        SharedTestState.reset();
-        Path path = Files.createTempFile("sim-io-random", ".tmp");
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = simulator(seed))
-            {
-                sim.simulate((SerializableRunnable) () -> {
-                    new InstrumentedFile(Path.of(pathString)).exists();
-                    UUID uuid = InterceptorOfSystemMethods.Global.randomUUID();
-                    SharedTestState.value1.set(uuid.getMostSignificantBits());
-                    SharedTestState.value2.set(uuid.getLeastSignificantBits());
-                });
-            }
-
-            assertEquals(expected, new UUID(SharedTestState.value1.get(), 
SharedTestState.value2.get()));
-            assertEquals(1, SharedTestState.parkCount.get());
-            assertEquals(1, SharedTestState.unparkCount.get());
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void nonSimulatorExecutionDoesNotPause() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-unsimulated", ".tmp");
-        try
-        {
-            assertTrue(new InstrumentedFile(path).exists());
-            assertEquals(1, SharedTestState.parkCount.get());
-            assertEquals(1, SharedTestState.unparkCount.get());
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void deterministicEvaluationDoesNotPause() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-deterministic", ".tmp");
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = simulator(42L);
-                 Session session = sim.byStep((SerializableRunnable) () -> {
-                     Runnable operation = () -> {
-                         if (new 
InstrumentedFile(Path.of(pathString)).exists())
-                             SharedTestState.eventCount.incrementAndGet();
-                     };
-                     try
-                     {
-                         
Class.forName("org.apache.cassandra.simulator.systems.InterceptibleThread")
-                              .getMethod("runDeterministic", Runnable.class)
-                              .invoke(null, operation);
-                     }
-                     catch (ReflectiveOperationException e)
-                     {
-                         throw new RuntimeException(e);
-                     }
-                 }))
-            {
-                assertTrue(session.step());
-                assertEquals(1, SharedTestState.parkCount.get());
-                assertEquals(1, SharedTestState.unparkCount.get());
-                assertEquals(1, SharedTestState.eventCount.get());
-                assertFalse(session.hasNext());
-            }
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-
-    @Test
-    void fileDeleteIfExists_isParkableAndResumableIoStep() throws Exception
-    {
-        Path path = Files.createTempFile("sim-io-delete", ".tmp");
-        try
-        {
-            String pathString = path.toString();
-            try (Simulator sim = instrumentedSimulator(42L);
-                 Session session = sim.byStep((SerializableRunnable) () -> {
-                var done = CassandraCountDownLatch.newCountDownLatch(2);
-
-                new Thread(() -> {
-                    new InstrumentedFile(Path.of(pathString)).deleteIfExists();
-                    SharedTestState.eventCount.incrementAndGet();
-                    done.decrement();
-                }, "delete").start();
-
-                new Thread(() -> {
-                    SharedTestState.threadRan.incrementAndGet();
-                    done.decrement();
-                }, "observer").start();
-
-                try { done.await(); } catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-            }))
-            {
-                assertTrue(Files.exists(path), "step 0: host file exists 
before simulation");
-
-                assertTrue(session.stepThrough(1));
-                assertTrue(Files.exists(path), "step 1: delete thread queued, 
file still exists");
-
-                assertTrue(session.stepThrough(1));
-                assertEquals(1, SharedTestState.parkCount.get(), "step 2: 
delete reached an I/O step");
-                assertEquals(0, SharedTestState.unparkCount.get(), "step 2: 
delete is parked before host Files.deleteIfExists");
-                assertEquals(0, SharedTestState.eventCount.get(), "step 2: 
delete completion is not visible");
-                assertTrue(Files.exists(path), "step 2: host file has not been 
deleted before release");
-
-                assertTrue(session.stepThrough(1));
-                assertEquals(1, SharedTestState.threadRan.get(), "step 3: 
another runnable ran while delete was parked");
-                assertTrue(Files.exists(path), "step 3: host file still has 
not been deleted");
-
-                assertTrue(session.stepThrough(1));
-                assertEquals(1, SharedTestState.unparkCount.get(), "step 4: 
delete resumed after the I/O step");
-                assertEquals(1, SharedTestState.eventCount.get(), "step 4: 
delete completion is now visible");
-                assertFalse(Files.exists(path), "step 4: host file was deleted 
only after release");
-            }
-        }
-        finally
-        {
-            Files.deleteIfExists(path);
-        }
-    }
-}
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/LatchChainSimTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/LatchChainSimTest.java
deleted file mode 100644
index 9e7dad8..0000000
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/LatchChainSimTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.simulator.test;
-
-import org.apache.cassandra.simulator.Simulator;
-import org.junit.jupiter.api.Test;
-
-import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
-import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
-import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * Creates a chain of 10 simulated threads where each thread waits on its 
predecessor's
- * latch and then decrements the next, forming a sequential wake-up cascade.
- *
- * <p>Uses the Cassandra {@link CassandraCountDownLatch} factory registered via
- * {@link Simulator#intercept(Class, Class)}.
- */
-public class LatchChainSimTest
-{
-    private static final int CHAIN_LENGTH = 10;
-
-    @Test
-    void latchChainPropagates()
-    {
-        try (Simulator sim = new Simulator(41L, 
"org/apache/cassandra/simulator/test/"))
-        {
-            sim.intercept(CassandraCountDownLatch.class, 
InstrumentedCassandraCountDownLatch.class);
-
-            sim.simulate((SerializableRunnable) () -> {
-                CassandraCountDownLatch[] latches = new 
CassandraCountDownLatch[CHAIN_LENGTH + 1];
-                for (int i = 0; i <= CHAIN_LENGTH; i++)
-                    latches[i] = CassandraCountDownLatch.newCountDownLatch(1);
-
-                CassandraCountDownLatch allDone = 
CassandraCountDownLatch.newCountDownLatch(CHAIN_LENGTH);
-
-                for (int i = 0; i < CHAIN_LENGTH; i++)
-                {
-                    final int idx = i;
-                    new Thread(() -> {
-                        try { latches[idx].await(); }
-                        catch (InterruptedException e) { 
Thread.currentThread().interrupt(); return; }
-                        latches[idx + 1].decrement();
-                        allDone.decrement();
-                    }, "chain-" + i).start();
-                }
-
-                new Thread(() -> latches[0].decrement(), "kicker").start();
-
-                try { allDone.await(); }
-                catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-
-                assertEquals(0, latches[CHAIN_LENGTH].count(),
-                             "terminal latch should have been decremented by 
the last chain thread");
-            });
-        }
-    }
-}
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedCassandraCountDownLatchTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedCassandraCountDownLatchTest.java
similarity index 97%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedCassandraCountDownLatchTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedCassandraCountDownLatchTest.java
index d188f0e..5dab621 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedCassandraCountDownLatchTest.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedCassandraCountDownLatchTest.java
@@ -16,15 +16,14 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.test;
+package org.apache.cassandra.simulator.test.step;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
 import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
-import org.apache.cassandra.simulator.test.step.StepApiTestSupport;
 import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
 import org.junit.jupiter.api.Test;
 
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedSemaphoreTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedSemaphoreTest.java
similarity index 96%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedSemaphoreTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedSemaphoreTest.java
index 80881fb..f308691 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedSemaphoreTest.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedSemaphoreTest.java
@@ -16,15 +16,14 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.test;
+package org.apache.cassandra.simulator.test.step;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
 import org.apache.cassandra.simulator.systems.InstrumentedSemaphore;
-import org.apache.cassandra.simulator.test.step.StepApiTestSupport;
 import org.apache.cassandra.utils.concurrent.Semaphore;
 import org.junit.jupiter.api.Test;
 
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedWaitQueueTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedWaitQueueTest.java
similarity index 97%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedWaitQueueTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedWaitQueueTest.java
index 54ef28e..f2ec23e 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/InstrumentedWaitQueueTest.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/InstrumentedWaitQueueTest.java
@@ -16,15 +16,14 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.test;
+package org.apache.cassandra.simulator.test.step;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
 import org.apache.cassandra.simulator.systems.InstrumentedWaitQueue;
-import org.apache.cassandra.simulator.test.step.StepApiTestSupport;
 import org.apache.cassandra.utils.concurrent.WaitQueue;
 import org.junit.jupiter.api.Test;
 
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkCDLTest.java
similarity index 90%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkCDLTest.java
index 2d9c63f..21df976 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkTest.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/LockSupportParkUnparkCDLTest.java
@@ -19,13 +19,12 @@
 package org.apache.cassandra.simulator.test.step;
 
 import java.util.concurrent.locks.LockSupport;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
 import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
-import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
 import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
 import org.junit.jupiter.api.Test;
 
@@ -35,19 +34,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 /**
  * Verifies capture and wakeup ordering for LockSupport park and unpark 
operations.
  */
-public class LockSupportParkUnparkTest extends StepApiTestSupport
+public class LockSupportParkUnparkCDLTest extends StepApiTestSupport
 {
-    private static Simulator instrumentedSimulator(long seed)
-    {
-        Simulator sim = new Simulator(seed, 
"org/apache/cassandra/simulator/test/");
-        sim.intercept(CassandraCountDownLatch.class, 
InstrumentedCassandraCountDownLatch.class);
-        return sim;
-    }
-
     @Test
     void lockSupportPark_parkerSuspendsAndResumesViaUnpark()
     {
-        try (Simulator sim = instrumentedSimulator(42L);
+        try (Simulator sim = new Simulator(42, 
"org/apache/cassandra/simulator/test/");
              Session session = sim.byStep((SerializableRunnable) () ->
         {
             // Thread.start() replaces the original Thread with an 
InterceptibleThread.
@@ -91,7 +83,7 @@ public class LockSupportParkUnparkTest extends 
StepApiTestSupport
     @Test
     void lockSupportParkNanos_nothingHappensWithoutStep()
     {
-        try (Simulator sim = instrumentedSimulator(42L);
+        try (Simulator sim = new Simulator(42, 
"org/apache/cassandra/simulator/test/");
              Session session = sim.byStep((SerializableRunnable) () -> {
             Thread[] parkerRef = { null };
             var ready = CassandraCountDownLatch.newCountDownLatch(1);
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
deleted file mode 100644
index 23aba82..0000000
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.simulator.test.step;
-
-import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
-import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
-import org.apache.cassandra.simulator.step.Session;
-import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatch;
-import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * Verifies that instrumented MONITORENTER operations create scheduler actions.
- */
-public class MonitorEnterTest extends StepApiTestSupport
-{
-    private static Simulator instrumentedSimulator(long seed, float 
monitorDelayChance)
-    {
-        Simulator sim = new Simulator(seed, monitorDelayChance, 
"org/apache/cassandra/simulator/test/");
-        sim.intercept(CassandraCountDownLatch.class, 
InstrumentedCassandraCountDownLatch.class);
-        return sim;
-    }
-
-    @Test
-    void synchronizedBlock_monitorEntryIsSchedulingPoint()
-    {
-        try (Simulator sim = instrumentedSimulator(42L, 1.0f);
-             Session session = sim.byStep((SerializableRunnable) () -> {
-            Object lock = new Object();
-            var done = CassandraCountDownLatch.newCountDownLatch(2);
-
-            new Thread(() -> {
-                synchronized (lock) { 
SharedTestState.eventCount.incrementAndGet(); }
-                done.decrement();
-            }, "A").start();
-
-            new Thread(() -> {
-                synchronized (lock) { 
SharedTestState.eventCount.incrementAndGet(); }
-                done.decrement();
-            }, "B").start();
-
-            try { done.await(); } catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-        }))
-        {
-            assertEquals(0, SharedTestState.eventCount.get(), "nothing ran 
yet");
-
-            int steps = 0;
-            while (session.hasNext())
-            {
-                assertTrue(session.stepThrough(1));
-                steps++;
-            }
-
-            assertEquals(2, SharedTestState.eventCount.get(), "both threads 
entered the synchronized block");
-
-            // The outer action and two worker actions need at most four steps 
without
-            // monitor delays. Forced MONITORENTER delays add scheduler 
actions.
-            assertTrue(steps > 4, "monitor delay chance 1.0f must have created 
extra scheduling steps; got: " + steps);
-        }
-    }
-}
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/PluggablePrimitiveIntegrationTest.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/PluggablePrimitiveIntegrationTest.java
similarity index 98%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/PluggablePrimitiveIntegrationTest.java
rename to 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/PluggablePrimitiveIntegrationTest.java
index d344797..183f9ac 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/PluggablePrimitiveIntegrationTest.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/PluggablePrimitiveIntegrationTest.java
@@ -16,11 +16,11 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.test;
+package org.apache.cassandra.simulator.test.step;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
 import org.apache.cassandra.simulator.systems.InterceptingAwaitable;
@@ -28,7 +28,6 @@ import 
org.apache.cassandra.simulator.systems.InstrumentedCassandraCountDownLatc
 import org.apache.cassandra.simulator.systems.InstrumentedSemaphore;
 import org.apache.cassandra.simulator.systems.InstrumentedWaitQueue;
 import org.apache.cassandra.simulator.utils.concurrent.Condition;
-import org.apache.cassandra.simulator.test.step.StepApiTestSupport;
 import org.apache.cassandra.utils.concurrent.CassandraCountDownLatch;
 import org.apache.cassandra.utils.concurrent.Semaphore;
 import org.apache.cassandra.utils.concurrent.WaitQueue;
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
index c191cf5..82de36a 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
+++ 
b/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import org.junit.jupiter.api.BeforeEach;
 
 public abstract class StepApiTestSupport
diff --git a/journal/src/main/java/org/apache/cassandra/journal/Journal.java 
b/journal/src/main/java/org/apache/cassandra/journal/Journal.java
index 5e564a0..9fe28a3 100644
--- a/journal/src/main/java/org/apache/cassandra/journal/Journal.java
+++ b/journal/src/main/java/org/apache/cassandra/journal/Journal.java
@@ -72,10 +72,8 @@ import static 
org.apache.cassandra.utils.Simulate.With.MONITORS;
 
 /**
  * A generic append-only journal with some special features:
- * <p><ul>
- * <li>Records can be looked up by key
- * <li>Invalidated records get purged during segment compaction
- * </ul><p>
+ * - Records can be looked up by key.
+ * - Invalidated records get purged during segment compaction.
  *
  * Segment lifecycle:
  *
@@ -250,10 +248,10 @@ public class Journal<K, V> implements Shutdownable
 
     /**
      * Looks up a record by the provided id.
-     * <p/>
+     *
      * Looking up an invalidated record may or may not return a record, 
depending on
      * compaction progress.
-     * <p/>
+     *
      * In case multiple copies of the record exist in the log (e.g. because of 
user retries),
      * the first one found will be returned.
      *
@@ -322,10 +320,10 @@ public class Journal<K, V> implements Shutdownable
 
     /**
      * Looks up a record by the provided id, if the value satisfies the 
provided condition.
-     * <p/>
+     *
      * Looking up an invalidated record may or may not return a record, 
depending on
      * compaction progress.
-     * <p/>
+     *
      * In case multiple copies of the record exist in the log (e.g. because of 
user retries),
      * and more than one of them satisfy the provided condition, the first one 
found will be returned.
      *
@@ -370,10 +368,10 @@ public class Journal<K, V> implements Shutdownable
 
     /**
      * Looks up a record by the provided id.
-     * <p/>
+     *
      * Looking up an invalidated record may or may not return a record, 
depending on
      * compaction progress.
-     * <p/>
+     *
      * In case multiple copies of the record exist in the log (e.g. because of 
user retries),
      * only the first found record will be consumed.
      *
@@ -400,7 +398,7 @@ public class Journal<K, V> implements Shutdownable
 
     /**
      * Synchronously write a record to the journal.
-     * <p/>
+     *
      * Blocks until the record has been deemed durable according to the 
journal flush mode.
      *
      * @param id user-provided record id, expected to roughly correlate with 
time and go up
@@ -414,7 +412,7 @@ public class Journal<K, V> implements Shutdownable
     /**
      * Asynchronously write a record to the journal. Writes to the journal in 
the calling thread,
      * but doesn't wait for flush.
-     * <p/>
+     *
      * Executes the supplied callback on the executor provided once the record 
has been durably written to disk
      *
      * @param id user-provided record id, expected to roughly correlate with 
time and go up
diff --git a/journal/src/main/java/org/apache/cassandra/journal/KeySupport.java 
b/journal/src/main/java/org/apache/cassandra/journal/KeySupport.java
index efc41aa..d1f31e6 100644
--- a/journal/src/main/java/org/apache/cassandra/journal/KeySupport.java
+++ b/journal/src/main/java/org/apache/cassandra/journal/KeySupport.java
@@ -27,7 +27,6 @@ import org.apache.cassandra.io.util.DataOutputPlus;
 
 /**
  * Record keys must satisfy two properties:
- * <p>
  * 1. Must have a fixed serialized size
  * 2. Must be byte-order comparable
  */
diff --git 
a/journal/src/main/java/org/apache/cassandra/journal/OnDiskIndex.java 
b/journal/src/main/java/org/apache/cassandra/journal/OnDiskIndex.java
index 8c662d8..1fc5297 100644
--- a/journal/src/main/java/org/apache/cassandra/journal/OnDiskIndex.java
+++ b/journal/src/main/java/org/apache/cassandra/journal/OnDiskIndex.java
@@ -40,7 +40,7 @@ import static 
org.apache.cassandra.utils.FBUtilities.updateChecksumLong;
 
 /**
  * An on-disk (memory-mapped) index for a completed flushed segment.
- * <p/>
+ *
  * TODO (expected): block-level CRC
  */
 final class OnDiskIndex<K> extends Index<K>
diff --git 
a/journal/src/main/java/org/apache/cassandra/utils/UncheckedCloseable.java 
b/journal/src/main/java/org/apache/cassandra/utils/Closeable.java
similarity index 100%
rename from 
journal/src/main/java/org/apache/cassandra/utils/UncheckedCloseable.java
rename to journal/src/main/java/org/apache/cassandra/utils/Closeable.java
diff --git a/settings.gradle b/settings.gradle
index f498fc6..3ff1d6f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,5 @@
 rootProject.name = 'standalone-simulator'
+include 'test-utils'
 include 'simulator-context'
 include 'simulator-asm'
 include 'simulator-core'
diff --git 
a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java
 
b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java
index bf98363..562104d 100644
--- 
a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java
+++ 
b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java
@@ -37,11 +37,11 @@ import static 
org.apache.cassandra.simulator.asm.TransformationKind.THREAD_START
 /**
  * Redirects intercepted method calls and constructor instantiations at the 
bytecode level.
  *
- * <h3>Group A (built-in rules selected by transformation flags)</h3>
+ * Group A: built-in rules selected by transformation flags.
  * JVM-level primitives: {@code LockSupport.park/unpark}, {@code Thread.sleep},
  * {@code System.nanoTime/currentTimeMillis}, {@code UUID.randomUUID}, etc.
  *
- * <h3>Group B (configurable via {@code customRules})</h3>
+ * Group B: configurable via {@code customRules}.
  * Application-specific factory methods and constructor redirects registered 
via
  * {@code Simulator.intercept()}. Includes both Cassandra custom-factory 
interfaces
  * (e.g. {@code WaitQueue.newWaitQueue()}) and JDK concrete-class substitutions
diff --git 
a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IIsolatedExecutor.java
 
b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IIsolatedExecutor.java
index 75864d4..d73d158 100644
--- 
a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IIsolatedExecutor.java
+++ 
b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IIsolatedExecutor.java
@@ -62,6 +62,37 @@ public interface IIsolatedExecutor
     interface DynamicFunction<T> { <R extends T> R apply(R in); }
     interface SerializableDynamicFunction<T>                                   
                                   extends DynamicFunction<T>, Serializable {}
 
+    interface UncheckedSerializableRunnable extends SerializableRunnable {
+        default void run()
+        {
+            try
+            {
+                runThrowing();
+            }
+            catch (Throwable e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        void runThrowing() throws Throwable;
+    }
+
+    interface UncheckedRunnable extends Runnable {
+        default void run()
+        {
+            try
+            {
+                runThrowing();
+            }
+            catch (Throwable e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        void runThrowing() throws Throwable;
+    }
     // ── Execution 
────────────────────────────────────────────────────────────
 
     <O> Supplier<Future<O>>        supplyAsync(SerializableSupplier<O> call);
diff --git a/simulator-core/build.gradle b/simulator-core/build.gradle
index 5803824..c84260a 100644
--- a/simulator-core/build.gradle
+++ b/simulator-core/build.gradle
@@ -26,6 +26,7 @@ dependencies {
     implementation 'com.google.guava:guava:32.1.3-jre'
     implementation 'io.netty:netty-common:4.1.104.Final'
     implementation 'info.picocli:picocli:4.7.5'
+    testImplementation project(':test-utils')
     testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
     testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.36'
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java
index ad3aa0e..8ff99e3 100644
--- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java
+++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java
@@ -362,12 +362,12 @@ public abstract class Action implements PriorityQueueNode
     /** The immediate parent, and furthest ancestor of this Action */
     protected Action parent, origin = this, pseudoParent;
 
-    /** The number of direct consequences of this action that have not 
<i>transitively</i> terminated */
+    /** The number of direct consequences of this action that have not 
transitively terminated */
     private int childCount;
 
     /**
      * Consequences marked WITHHOLD are kept in their parent (or parent 
thread's) {@code withheld} queue until all
-     * other immediate children have <i>transitively</i> terminated their 
execution
+     * other immediate children have transitively terminated their execution
      */
     private DefaultPriorityQueue<Action> withheld;
 
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 298718d..709b976 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
@@ -72,26 +72,25 @@ import static 
org.apache.cassandra.simulator.InterceptHelper.methodDescriptor;
 /**
  * Thin orchestrator that wires together all simulator subsystems for testing.
  *
- * <p>The public API is intentionally narrow: create a simulator with a seed, 
start it,
+ * The public API is intentionally narrow: create a simulator with a seed, 
start it,
  * run one or more {@link IIsolatedExecutor.SerializableRunnable} bodies 
through the
- * ASM-transformed classloader, and stop it.  All thread creation, locking, 
and coordination
+ * ASM-transformed classloader, and stop it. All thread creation, locking, and 
coordination
  * inside the bodies use standard Java concurrency primitives ({@code 
synchronized},
  * {@link java.util.concurrent.CountDownLatch}, {@link Thread#start()}, etc.) 
which the
  * ASM transformer rewrites to simulator-controlled equivalents.
  *
- * <p>Typical usage:
- * <pre>
- *   try (Simulator sim = new Simulator(42L, "org/example/application/")) {
- *       sim.simulate((SerializableRunnable) () -&gt; {
- *           // natural Java concurrency - all intercepted by ASM
- *           CountDownLatch latch = new CountDownLatch(1);
- *           new Thread(() -&gt; { latch.countDown(); }).start();
- *           latch.await();
- *       });
- *   }
- * </pre>
+ * Typical usage:
  *
- * <p>The {@code monitorDelayChance} parameter controls how aggressively the 
simulator
+ *     try (Simulator sim = new Simulator(42L, "org/example/application/")) {
+ *         sim.simulate((SerializableRunnable) () -> {
+ *             // natural Java concurrency - all intercepted by ASM
+ *             CountDownLatch latch = new CountDownLatch(1);
+ *             new Thread(() -> { latch.countDown(); }).start();
+ *             latch.await();
+ *         });
+ *     }
+ *
+ * The {@code monitorDelayChance} parameter controls how aggressively the 
simulator
  * injects scheduling points at {@code synchronized} block entry.  A value of 
{@code 0f}
  * means no delays (threads acquire uncontended locks without yielding); 
{@code 1.0f}
  * means every {@code synchronized} entry yields, maximising interleaving 
exploration.
@@ -316,13 +315,13 @@ public class Simulator implements AutoCloseable
      * Register a simulation-aware replacement for a class or interface.
      * Must be called before {@link #ensureStarted()} (or before the first 
{@link #simulate} call).
      *
-     * <p><b>Interface target</b> (e.g. {@code WaitQueue.class}):
+     * Interface target (e.g. {@code WaitQueue.class}):
      * Scans the interface for {@link Intercept}-annotated static factory 
methods and generates
-     * {@link InterceptRule.FactoryMethod} ASM redirect rules for each one.  
Also validates that
-     * {@code implClass} overrides every {@link Intercept}-annotated 
<em>instance</em> method, or
+     * {@link InterceptRule.FactoryMethod} ASM redirect rules for each one. 
Also validates that
+     * {@code implClass} overrides every {@link Intercept}-annotated instance 
method, or
      * that unoverridden methods are annotated {@link NoSimulation} on the 
interface.
      *
-     * <p><b>Concrete class target</b> (e.g. {@code CountDownLatch.class}):
+     * Concrete class target (e.g. {@code CountDownLatch.class}):
      * Generates a {@link InterceptRule.Constructor} rule so that every {@code 
new target(...)}
      * in instrumented code creates an {@code implClass} instance instead.  
Also verifies that
      * {@code implClass} has constructors compatible with {@code target}'s 
public constructors.
@@ -487,16 +486,14 @@ public class Simulator implements AutoCloseable
      * {@link IsolatedClassLoader}, wrapping each in a simulated thread, and 
executing
      * them under scheduler control.
      *
-     * <p>Inside the bodies, standard Java concurrency primitives are 
transparently
+     * Inside the bodies, standard Java concurrency primitives are 
transparently
      * intercepted by the ASM transformer:
-     * <ul>
-     *   <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>
-     * </ul>
+     * - {@code CountDownLatch.await/countDown} becomes scheduler-controlled 
through instrumented AQS.
+     * - {@code thread.start()} becomes a scheduler-controlled thread start.
+     * - {@code synchronized} blocks use simulator-controlled monitor 
acquire/release.
+     * - {@code LockSupport.park/unpark} becomes scheduler-controlled 
park/unpark.
      *
-     * <p>Assertions thrown inside bodies propagate through {@link #close()}.
+     * Assertions thrown inside bodies propagate through {@link #close()}.
      *
      * @param bodies serializable runnables to execute as simulated threads
      * @return number of actions executed
@@ -550,7 +547,7 @@ public class Simulator implements AutoCloseable
     /**
      * Prepare a simulation for step-by-step execution.
      *
-     * <p>The bodies are serialized into an {@link IsolatedClassLoader}, 
wrapped in
+     * The bodies are serialized into an {@link IsolatedClassLoader}, wrapped 
in
      * simulated thread actions, and queued in an {@link ActionSchedule} - but 
nothing
      * executes until the caller drives the returned {@link Session}.
      *
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 b81d10e..9446cc9 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
@@ -50,6 +50,7 @@ public interface ObservableAction
         NOTIFY_CAPTURED,
         SLEEP_CAPTURED,
         NEMESIS_CAPTURED,
+        MONITOR_CONTENTION,
         PRE_IO_CAPTURED,
         TIMEOUT_FIRED,
         CUSTOM
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 6c630b3..25753c7 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
@@ -30,8 +30,7 @@ import java.util.function.Predicate;
  * stepping behavior.
  *
  * Tests that inspect state between actions must put that state in a class 
shared
- * across the simulation classloader boundary, such as
- * {@link org.apache.cassandra.simulator.context.SharedTestState}.
+ * across the simulation classloader boundary.
  *
  * Closing a session executes any remaining actions and releases the simulator.
  */
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptUtils.java
 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptUtils.java
new file mode 100644
index 0000000..0c17e25
--- /dev/null
+++ 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptUtils.java
@@ -0,0 +1,41 @@
+package org.apache.cassandra.simulator.systems;
+
+import java.util.function.Function;
+import org.apache.cassandra.simulator.utils.UncheckedInterruptedException;
+
+public class InterceptUtils
+{
+    /**
+     * Pause, shared (currently) between IO and monoitor waits; we do an 
unbounded wait in both cases
+     */
+    public static void pause(InterceptibleThread thread, InterceptedWait.Kind 
kind, boolean disabled, Function<Thread, InterceptedWait.CaptureSites> 
captureWaitSite)
+    {
+        InterceptedWait.InterceptedConditionWait signal = new 
InterceptedWait.InterceptedConditionWait(kind, 0L, thread, 
captureWaitSite.apply(thread), null);
+        thread.interceptWait(signal);
+
+        // Save interrupt state to restore afterwards; new interrupts only 
arrive when terminating the simulation.
+        boolean restoreInterrupt = Thread.interrupted();
+        try
+        {
+            while (true)
+            {
+                try
+                {
+                    signal.awaitDeclaredUninterruptible();
+                    return;
+                }
+                catch (InterruptedException e)
+                {
+                    if (disabled)
+                        throw new UncheckedInterruptedException(e);
+                    restoreInterrupt = true;
+                }
+            }
+        }
+        finally
+        {
+            if (restoreInterrupt)
+                thread.interrupt();
+        }
+    }
+}
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptibleConcurrentHashMap.java
 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptibleConcurrentHashMap.java
index a7884b6..3a2f62d 100644
--- 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptibleConcurrentHashMap.java
+++ 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptibleConcurrentHashMap.java
@@ -25,18 +25,16 @@ import java.util.concurrent.ConcurrentHashMap;
  * A {@link ConcurrentHashMap} subclass used by the ASM transformer as a 
drop-in
  * replacement when transforming user code under simulation.
  *
- * <p>The ASM transformer ({@code GlobalMethodTransformer}) rewrites every
+ * The ASM transformer ({@code GlobalMethodTransformer}) rewrites every
  * {@code new ConcurrentHashMap(...)} in instrumented classes to
  * {@code new InterceptibleConcurrentHashMap(...)} so that:
- * <ol>
- *   <li>{@code ThreadLocalRandom} probe calls inside the map are replaced with
- *       deterministic zero-probes (handled at the call-site level by the same
- *       transformer's {@code DETERMINISTIC} flag).</li>
- *   <li>The map instance is tagged as belonging to this simulation run, 
enabling
- *       future extensions (e.g. recording access patterns, injecting 
faults).</li>
- * </ol>
+ * - {@code ThreadLocalRandom} probe calls inside the map are replaced with
+ *   deterministic zero-probes, handled at the call-site level by the same
+ *   transformer's {@code DETERMINISTIC} flag.
+ * - The map instance is tagged as belonging to this simulation run, enabling
+ *   future extensions such as recording access patterns or injecting faults.
  *
- * <p>In the standalone simulator the class is intentionally minimal: it simply
+ * In the standalone simulator the class is intentionally minimal: it simply
  * delegates to the parent constructors.  The determinism guarantee comes from 
the
  * ASM rewriting of {@code ThreadLocalRandom.getProbe()} to {@code 0} that 
happens
  * at the call site (see {@code GlobalMethodTransformer.visitMethodInsn}), not 
from
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java
 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java
index ef82fc5..8ec1d3e 100644
--- 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java
+++ 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java
@@ -30,7 +30,6 @@ import javax.annotation.Nullable;
 import org.apache.cassandra.simulator.RandomSource;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import 
org.apache.cassandra.simulator.systems.InterceptedWait.CaptureSites.Capture;
-import 
org.apache.cassandra.simulator.systems.InterceptedWait.InterceptedConditionWait;
 import org.apache.cassandra.simulator.utils.Clock;
 
 import static 
org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DETERMINISM_CHECK;
@@ -158,7 +157,7 @@ public class InterceptingGlobalMethods extends 
InterceptingMonitors implements I
             return;
 
         thread.observe(ObservableAction.Kind.NEMESIS_CAPTURED);
-        pause(thread, NEMESIS);
+        InterceptUtils.pause(thread, NEMESIS, disabled, this::captureWaitSite);
     }
 
     @Override
@@ -169,38 +168,7 @@ public class InterceptingGlobalMethods extends 
InterceptingMonitors implements I
             return;
 
         thread.observe(ObservableAction.Kind.PRE_IO_CAPTURED);
-        pause(thread, PRE_IO);
-    }
-
-    private void pause(InterceptibleThread thread, InterceptedWait.Kind kind)
-    {
-        InterceptedConditionWait signal = new InterceptedConditionWait(kind, 
0L, thread, captureWaitSite(thread), null);
-        thread.interceptWait(signal);
-
-        // Save interrupt state to restore afterwards; new interrupts only 
arrive when terminating the simulation.
-        boolean restoreInterrupt = Thread.interrupted();
-        try
-        {
-            while (true)
-            {
-                try
-                {
-                    signal.awaitDeclaredUninterruptible();
-                    return;
-                }
-                catch (InterruptedException e)
-                {
-                    restoreInterrupt = true;
-                    if (disabled)
-                        return;
-                }
-            }
-        }
-        finally
-        {
-            if (restoreInterrupt)
-                thread.interrupt();
-        }
+        InterceptUtils.pause(thread, PRE_IO, disabled, this::captureWaitSite);
     }
 
     @Override
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java
 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java
index 12e13de..5243f74 100644
--- 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java
+++ 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java
@@ -31,7 +31,6 @@ import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.simulator.RandomSource;
 import org.apache.cassandra.simulator.step.ObservableAction;
-import 
org.apache.cassandra.simulator.systems.InterceptedWait.InterceptedConditionWait;
 import org.apache.cassandra.simulator.utils.Clock;
 import org.apache.cassandra.simulator.utils.Closeable;
 import org.apache.cassandra.simulator.utils.Threads;
@@ -706,29 +705,12 @@ public abstract class InterceptingMonitors implements 
InterceptorOfGlobalMethods
         boolean restoreInterrupt = Thread.interrupted();
         try
         {
-            if (   !thread.isEvaluationDeterministic()
-                && random.decide(preMonitorDelayChance))
+            if (!thread.isEvaluationDeterministic() && 
random.decide(preMonitorDelayChance))
             {
                 // TODO (feature): hold a stack of threads already paused by 
the nemesis, and, if one of the threads
                 //        is entering the monitor, put the contents of this 
stack into `waitingOn` for this monitor.
-                thread.observe(ObservableAction.Kind.NEMESIS_CAPTURED);
-                InterceptedConditionWait signal = new 
InterceptedConditionWait(NEMESIS, 0L, thread, captureWaitSite(thread), null);
-                thread.interceptWait(signal);
-
-                while (true)
-                {
-                    try
-                    {
-                        signal.awaitDeclaredUninterruptible();
-                        break;
-                    }
-                    catch (InterruptedException e)
-                    {
-                        if (disabled)
-                            throw new UncheckedInterruptedException(e);
-                        restoreInterrupt = true;
-                    }
-                }
+                thread.observe(ObservableAction.Kind.NEMESIS_CAPTURED, thread, 
"Nemesis on preMonitorEnter");
+                InterceptUtils.pause(thread, NEMESIS, disabled, 
this::captureWaitSite);
             }
 
             MonitorState state = state(monitor);
@@ -741,6 +723,7 @@ public abstract class InterceptingMonitors implements 
InterceptorOfGlobalMethods
                         throw new AssertionError("Thread " + thread + " is 
running but is not simulated");
 
                     checkForDeadlock(thread, state.heldBy);
+                    thread.observe(ObservableAction.Kind.MONITOR_CONTENTION, 
thread, "Unbounded wait on monitor contention");
                     InterceptedMonitorWait wait = new 
InterceptedMonitorWait(UNBOUNDED_WAIT, 0L, state, thread, 
captureWaitSite(thread));
                     wait.suspendedMonitorDepth = 1;
                     state.log("monitorenter_wait", thread);
diff --git 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/SimulatedWait.java
 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/SimulatedWait.java
index 4bd9b64..f34c1d1 100644
--- 
a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/SimulatedWait.java
+++ 
b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/SimulatedWait.java
@@ -36,22 +36,21 @@ import static 
org.apache.cassandra.simulator.systems.InterceptorOfGlobalMethods.
  * Returns {@code null} when not under simulation - callers should fall back 
to their
  * non-simulated blocking path.
  *
- * <h3>Usage in a blocking method</h3>
- * <pre>{@code
- *   public void await() throws InterruptedException {
- *       InterceptedConditionWait w = SimulatedWait.begin(Kind.UNBOUNDED_WAIT, 
0, inner);
- *       if (w == null) { inner.await(); return; }   // non-simulator path
- *       w.await();                                    // simulator path
- *   }
- * }</pre>
+ * Usage in a blocking method:
  *
- * <h3>Usage in a signalling method</h3>
- * <pre>{@code
- *   public void signal() {
- *       inner.signal();                                                       
// always
- *       if (pending != null) pending.interceptWakeup(SIGNAL, 
Thread.currentThread()); // simulator
- *   }
- * }</pre>
+ *     public void await() throws InterruptedException {
+ *         InterceptedConditionWait w = 
SimulatedWait.begin(Kind.UNBOUNDED_WAIT, 0, inner);
+ *         if (w == null) { inner.await(); return; }   // non-simulator path
+ *         w.await();                                  // simulator path
+ *     }
+ *
+ * Usage in a signalling method:
+ *
+ *     public void signal() {
+ *         inner.signal();
+ *         if (pending != null)
+ *             pending.interceptWakeup(SIGNAL, Thread.currentThread());
+ *     }
  */
 public final class SimulatedWait
 {
diff --git 
a/simulator-core/src/test/java/com/example/InterceptRuleMethodCallOutsideConfiguredPackagesTest.java
 
b/simulator-core/src/test/java/com/example/InterceptRuleMethodCallOutsideConfiguredPackagesTest.java
index 53a6ef0..8f9c28c 100644
--- 
a/simulator-core/src/test/java/com/example/InterceptRuleMethodCallOutsideConfiguredPackagesTest.java
+++ 
b/simulator-core/src/test/java/com/example/InterceptRuleMethodCallOutsideConfiguredPackagesTest.java
@@ -20,7 +20,7 @@ package com.example;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.utils.Intercept;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ConcurrentKVStoreSimTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ConcurrentKVStoreSimTest.java
deleted file mode 100644
index d18d896..0000000
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ConcurrentKVStoreSimTest.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.simulator.test;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-
-import org.apache.cassandra.simulator.Simulator;
-import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * A concurrent in-memory key-value store tested under the deterministic 
simulator.
- *
- * <h2>The system under test</h2>
- *
- * {@link BankLedger} is a concurrent key-value store mapping account names to 
integer
- * balances. It supports atomic single-key operations ({@code deposit}, {@code 
balance})
- * and a compound {@code transfer(from, to, amount)} that moves money between 
two accounts.
- *
- * Two transfer implementations are provided:
- * <ul>
- *   <li><b>{@code safeTransfer}</b> — acquires both locks in a consistent 
order (by account
- *       name) to prevent deadlocks and ensure atomicity.</li>
- *   <li><b>{@code buggyTransfer}</b> — acquires and releases locks 
independently for the
- *       source and destination, creating a TOCTOU window where concurrent 
transfers can
- *       corrupt balances.</li>
- * </ul>
- *
- * <h2>The test oracle</h2>
- *
- * The <b>conservation oracle</b> asserts that the total money in the system 
is invariant:
- * if we start with {@code N} accounts each holding {@code B} dollars, the sum 
of all
- * balances must remain {@code N * B} after any number of transfers — 
regardless of thread
- * interleaving.
- *
- * <h2>What the simulator does</h2>
- *
- * The ASM transformer rewrites {@code synchronized} blocks, {@code 
Thread.start()}, and
- * {@code CountDownLatch} operations so the simulator controls all scheduling. 
 With
- * {@code monitorDelayChance = 1.0f}, every {@code synchronized} entry is a 
scheduling
- * point: the simulator can interleave another thread between any two lock 
acquisitions.
- * This deterministically explores interleavings that would be astronomically 
unlikely
- * under real thread scheduling.
- */
-public class ConcurrentKVStoreSimTest
-{
-    // ════════════════════════════════════════════════════════════════════════
-    //  System Under Test: BankLedger
-    // ════════════════════════════════════════════════════════════════════════
-
-    /**
-     * A concurrent bank ledger backed by a {@link HashMap} with per-account 
locking.
-     *
-     * <p>Each account name is its own lock object (via the interned String or 
a dedicated
-     * lock map — here we use a separate {@code Object} per account in a lock 
table).
-     * This is simple striped locking: operations on different accounts don't 
contend.
-     *
-     * <p>Must be {@link Serializable} so it can cross the classloader 
boundary via
-     * {@link 
org.apache.cassandra.simulator.context.IsolatedExecutor#transferTo}.
-     */
-    static class BankLedger implements Serializable
-    {
-        private static final long serialVersionUID = 1L;
-
-        private final HashMap<String, Integer> balances = new HashMap<>();
-        private final HashMap<String, Object>  locks    = new HashMap<>();
-
-        void open(String account, int initialBalance)
-        {
-            balances.put(account, initialBalance);
-            locks.put(account, new Object());
-        }
-
-        private Object lockFor(String account)
-        {
-            return locks.get(account);
-        }
-
-        int balance(String account)
-        {
-            synchronized (lockFor(account))
-            {
-                return balances.getOrDefault(account, 0);
-            }
-        }
-
-        /**
-         * Correct transfer: acquires both locks in a consistent order 
(lexicographic
-         * by account name) to prevent deadlocks, and holds both for the entire
-         * read-modify-write, ensuring atomicity.
-         */
-        boolean safeTransfer(String from, String to, int amount)
-        {
-            if (from.equals(to) || amount <= 0) return false;
-
-            // Consistent lock ordering prevents deadlock
-            String first  = from.compareTo(to) < 0 ? from : to;
-            String second = from.compareTo(to) < 0 ? to   : from;
-
-            synchronized (lockFor(first))
-            {
-                synchronized (lockFor(second))
-                {
-                    int fromBal = balances.getOrDefault(from, 0);
-                    if (fromBal < amount) return false;
-                    int toBal = balances.getOrDefault(to, 0);
-                    balances.put(from, fromBal - amount);
-                    balances.put(to,   toBal   + amount);
-                    return true;
-                }
-            }
-        }
-
-        /**
-         * <b>Buggy</b> transfer: acquires and releases each lock 
independently.
-         *
-         * <p>The bug: after releasing the source lock and before acquiring 
the destination
-         * lock, another thread can read a stale source balance (or modify the 
destination).
-         * This creates a TOCTOU window that violates the conservation 
invariant.
-         *
-         * <pre>
-         *   Thread A: lock(src), read src=100, unlock(src)
-         *                                                     Thread B: 
lock(src), read src=100, unlock(src)
-         *   Thread A: lock(dst), read dst=0, write src=90, write dst=10, 
unlock(dst)
-         *                                                     Thread B: 
lock(dst), read dst=10, write src=90, write dst=20, unlock(dst)
-         *   Result: src=90, dst=20 → total=110 (was 100!) — $10 created from 
thin air
-         * </pre>
-         */
-        boolean buggyTransfer(String from, String to, int amount)
-        {
-            if (from.equals(to) || amount <= 0) return false;
-
-            int fromBal;
-            synchronized (lockFor(from))
-            {
-                fromBal = balances.getOrDefault(from, 0);
-                if (fromBal < amount) return false;
-            }
-            // ← TOCTOU gap: another thread can modify `from` or `to` here
-
-            synchronized (lockFor(to))
-            {
-                int toBal = balances.getOrDefault(to, 0);
-                // Write both — but `from` lock is not held, so this races
-                balances.put(from, fromBal - amount);
-                balances.put(to,   toBal + amount);
-                return true;
-            }
-        }
-
-        int totalBalance()
-        {
-            int total = 0;
-            for (int b : balances.values())
-                total += b;
-            return total;
-        }
-
-        Map<String, Integer> snapshot()
-        {
-            return new HashMap<>(balances);
-        }
-    }
-
-    // ════════════════════════════════════════════════════════════════════════
-    //  Test Oracle
-    // ════════════════════════════════════════════════════════════════════════
-
-    /**
-     * The conservation oracle: total money must be constant.
-     */
-    static void assertConservation(BankLedger ledger, int expectedTotal, 
String context)
-    {
-        int actual = ledger.totalBalance();
-        if (actual != expectedTotal)
-        {
-            fail("Conservation violated (" + context + "): expected total=" + 
expectedTotal
-                 + " but got " + actual + "; balances=" + ledger.snapshot());
-        }
-    }
-
-    // ════════════════════════════════════════════════════════════════════════
-    //  Tests
-    // ════════════════════════════════════════════════════════════════════════
-
-    static final int NUM_ACCOUNTS = 5;
-    static final int INITIAL_BALANCE = 1000;
-    static final int EXPECTED_TOTAL = NUM_ACCOUNTS * INITIAL_BALANCE;
-    static final int WORKERS = 4;
-    static final int TRANSFERS_PER_WORKER = 50;
-    static final String[] ACCOUNTS = { "alice", "bob", "carol", "dave", "eve" 
};
-
-    /**
-     * The safe transfer maintains the conservation invariant under all 
interleavings.
-     * We run with multiple seeds to exercise different scheduling orders.
-     */
-    @Test
-    void safeTransferMaintainsConservation()
-    {
-        for (long seed : new long[]{ 1L, 42L, 123L, 9999L })
-        {
-            runTransferTest(seed, true);
-        }
-    }
-
-    /**
-     * The buggy transfer violates the conservation invariant.  We try 
multiple seeds
-     * because the bug only manifests under specific interleavings — the 
simulator's
-     * deterministic scheduling makes it reproducible.
-     */
-    @Test
-    void buggyTransferViolatesConservation()
-    {
-        boolean foundViolation = false;
-        List<Long> triedSeeds = new ArrayList<>();
-
-        for (long seed = 1; seed <= 50 && !foundViolation; seed++)
-        {
-            triedSeeds.add(seed);
-            try
-            {
-                runTransferTest(seed, false);
-            }
-            catch (AssertionError e)
-            {
-                if (e.getMessage() != null && 
e.getMessage().contains("Conservation violated"))
-                {
-                    foundViolation = true;
-                    System.out.println("Seed " + seed + " found the bug: " + 
e.getMessage());
-                }
-                else
-                {
-                    throw e;  // unexpected failure
-                }
-            }
-        }
-
-        assertTrue(foundViolation,
-                   "Expected the buggy transfer to violate conservation under 
at least one seed. "
-                   + "Tried seeds: " + triedSeeds);
-    }
-
-    /**
-     * Determinism: the same seed produces the exact same outcome.
-     * The conservation oracle inside {@link #runTransferTest} asserts 
correctness;
-     * running with the same seed twice verifies the simulation is 
deterministic.
-     */
-    @Test
-    void sameSeedProducesSameResult()
-    {
-        runTransferTest(42L, true);
-        runTransferTest(42L, true);
-    }
-
-    private void runTransferTest(long seed, boolean useSafeTransfer)
-    {
-        // monitorDelayChance = 1.0f: every synchronized block entry is a 
scheduling point,
-        // maximising the interleavings the simulator explores.
-        Simulator.simulate(seed, 1.0f, "org/apache/cassandra/simulator/test/", 
(SerializableRunnable) () -> {
-            BankLedger ledger = new BankLedger();
-            for (String acct : ACCOUNTS)
-                ledger.open(acct, INITIAL_BALANCE);
-
-            // Verify initial state
-            assertConservation(ledger, EXPECTED_TOTAL, "before transfers");
-
-            CountDownLatch done = new CountDownLatch(WORKERS);
-
-            for (int w = 0; w < WORKERS; w++)
-            {
-                final int workerId = w;
-                new Thread(() -> {
-                    // Simple deterministic "random" based on worker id and 
iteration
-                    for (int i = 0; i < TRANSFERS_PER_WORKER; i++)
-                    {
-                        int fromIdx = (workerId + i) % NUM_ACCOUNTS;
-                        int toIdx   = (workerId + i + 1 + (i % (NUM_ACCOUNTS - 
1))) % NUM_ACCOUNTS;
-                        int amount  = 1 + (i % 20);
-
-                        if (useSafeTransfer)
-                            ledger.safeTransfer(ACCOUNTS[fromIdx], 
ACCOUNTS[toIdx], amount);
-                        else
-                            ledger.buggyTransfer(ACCOUNTS[fromIdx], 
ACCOUNTS[toIdx], amount);
-                    }
-                    done.countDown();
-                }, "worker-" + workerId).start();
-            }
-
-            try { done.await(); } catch (InterruptedException e) { 
Thread.currentThread().interrupt(); }
-
-            // Oracle check: total money must be conserved
-            assertConservation(ledger, EXPECTED_TOTAL, "after all transfers");
-        });
-    }
-}
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/DeterministicCounterTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/DeterministicCounterTest.java
index 369bcee..b48db3b 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/DeterministicCounterTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/DeterministicCounterTest.java
@@ -30,12 +30,12 @@ import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunn
  * blocks are fully serialised by the simulator and that the same seed always 
produces
  * the same interleaving.
  *
- * <p>The ASM transformer rewrites {@code synchronized} blocks into
+ * The ASM transformer rewrites {@code synchronized} blocks into
  * {@code preMonitorEnter}/{@code preMonitorExit} calls, putting all lock
  * acquisition under simulator scheduler control.  With {@code 
monitorDelayChance = 1.0f},
  * every lock acquisition yields to the scheduler, maximising interleaving 
exploration.
  *
- * <p>Assertions are placed inside the {@link SerializableRunnable} body 
rather than
+ * Assertions are placed inside the {@link SerializableRunnable} body rather 
than
  * outside, because the body is serialized and deserialized into the instance 
classloader:
  * any array captured from the outer scope would be a separate copy, so writes 
inside
  * the simulation would not be visible outside.  Throwing inside the body 
causes
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ExecutorServiceInterceptTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ExecutorServiceInterceptTest.java
index 549b164..a494ab1 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ExecutorServiceInterceptTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/ExecutorServiceInterceptTest.java
@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.systems.InterceptibleThread;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
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 6a9e3e9..ad516ac 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
@@ -38,26 +38,24 @@ import org.junit.jupiter.api.Test;
  * point at which the simulator can switch threads, and (b) correctly resumes 
the waiting
  * thread after the causally-required action has completed.
  *
- * <p>Cassandra-specific factory-method primitives (WaitQueue, Semaphore, 
Condition,
+ * Cassandra-specific factory-method primitives (WaitQueue, Semaphore, 
Condition,
  * Cassandra CDL) are tested separately in {@code integration-test} via
  * {@code PluggablePrimitiveIntegrationTest}.
  *
- * <h2>Pattern used in every test</h2>
+ * Pattern used in every test
  *
  * Each test runs two (or more) threads under the simulator:
- * <pre>
- *   Thread A  - reaches the blocking primitive, suspends
- *   Thread B  - logs "B-before-release", performs the release action, logs 
"B-after-release"
- * </pre>
+ *
+ *     Thread A  - reaches the blocking primitive, suspends
+ *     Thread B  - logs "B-before-release", performs the release action, logs 
"B-after-release"
+ *
  * After both threads complete we assert {@code "B-before-release"} appears in 
the event log
- * <em>before</em> {@code "A-after-resume"}.  This proves that:
- * <ol>
- *   <li>The primitive was a real scheduling point (B ran while A was 
blocked).</li>
- *   <li>The primitive correctly resumed A only after B performed the 
release.</li>
- *   <li>Whatever A does after resuming is causally downstream of B's 
action.</li>
- * </ol>
+ * before {@code "A-after-resume"}. This proves that:
+ * - The primitive was a real scheduling point because B ran while A was 
blocked.
+ * - The primitive correctly resumed A only after B performed the release.
+ * - Whatever A does after resuming is causally downstream of B's action.
  *
- * <h2>Why assertions live inside the lambda</h2>
+ * Why assertions live inside the lambda
  *
  * {@link Simulator#run(SerializableRunnable...)} serialises and deserialises 
each lambda
  * into a separate classloader, so arrays or references captured from the 
outer scope become
@@ -65,36 +63,34 @@ import org.junit.jupiter.api.Test;
  * therefore throw {@link AssertionError} from inside the simulation body; the 
error propagates
  * through the uncaught-exception handler and is re-thrown by {@link 
Simulator#stop()}.
  *
- * <h2>Why Thread.currentThread() is captured for park/unpark</h2>
+ * Why Thread.currentThread() is captured for park/unpark
  *
  * When {@code new Thread(r).start()} is ASM-rewritten to
  * {@link 
org.apache.cassandra.simulator.systems.InterceptorOfGlobalMethods.Global#start},
  * the simulator creates a new {@code InterceptibleThread} to run the body -- 
the original
  * {@code Thread} object is never started.  {@code LockSupport.unpark} 
requires the
- * <em>running</em> thread, so we capture {@code Thread.currentThread()} (the
+ * running thread, so we capture {@code Thread.currentThread()} (the
  * {@code InterceptibleThread}) from inside the parker body and share it via a
  * {@code CountDownLatch}-guarded slot.
  *
- * <h2>Primitives covered (Group A - JDK only)</h2>
- * <ol>
- *   <li>{@link Thread#start()} -- via ASM to {@code 
InterceptorOfGlobalMethods.Global.start}</li>
- *   <li>{@code synchronized} block -- via ASM to {@code preMonitorEnter / 
preMonitorExit}</li>
- *   <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} -- 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>
- *   <li>{@link Thread#sleep(long, int)} -- same, nanos variant</li>
- *   <li>{@link TimeUnit#sleep(long)} -- via ASM to {@code 
InterceptorOfSystemMethods.Global.sleep}</li>
- *   <li>Nemesis -- explicit call to {@code 
InterceptorOfSystemMethods.Global.nemesis(1.0f)}</li>
- *   <li>{@link UUID#randomUUID()} -- deterministic, intercepted for 
reproducibility</li>
- *   <li>{@link Clock#nanoTime()} -- simulated monotonic clock</li>
- *   <li>{@link ConcurrentHashMap} (constructor rewritten to {@code 
InterceptibleConcurrentHashMap})</li>
- *   <li>{@link IdentityHashMap} (constructor rewritten to {@code 
InterceptedIdentityHashMap})</li>
- * </ol>
+ * Primitives covered (Group A - JDK only)
+ * - {@link Thread#start()} via ASM to {@code 
InterceptorOfGlobalMethods.Global.start}.
+ * - {@code synchronized} block via ASM to {@code preMonitorEnter / 
preMonitorExit}.
+ * - {@link Object#wait()} via ASM to {@code 
InterceptorOfSystemMethods.Global.wait}.
+ * - {@link Object#notify()} via ASM to {@code 
InterceptorOfSystemMethods.Global.notify}.
+ * - {@link Object#notifyAll()} via ASM to {@code 
InterceptorOfSystemMethods.Global.notifyAll}.
+ * - JDK {@link java.util.concurrent.CountDownLatch}, with blocking controlled 
through
+ *   instrumented AQS {@code LockSupport.park/unpark} calls.
+ * - {@link LockSupport#park()} via ASM to {@code InterceptibleThread.park}.
+ * - {@link LockSupport#parkNanos(long)} through the same mechanism.
+ * - {@link Thread#sleep(long)} via ASM to {@code 
InterceptorOfSystemMethods.Global.sleep}.
+ * - {@link Thread#sleep(long, int)} through the same mechanism for the nanos 
variant.
+ * - {@link TimeUnit#sleep(long)} via ASM to {@code 
InterceptorOfSystemMethods.Global.sleep}.
+ * - Nemesis through an explicit call to {@code 
InterceptorOfSystemMethods.Global.nemesis(1.0f)}.
+ * - {@link UUID#randomUUID()}, intercepted for deterministic reproducibility.
+ * - {@link Clock#nanoTime()}, using the simulated monotonic clock.
+ * - {@link ConcurrentHashMap}, whose constructor is rewritten to {@code 
InterceptibleConcurrentHashMap}.
+ * - {@link IdentityHashMap}, whose constructor is rewritten to {@code 
InterceptedIdentityHashMap}.
  */
 public class InstrumentedPrimitivesTest
 {
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleIntegrationTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleIntegrationTest.java
index 30abf90..2d7d81e 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleIntegrationTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleIntegrationTest.java
@@ -20,7 +20,7 @@ package org.apache.cassandra.simulator.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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.utils.Intercept;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleMethodCallTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleMethodCallTest.java
index 39ee27b..0d926ea 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleMethodCallTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptRuleMethodCallTest.java
@@ -20,7 +20,7 @@ package org.apache.cassandra.simulator.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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.utils.Intercept;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/FullyInstrumentedTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptibleThreadTest.java
similarity index 98%
rename from 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/FullyInstrumentedTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptibleThreadTest.java
index 4cd0b8c..8721e22 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/FullyInstrumentedTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/InterceptibleThreadTest.java
@@ -34,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * Verifies that threads created inside the ASM-transformed classloader are
  * {@link InterceptibleThread} instances, confirming full instrumentation.
  */
-public class FullyInstrumentedTest
+public class InterceptibleThreadTest
 {
     @Test
     void spawnedThreadIsInterceptible()
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/MonitorInvariantTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/MonitorInvariantTest.java
index f53dcea..4ebd0f0 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/MonitorInvariantTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/MonitorInvariantTest.java
@@ -31,7 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
  * Verifies that {@code synchronized}, {@code wait()}, and {@code 
notify()/notifyAll()}
  * are intercepted and behave correctly under the simulator.
  *
- * <p>All tests run through the ASM-transformed classloader.  The ASM 
transformer rewrites
+ * All tests run through the ASM-transformed classloader. The ASM transformer 
rewrites
  * monitor operations so the simulator controls thread scheduling at every 
lock boundary.
  */
 public class MonitorInvariantTest
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/TimeDeterminismTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/TimeDeterminismTest.java
index 70a5440..efbb419 100644
--- 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/TimeDeterminismTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/TimeDeterminismTest.java
@@ -31,7 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
  * Verifies that simulated time is deterministic: the same seed produces the 
same
  * {@code System.nanoTime()} values, and different seeds may produce different 
values.
  *
- * <p>The ASM transformer rewrites {@code System.nanoTime()} and
+ * The ASM transformer rewrites {@code System.nanoTime()} and
  * {@code System.currentTimeMillis()} to simulator-controlled time sources.
  */
 public class TimeDeterminismTest
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/step/CountDownLatchSimTest.java
similarity index 98%
rename from 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/CountDownLatchSimTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/CountDownLatchSimTest.java
index 20abc78..35baeda 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/step/CountDownLatchSimTest.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.test;
+package org.apache.cassandra.simulator.test.step;
 
 import java.util.concurrent.CountDownLatch;
 
@@ -24,7 +24,7 @@ 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.testutils.SharedTestState;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
 import org.apache.cassandra.simulator.systems.InterceptingCountDownLatch;
diff --git 
a/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
new file mode 100644
index 0000000..9e1d521
--- /dev/null
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/MonitorEnterTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.simulator.test.step;
+
+import java.util.concurrent.CountDownLatch;
+import org.apache.cassandra.simulator.Simulator;
+import org.apache.cassandra.simulator.context.IIsolatedExecutor;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
+import org.apache.cassandra.simulator.step.ObservableAction;
+import org.apache.cassandra.simulator.step.Session;
+import org.junit.jupiter.api.Test;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies that instrumented MONITORENTER operations create scheduler actions.
+ */
+public class MonitorEnterTest extends StepApiTestSupport
+{
+    @Test
+    void synchronizedBlock_monitorContentionIsObservable()
+    {
+        try (Simulator sim = new Simulator(42L, 0.0f, 
"org/apache/cassandra/simulator/test/");
+             Session session = 
sim.byStep((IIsolatedExecutor.UncheckedSerializableRunnable) () -> {
+                 Object lock = new Object();
+                 CountDownLatch aEntered = new CountDownLatch(1);
+                 CountDownLatch releaseA = new CountDownLatch(1);
+
+                 Thread a = new Thread((IIsolatedExecutor.UncheckedRunnable) 
() -> {
+                     synchronized (lock)
+                     {
+                         SharedTestState.eventCount.incrementAndGet();
+                         aEntered.countDown();
+                         releaseA.await();
+                     }
+                 }, "A");
+
+                 Thread b = new Thread((IIsolatedExecutor.UncheckedRunnable) 
() -> {
+                     aEntered.await();
+                     releaseA.countDown();
+                     synchronized (lock)
+                     {
+                         SharedTestState.eventCount.incrementAndGet();
+                     }
+                 }, "B");
+
+                 a.start();
+                 b.start();
+
+                 a.join();
+                 b.join();
+             }).enableDebug())
+        {
+            ObservableAction blocked = 
session.stepUntil(ObservableAction.Kind.MONITOR_CONTENTION).orElseThrow();
+
+            assertTrue(blocked.threadName().contains("B"));
+            assertEquals(1, SharedTestState.eventCount.get(), "only A ran 
while it held the monitor");
+
+            session.runToEnd();
+
+            assertEquals(2, SharedTestState.eventCount.get(), "both threads 
entered the synchronized block");
+        }
+    }
+
+}
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
similarity index 98%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
index 809f6a7..b187fb2 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/NemesisYieldTest.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
similarity index 98%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
index 37faa43..10d28b8 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ObjectWaitNotifyTest.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
similarity index 95%
copy from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
copy to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
index c191cf5..82de36a 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/StepApiTestSupport.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import org.junit.jupiter.api.BeforeEach;
 
 public abstract class StepApiTestSupport
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
similarity index 98%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
index 687aa65..2003c54 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadSleepTest.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
diff --git 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
similarity index 97%
rename from 
integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
rename to 
simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
index a6f2ecf..2ae5e60 100644
--- 
a/integration-test/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
+++ 
b/simulator-core/src/test/java/org/apache/cassandra/simulator/test/step/ThreadStartTest.java
@@ -19,7 +19,7 @@
 package org.apache.cassandra.simulator.test.step;
 
 import org.apache.cassandra.simulator.Simulator;
-import org.apache.cassandra.simulator.context.SharedTestState;
+import org.apache.cassandra.simulator.testutils.SharedTestState;
 import 
org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable;
 import org.apache.cassandra.simulator.step.ObservableAction;
 import org.apache.cassandra.simulator.step.Session;
@@ -46,7 +46,7 @@ public class ThreadStartTest extends StepApiTestSupport
     @Test
     void threadStart_workerBodyRunsInSeparateStep()
     {
-        try (Simulator sim = simulator(42L);
+        try (Simulator sim = StepApiTestSupport.simulator(42L);
              Session session = sim.byStep((SerializableRunnable) () ->
                  new Thread(() -> SharedTestState.threadRan.incrementAndGet(), 
"worker").start())
                                   .enableDebug())
diff --git a/test-utils/build.gradle b/test-utils/build.gradle
new file mode 100644
index 0000000..e387c4d
--- /dev/null
+++ b/test-utils/build.gradle
@@ -0,0 +1 @@
+description = 'Dependency-free shared support for simulator tests'
diff --git 
a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java
 
b/test-utils/src/main/java/org/apache/cassandra/simulator/testutils/SharedTestState.java
similarity index 74%
rename from 
simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java
rename to 
test-utils/src/main/java/org/apache/cassandra/simulator/testutils/SharedTestState.java
index edf132f..d7f565c 100644
--- 
a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java
+++ 
b/test-utils/src/main/java/org/apache/cassandra/simulator/testutils/SharedTestState.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.simulator.context;
+package org.apache.cassandra.simulator.testutils;
 
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
@@ -24,19 +24,15 @@ import java.util.concurrent.atomic.AtomicLong;
 /**
  * Shared mutable state for step-by-step simulator tests.
  *
- * This class MUST live in the {@code org.apache.cassandra.simulator.context} 
package
- * because IsolatedClassLoader.DEFAULT_SHARED_FILTER hard-codes that package
- * as always-shared.  When instrumented code running inside the instance 
classloader
- * touches {@code SharedTestState.threadRan}, the JVM delegates the class 
lookup to
- * the shared (system) classloader, returning the same {@code Class} object -- 
and
- * therefore the same static field instance -- that the outer test thread sees.
+ * This class lives outside {@code org.apache.cassandra.simulator.test} so that
+ * Simulator delegates its loading to the shared classloader. Instrumented 
code and
+ * the test harness therefore access the same static fields across the 
classloader boundary.
  *
  * Usage:
- * <pre>
- *   SharedTestState.reset();   // in @BeforeEach
- *   // ... run simulation steps ...
- *   assertEquals(1, SharedTestState.threadRan.get());
- * </pre>
+ *
+ *     SharedTestState.reset();   // in @BeforeEach
+ *     // ... run simulation steps ...
+ *     assertEquals(1, SharedTestState.threadRan.get());
  */
 public class SharedTestState
 {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to