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) () -> { - * // natural Java concurrency - all intercepted by ASM - * CountDownLatch latch = new CountDownLatch(1); - * new Thread(() -> { 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]
