Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# async-test-lib — Usage Guide

**async-test-lib** is a JUnit 5 extension for stress-testing concurrent Java code. It forces real thread collisions using a `CyclicBarrier`, then runs 148 specialized detectors across 18 phases to identify exactly what went wrong — including six JDK 25/26 detectors (Phases 16 and 18: `StableValue`, `StructuredTaskScope`, parallel `Gatherer`, `LazyConstant`, final-field mutation, shared `KDF`) you can also drive directly via AsyncTestContext accessors.
**async-test-lib** is a JUnit 5 extension for stress-testing concurrent Java code. It forces real thread collisions using a `CyclicBarrier`, then runs 149 specialized detectors across 18 phases to identify exactly what went wrong — including six JDK 25/26 detectors (Phases 16 and 18: `StableValue`, `StructuredTaskScope`, parallel `Gatherer`, `LazyConstant`, final-field mutation, shared `KDF`) you can also drive directly via AsyncTestContext accessors.

- Replaces `@Test` with `@AsyncTest` — zero other changes needed
- Requires Java 21 and JUnit 5 (Jupiter 6.0.3+)
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/regression-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ you are on a clean branch.
Anything it prints that section 3 does not describe is a new consumer. Add it there.

Most of its output is not consumers. It also lists every checkout under `/c/dev/private`,
including this repo's own worktrees (the fixtures, corpus-eval and 150 examples each, repeated
including this repo's own worktrees (the fixtures, corpus-eval and 151 examples each, repeated
per worktree) and stale worktrees of consumers such as `vibetags-cov`, which still pin old
versions. Filter to the repo names in section 3 before reading versions.

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The script rewrites the exact current version string where it is a pin, in an al
`docs/QUICK_REFERENCE.md`, `docs/DISTRIBUTION.md`, `.claude/SKILL.md`.

`versions/pom.xml`'s `<async-test.version>` is the one pin every build outside the reactor
resolves: the 150 examples, both consumer fixtures and the corpus inherit it, and their Gradle
resolves: the 151 examples, both consumer fixtures and the corpus inherit it, and their Gradle
builds read it. It must move with the release. Those builds resolve `mavenLocal()` before
`mavenCentral()`, and CI publishes the working tree there before building them, so a pin left on
an older version silently resolves that **old release from Central** and passes while proving
Expand Down
7 changes: 7 additions & 0 deletions .gemini/rules/async-test-detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
### se.deversity.asynctest.diagnostics.JdbcConnectionSharedDetector
- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/JdbcConnectionSharedDetectorTest.java

### se.deversity.asynctest.diagnostics.KotlinMutexReentrancyDetector
- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/KotlinMutexReentrancyDetectorTest.java

### se.deversity.asynctest.diagnostics.LambdaLostUpdateDetector
- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/LambdaLostUpdateDetectorTest.java

Expand Down Expand Up @@ -210,6 +213,10 @@
- **Strategy**: OTHER
- **Note**: ConcurrentHashMap-backed JDBC-resource tracking; per-resource State holds ConcurrentHashMap.newKeySet() for accessing threads.

### se.deversity.asynctest.diagnostics.KotlinMutexReentrancyDetector
- **Strategy**: OTHER
- **Note**: Per-mutex state in the weakly keyed ConcurrentHashMap of AbstractInstanceDetector; the holdings are a ConcurrentLinkedDeque of weakly held Jobs, and a reentry writes its description before its count.

### se.deversity.asynctest.diagnostics.LambdaLostUpdateDetector
- **Strategy**: OTHER
- **Note**: ConcurrentHashMap keyed on lambda identity plus captured name; events are appended to a copy-on-write list. The rule groups by observed pre-value and needs no ordering. holdsLock is sampled on the recording thread at record time, which is the only place it means anything.
Expand Down
2 changes: 1 addition & 1 deletion .gemini/rules/async-test-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

### se.deversity.asynctest.agent.AsyncTestAgent
- **Sensitivity**: Critical
- **Note**: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave accesses and double-count every one. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while no weaver adds a member, and there are four besides the Advice, which is a method-entry prologue. FieldAccessWeaver inserts a stack-neutral, branch-free call before each field instruction, a load of the monitor plus a call at the entry of a synchronized method and before each of its returns (#822), a DUP of the monitor, an ALOAD of the local the owner was loaded from, an LDC of the field and a call before a MONITORENTER whose monitor the four instructions before it read from an instance field (ALOAD, GETFIELD, DUP, ASTORE, with no label between, #793), a DUP plus call after a VarHandle reference getAndSet it does not substitute (the ownership take, #555), DUP2 and SWAP plus a call before a JCTools offer, and a DUP before plus DUP_X1 and SWAP plus a call after a JCTools poll (#664), a copy of owner and value plus a call before a volatile field store and after a volatile field load, with a DUP of the receiver before an instance load (#742), and replaces an int VarHandle, AtomicIntegerFieldUpdater, AtomicBoolean or AtomicInteger store, swap or value-returning release, or a reference-slot store or getAndSet on an AtomicReference, its updater, its array or an instance-field VarHandle, or an acquiring reference read, an AtomicReference get or getAcquire, an updater get, an AtomicReferenceArray get or getAcquire or a VarHandle getVolatile or getAcquire (the acquire of the store it returned, #741), or an AtomicStampedReference get, getReference, set, compareAndSet, weakCompareAndSet or attemptStamp (#817), or an AtomicInteger get or an AtomicLong get, store or compare-and-set (a get then set, #1157), with a static hook consuming the same stack and returning the same result, plus one POP where a VarHandle call site declared a void result and one CHECKCAST to the declared type after a VarHandle reference getAndSet or acquiring read (the spinlock, #554, #558, #658, #667; the ownership offer and take, #664). CollectionAccessWeaver replaces each call its tables list with a static hook consuming the same stack, and inserts only the monitor loads and the loop back-edge call its own rules describe. ThreadConstructionWeaver inserts a DUP plus a call after the constructor call of a new Thread(...), and an ALOAD 0 plus a call after a Thread subclass constructor's superclass call (#737). ConstructionWeaver inserts a call that consumes and returns the order argument before a LinkedHashMap(int, float, boolean) constructor call and a DUP or an ALOAD 0 plus a call after it (#807), a DUP plus a call after a new CompletableFuture() constructor call (#1054), and an ALOAD 0, an LDC of the class name and a call before each RETURN of a constructor and after a this(...) delegation (#791); it leaves alone a class in a named module that cannot read the library. InterruptHandlerWeaver inserts an LDC of the locations of a method's InterruptedException handlers that neither rethrow nor restore the flag, and a call, before the first instruction of a method that declares one, and leaves alone a class in a named module that cannot read the library. PersistenceHandleWeaver inserts, before a call whose owner is a JPA EntityManager or Hibernate Session by name, a stack-neutral copy of the receiver (DUP; SWAP, DUP_X1; or DUP2_X1, POP2, DUP_X2 for zero, one or two one-slot arguments) and a call, and leaves calls with more or wider arguments alone (#992). RunBlockingWeaver inserts, before a static call of kotlinx.coroutines.BuildersKt.runBlockingK, runBlocking or their $default bridges by name, one call of a ()V hook that takes and leaves nothing, in every woven class outside kotlinx.coroutines (#1007). So frames stay valid and only maxStack grows (COMPUTE_MAXS, never COMPUTE_FRAMES, which would load classes from inside the agent). In every mode, install and the transformer open java.util.concurrent.locks, with fields=true also java.util.concurrent.atomic, and with collections=true also java.util and java.io (a synchronized wrapper's mutex, #996; a piped writer's sink, #994), to one module per woven loader and to nothing else: the module of the TelemetryRegistry copy that loader resolves, never the woven loader or its ancestors as such (UpdaterAccess, #659, #668, #855); the locks opening lets the library read a ReentrantLock owner at analysis. In every mode, a woven class in a named module gets a read edge to the library copy its loader resolves (UpdaterAccess.letRead, #862): the JVM's own edge for a transformed class reaches only the agent loader's unnamed module. The library resolves an updater bound before the attach by reading the JDK implementation from that copy (JdkUpdaterShapeCanaryTest pins the shape it reads), and the opening must fail silently like the rest of install. Nothing may throw out of premain — an exception there aborts JVM startup, which is why install() catches Throwable and releases the gate rather than propagating. Both modes use AgentBuilder.InitializationStrategy.NoOp: no weaver needs a loaded-type initializer, and Byte Buddy's default self-injection gave every class woven at load a <clinit> that loads net.bytebuddy.dynamic.Nexus through the system class loader, whose lock virtual threads initializing classes at once contend on and pin (#984). The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar.
- **Note**: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave accesses and double-count every one. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while no weaver adds a member, and there are four besides the Advice, which is a method-entry prologue. FieldAccessWeaver inserts a stack-neutral, branch-free call before each field instruction, a load of the monitor plus a call at the entry of a synchronized method and before each of its returns (#822), a DUP of the monitor, an ALOAD of the local the owner was loaded from, an LDC of the field and a call before a MONITORENTER whose monitor the four instructions before it read from an instance field (ALOAD, GETFIELD, DUP, ASTORE, with no label between, #793), a DUP plus call after a VarHandle reference getAndSet it does not substitute (the ownership take, #555), DUP2 and SWAP plus a call before a JCTools offer, and a DUP before plus DUP_X1 and SWAP plus a call after a JCTools poll (#664), a copy of owner and value plus a call before a volatile field store and after a volatile field load, with a DUP of the receiver before an instance load (#742), and replaces an int VarHandle, AtomicIntegerFieldUpdater, AtomicBoolean or AtomicInteger store, swap or value-returning release, or a reference-slot store or getAndSet on an AtomicReference, its updater, its array or an instance-field VarHandle, or an acquiring reference read, an AtomicReference get or getAcquire, an updater get, an AtomicReferenceArray get or getAcquire or a VarHandle getVolatile or getAcquire (the acquire of the store it returned, #741), or an AtomicStampedReference get, getReference, set, compareAndSet, weakCompareAndSet or attemptStamp (#817), or an AtomicInteger get or an AtomicLong get, store or compare-and-set (a get then set, #1157), with a static hook consuming the same stack and returning the same result, plus one POP where a VarHandle call site declared a void result and one CHECKCAST to the declared type after a VarHandle reference getAndSet or acquiring read (the spinlock, #554, #558, #658, #667; the ownership offer and take, #664). CollectionAccessWeaver replaces each call its tables list with a static hook consuming the same stack, and inserts only the monitor loads and the loop back-edge call its own rules describe. ThreadConstructionWeaver inserts a DUP plus a call after the constructor call of a new Thread(...), and an ALOAD 0 plus a call after a Thread subclass constructor's superclass call (#737). ConstructionWeaver inserts a call that consumes and returns the order argument before a LinkedHashMap(int, float, boolean) constructor call and a DUP or an ALOAD 0 plus a call after it (#807), a DUP plus a call after a new CompletableFuture() constructor call (#1054), and an ALOAD 0, an LDC of the class name and a call before each RETURN of a constructor and after a this(...) delegation (#791); it leaves alone a class in a named module that cannot read the library. InterruptHandlerWeaver inserts an LDC of the locations of a method's InterruptedException handlers that neither rethrow nor restore the flag, and a call, before the first instruction of a method that declares one, and leaves alone a class in a named module that cannot read the library. PersistenceHandleWeaver inserts, before a call whose owner is a JPA EntityManager or Hibernate Session by name, a stack-neutral copy of the receiver (DUP; SWAP, DUP_X1; or DUP2_X1, POP2, DUP_X2 for zero, one or two one-slot arguments) and a call, and leaves calls with more or wider arguments alone (#992). RunBlockingWeaver inserts, before a static call of kotlinx.coroutines.BuildersKt.runBlockingK, runBlocking or their $default bridges by name, one call of a ()V hook that takes and leaves nothing, in every woven class outside kotlinx.coroutines (#1007). KotlinMutexWeaver replaces a kotlinx.coroutines.sync.Mutex lock, unlock or tryLock call, or its static $default bridge, matched by owner, name and descriptor, with a static hook consuming the same stack and returning the same result, in every woven class outside kotlinx.coroutines (#1007). So frames stay valid and only maxStack grows (COMPUTE_MAXS, never COMPUTE_FRAMES, which would load classes from inside the agent). In every mode, install and the transformer open java.util.concurrent.locks, with fields=true also java.util.concurrent.atomic, and with collections=true also java.util and java.io (a synchronized wrapper's mutex, #996; a piped writer's sink, #994), to one module per woven loader and to nothing else: the module of the TelemetryRegistry copy that loader resolves, never the woven loader or its ancestors as such (UpdaterAccess, #659, #668, #855); the locks opening lets the library read a ReentrantLock owner at analysis. In every mode, a woven class in a named module gets a read edge to the library copy its loader resolves (UpdaterAccess.letRead, #862): the JVM's own edge for a transformed class reaches only the agent loader's unnamed module. The library resolves an updater bound before the attach by reading the JDK implementation from that copy (JdkUpdaterShapeCanaryTest pins the shape it reads), and the opening must fail silently like the rest of install. Nothing may throw out of premain — an exception there aborts JVM startup, which is why install() catches Throwable and releases the gate rather than propagating. Both modes use AgentBuilder.InitializationStrategy.NoOp: no weaver needs a loaded-type initializer, and Byte Buddy's default self-injection gave every class woven at load a <clinit> that loads net.bytebuddy.dynamic.Nexus through the system class loader, whose lock virtual threads initializing classes at once contend on and pin (#984). The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar.

## Contract-Frozen Signature
- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/corpus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name: Corpus Eval
#
# Its own workflow rather than a job in e2e-tests.yml, because this eval answers a question about
# the product rather than about the build: whether a finding on somebody else's code means their
# code is wrong. It publishes numbers that the README and the trust tiers both rest on - 34
# code is wrong. It publishes numbers that the README and the trust tiers both rest on - 35
# detectors carry VERDICT on the strength of pairs measured here - so it earns a history and a
# badge of its own, and a workflow_dispatch for running it against a library change without waiting
# for the whole end-to-end suite.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ jobs:

# 2. A library change edits no examples/** path, but every example consumes the built
# artifact, so one can break all of them while this filter sees nothing. Run a
# deterministic every-4th sample, 38 of the 150 examples: enough to catch a
# deterministic every-4th sample, 38 of the 151 examples: enough to catch a
# systemic break at PR time, cheap enough to afford on every library PR. The full
# reactor still runs on push and nightly, so this moves discovery earlier rather
# than adding coverage.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gradle-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ jobs:

# 2. A library change edits no examples/** path, but every example consumes the built
# artifact, so one can break all of them while this filter sees nothing. Run a
# deterministic every-4th sample, 38 of the 150 examples: enough to catch a
# deterministic every-4th sample, 38 of the 151 examples: enough to catch a
# systemic break at PR time, cheap enough to afford on every library PR. The full
# reactor still runs on push and nightly, so this moves discovery earlier rather
# than adding coverage.
Expand Down
Loading
Loading