From fe3969c2369fe862d0922c8c9642570c595fba2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Burstr=C3=B6m?= Date: Sat, 28 Mar 2026 10:38:26 +0100 Subject: [PATCH 1/5] chore: Bump PMD tool version to 7.0.0 --- build.gradle.kts | 1 + config/pmd/rulesets.xml | 13 +------------ .../davidburstrom/contester/ConTesterDriver.java | 4 ++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a7a57a5..c95c917 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,6 +75,7 @@ allprojects { apply(plugin = "pmd") configure { + toolVersion = "7.0.0" ruleSets = listOf() ruleSetConfig = resources.text.fromFile(rootProject.file("config/pmd/rulesets.xml")) } diff --git a/config/pmd/rulesets.xml b/config/pmd/rulesets.xml index 6aaf80a..5de8b51 100644 --- a/config/pmd/rulesets.xml +++ b/config/pmd/rulesets.xml @@ -15,16 +15,13 @@ - - - @@ -36,7 +33,6 @@ - @@ -49,23 +45,16 @@ - - - - - - - @@ -86,7 +75,6 @@ - @@ -95,6 +83,7 @@ + diff --git a/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java b/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java index 6cffe2e..4aa1260 100644 --- a/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java +++ b/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java @@ -622,7 +622,7 @@ private static Set getEnabledBreakpoints(final Thread thread) { } /** Holds the data associated with a given driver thread, e.g. a test worker thread. */ - private static class DriverData { + private static final class DriverData { private final Map threadRegistry = new WeakHashMap<>(); private final Map> enabledBreakpoints = new HashMap<>(4); @@ -641,7 +641,7 @@ private Map> getEnabledBreakpoints() { } } - private static class ThreadData { + private static final class ThreadData { private Throwable uncaughtThrowable; private String breakpointId; From bd1a97c8059bd1b108d97d914c2249e9f396f131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Burstr=C3=B6m?= Date: Sat, 28 Mar 2026 13:05:13 +0100 Subject: [PATCH 2/5] Bump PMD to 7.5.0, make the library compatible with virtual threads --- build.gradle.kts | 2 +- .../contester/ConTesterDriver.java | 57 ++++++++++++++----- .../contester/ConTesterDriverTest.java | 32 +++++++++++ .../contester/examples/Modification.java | 13 ++++- 4 files changed, 87 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c95c917..68c5538 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,7 +75,7 @@ allprojects { apply(plugin = "pmd") configure { - toolVersion = "7.0.0" + toolVersion = "7.5.0" ruleSets = listOf() ruleSetConfig = resources.text.fromFile(rootProject.file("config/pmd/rulesets.xml")) } diff --git a/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java b/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java index 4aa1260..e649350 100644 --- a/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java +++ b/driver/src/main/java/io/github/davidburstrom/contester/ConTesterDriver.java @@ -29,6 +29,8 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -59,7 +61,6 @@ * using {@link #join} and {@link #getUncaughtThrowable}. * */ -@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") public final class ConTesterDriver { /** Standard timeout, in milliseconds, for blocking APIs. */ @@ -67,6 +68,7 @@ public final class ConTesterDriver { private static final Map DRIVER_REGISTRY = Collections.synchronizedMap(new WeakHashMap<>()); + private static final ReentrantLock DRIVER_REGISTRY_LOCK = new ReentrantLock(); /** Prohibit instantiation */ private ConTesterDriver() {} @@ -271,11 +273,14 @@ public static void waitForBreakpoint(Thread thread, String id, long timeout, Tim final long endTime = System.nanoTime() + timeUnit.toNanos(timeout); final ThreadData threadData = driverData.getThreadRegistry().get(thread); - synchronized (threadData) { + threadData.lock.lock(); + try { + Condition condition = threadData.lock.newCondition(); while (threadData.getSuspended() == null && System.nanoTime() < endTime) { try { - threadData.wait(1); + //noinspection ResultOfMethodCallIgnored + condition.awaitNanos(1); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -300,6 +305,8 @@ public static void waitForBreakpoint(Thread thread, String id, long timeout, Tim throw new AssertionError( "Thread suspended on unexpected breakpoint '" + threadData.getSuspended()); } + } finally { + threadData.lock.unlock(); } } @@ -391,13 +398,16 @@ public static void waitForBlockedOrTerminated(Thread thread, long timeout, TimeU public static void resume(Thread thread) { final DriverData driverData = DRIVER_REGISTRY.get(Thread.currentThread()); final ThreadData threadData = driverData.getThreadRegistry().get(thread); - synchronized (threadData) { + threadData.lock.lock(); + try { if (threadData.getSuspended() != null) { threadData.setSuspended(null); threadData.semaphore.release(); } else { throw new AssertionError("Thread is not suspended"); } + } finally { + threadData.lock.unlock(); } } @@ -428,8 +438,11 @@ public static void join(final Thread thread, long timeout, TimeUnit timeUnit) { final DriverData driverData = DRIVER_REGISTRY.get(Thread.currentThread()); final Map> enabledBreakpoints = driverData.getEnabledBreakpoints(); - synchronized (enabledBreakpoints) { + driverData.lock.lock(); + try { enabledBreakpoints.forEach((id, threads) -> threads.remove(thread)); + } finally { + driverData.lock.unlock(); } if (isSuspended(thread)) { @@ -517,18 +530,23 @@ static void visitBreakpoint(String id, BooleanSupplier condition) { final Map> enabledBreakpoints = driverData.get().getEnabledBreakpoints(); final ThreadData threadData = driverData.get().getThreadRegistry().get(Thread.currentThread()); final boolean suspend; - synchronized (enabledBreakpoints) { + driverData.get().lock.lock(); + try { final Set set = enabledBreakpoints.get(id); if (set != null && set.contains(Thread.currentThread()) && condition.getAsBoolean()) { - synchronized (threadData) { + threadData.lock.lock(); + try { suspend = true; threadData.setSuspended(id); // This can only be mutation tested by injecting a custom wait time in waitForBreakpoint - threadData.notifyAll(); + } finally { + threadData.lock.unlock(); } } else { suspend = false; } + } finally { + driverData.get().lock.unlock(); } if (suspend) { try { @@ -540,8 +558,11 @@ static void visitBreakpoint(String id, BooleanSupplier condition) { } private static DriverData getOrCreateDriverData() { - synchronized (DRIVER_REGISTRY) { + DRIVER_REGISTRY_LOCK.lock(); + try { return DRIVER_REGISTRY.computeIfAbsent(Thread.currentThread(), t -> new DriverData()); + } finally { + DRIVER_REGISTRY_LOCK.unlock(); } } @@ -550,12 +571,14 @@ private static Optional findThreadData(Thread thread) { if (DRIVER_REGISTRY.isEmpty()) { return Optional.empty(); } - - synchronized (DRIVER_REGISTRY) { + DRIVER_REGISTRY_LOCK.lock(); + try { return DRIVER_REGISTRY.values().stream() .map(driverData -> driverData.getThreadRegistry().get(thread)) .filter(Objects::nonNull) .findFirst(); + } finally { + DRIVER_REGISTRY_LOCK.unlock(); } } @@ -564,12 +587,15 @@ private static Optional findDriverData(Thread thread) { if (DRIVER_REGISTRY.isEmpty()) { return Optional.empty(); } + DRIVER_REGISTRY_LOCK.lock(); + try { - synchronized (DRIVER_REGISTRY) { return DRIVER_REGISTRY.entrySet().stream() .filter(entry -> entry.getValue().getThreadRegistry().containsKey(thread)) .findFirst() .map(Map.Entry::getValue); + } finally { + DRIVER_REGISTRY_LOCK.unlock(); } } @@ -602,8 +628,11 @@ private static void startIfNecessary(final Thread thread) { private static boolean isSuspended(Thread thread) { final DriverData driverData = DRIVER_REGISTRY.get(Thread.currentThread()); final ThreadData threadData = driverData.getThreadRegistry().get(thread); - synchronized (threadData) { + threadData.lock.lock(); + try { return threadData.getSuspended() != null; + } finally { + threadData.lock.unlock(); } } @@ -627,6 +656,7 @@ private static final class DriverData { private final Map threadRegistry = new WeakHashMap<>(); private final Map> enabledBreakpoints = new HashMap<>(4); private final AtomicInteger threadIdGenerator = new AtomicInteger(1); + private final ReentrantLock lock = new ReentrantLock(); private int getNextThreadId() { return threadIdGenerator.getAndIncrement(); @@ -646,6 +676,7 @@ private static final class ThreadData { private Throwable uncaughtThrowable; private String breakpointId; private final Semaphore semaphore = new Semaphore(0); + private final ReentrantLock lock = new ReentrantLock(); Optional getUncaughtThrowable() { return Optional.ofNullable(uncaughtThrowable); diff --git a/driver/src/test/java/io/github/davidburstrom/contester/ConTesterDriverTest.java b/driver/src/test/java/io/github/davidburstrom/contester/ConTesterDriverTest.java index 54e2505..63f0797 100644 --- a/driver/src/test/java/io/github/davidburstrom/contester/ConTesterDriverTest.java +++ b/driver/src/test/java/io/github/davidburstrom/contester/ConTesterDriverTest.java @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -385,6 +386,7 @@ void threadIsAutomaticallyRegistered() { assertDoesNotThrow(() -> join(thread)); } + @SuppressWarnings("PMD.AvoidSynchronizedStatement") @Test void runUntilBlocked() { Object lock = new Object(); @@ -409,6 +411,36 @@ void runUntilBlocked() { join(thread2); } + @Test + void runUntilWaiting() { + ReentrantLock lock = new ReentrantLock(); + final Thread thread1 = + thread( + () -> { + lock.lock(); + try { + visitBreakpoint("id"); + } finally { + lock.unlock(); + } + }); + final Thread thread2 = + thread( + () -> { + lock.lock(); + try { + visitBreakpoint("dummy"); + } finally { + lock.unlock(); + } + }); + runToBreakpoint(thread1, "id"); + runUntilBlockedOrTerminated(thread2); + assertEquals(Thread.State.WAITING, thread2.getState()); + join(thread1); + join(thread2); + } + @Test void runUntilBlockedOrTerminatedResumesAutomatically() { final Thread thread = thread(() -> visitBreakpoint("id")); diff --git a/examples/src/main/java/io/github/davidburstrom/contester/examples/Modification.java b/examples/src/main/java/io/github/davidburstrom/contester/examples/Modification.java index 96823c1..91bde3d 100644 --- a/examples/src/main/java/io/github/davidburstrom/contester/examples/Modification.java +++ b/examples/src/main/java/io/github/davidburstrom/contester/examples/Modification.java @@ -16,6 +16,7 @@ package io.github.davidburstrom.contester.examples; import io.github.davidburstrom.contester.ConTesterBreakpoint; +import java.util.concurrent.locks.ReentrantLock; /** * Simulates an issue where one method is modifying a field while another method is referencing it, @@ -49,25 +50,31 @@ public void print() { } class Fixed implements Modification { - final Object lock = new Object(); + final ReentrantLock lock = new ReentrantLock(); private Object member = new Object(); @Override public void reset() { ConTesterBreakpoint.defineBreakpoint("reset"); - synchronized (lock) { + lock.lock(); + try { member = null; + } finally { + lock.unlock(); } } @Override public void print() { /* this synchronized block is introduced during a bug fix, and is proven to work */ - synchronized (lock) { + lock.lock(); + try { if (member != null) { ConTesterBreakpoint.defineBreakpoint("print"); System.out.println(member.getClass()); } + } finally { + lock.unlock(); } } } From 933531d63926c3f5c2c0cf0bf6b514d14dde6b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Burstr=C3=B6m?= Date: Sat, 28 Mar 2026 13:06:17 +0100 Subject: [PATCH 3/5] build: Declare the pmdVersion more centrally --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 68c5538..cfe9c27 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ val errorProneVersion = "2.48.0" val ktlintVersion = "1.8.0" val pitestMainVersion = "1.23.0" val pitestJUnit5PluginVersion = "1.2.3" +val pmdVersion = "7.5.0" ext["jmhVersion"] = "1.37" configurations { @@ -75,7 +76,7 @@ allprojects { apply(plugin = "pmd") configure { - toolVersion = "7.5.0" + toolVersion = pmdVersion ruleSets = listOf() ruleSetConfig = resources.text.fromFile(rootProject.file("config/pmd/rulesets.xml")) } From dbf9e5e2c8845231394e65e03fa7d3b024ca7a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Burstr=C3=B6m?= Date: Sat, 28 Mar 2026 13:08:46 +0100 Subject: [PATCH 4/5] chore: Bump PMD to 7.7.0 --- build.gradle.kts | 2 +- config/pmd/rulesets.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index cfe9c27..8ccb337 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ val errorProneVersion = "2.48.0" val ktlintVersion = "1.8.0" val pitestMainVersion = "1.23.0" val pitestJUnit5PluginVersion = "1.2.3" -val pmdVersion = "7.5.0" +val pmdVersion = "7.7.0" ext["jmhVersion"] = "1.37" configurations { diff --git a/config/pmd/rulesets.xml b/config/pmd/rulesets.xml index 5de8b51..2123791 100644 --- a/config/pmd/rulesets.xml +++ b/config/pmd/rulesets.xml @@ -7,9 +7,9 @@ - - - + + + From 6c02a2772f90d44d02c317af671dacd5bd92d3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Burstr=C3=B6m?= Date: Sat, 28 Mar 2026 13:14:25 +0100 Subject: [PATCH 5/5] chore: Bump PMD to 7.23.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8ccb337..15425a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ val errorProneVersion = "2.48.0" val ktlintVersion = "1.8.0" val pitestMainVersion = "1.23.0" val pitestJUnit5PluginVersion = "1.2.3" -val pmdVersion = "7.7.0" +val pmdVersion = "7.23.0" ext["jmhVersion"] = "1.37" configurations {