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.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Guardrails for module `async-test-analysis` are maintained in that module's own
<project_guardrails>
<locked_files>
<file path="se.deversity.asynctest.DetectorType">
<reason>Each enum constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) both branches of AsyncTestConfig.build() (detectAll block + excludes block), and (5) DetectorRegistry constructor. Adding a value here in isolation breaks the system.</reason>
<reason>Adding or removing a constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) the resolution line in AsyncTestConfig.build() ((detectAll || flag) &amp;&amp; !excludes.contains(TYPE)), and (5) DetectorRegistry constructor. Adding a value here in isolation compiles and detects nothing. The lock is on the constant set, not the file: editing javadoc on existing constants cannot break that invariant and needs no ceremony.</reason>
</file>
</locked_files>

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,13 +439,28 @@ See [intellij-plugin/README.md](intellij-plugin/README.md) for full instructions
|---|---|
| CI (any `GITHUB_ACTIONS` or `CI` env var set, no key) | Auto-mocked — tests run freely |
| Local, no key, `-Dlicense.mock.mode=true` | Mock mode active — tests run freely |
| Local, no key, no mock flag | License gate runs; outcome depends on the configured backend |
| Local, no key, no mock flag | **The gate runs and can refuse.** See below |
| Real key via `-Dlicense.key=<key>` | Full validation against the licensing backend |

> **First run on a new machine.** With no key configured and no mock flag, the gate consults the
> licensing backend and a denial throws, before any test body runs:
>
> ```
> java.lang.SecurityException: LICENSE DENIED: <reason>
> To run locally without a key: -Dlicense.mock.mode=true
> In CI (GITHUB_ACTIONS or CI env var set, no key): mock mode activates automatically.
> ```
>
> This is the gate working as intended, not a bug in your test. CI is unaffected: mock mode turns
> itself on there when no key is present, which is why a suite that passes in CI can still stop on
> a developer laptop.

To run locally without a key during development:
```
mvn test -Dlicense.mock.mode=true
```
Or add to your IDE's JVM args: `-Dlicense.mock.mode=true`
Or add to your IDE's JVM args: `-Dlicense.mock.mode=true`. Setting it once in your IDE's default
JUnit configuration is the usual fix, so it applies to every run rather than being remembered
per-test.

Set your email identity when using a real key: `-Dlicense.user.email=you@example.com`
6 changes: 3 additions & 3 deletions async-test-lib/.claude/rules/async-test-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ paths: ["**/DetectorType.java", "**/AsyncTestConfig.java", "**/DetectorRegistry.
## Locked Status

### se.deversity.asynctest.DetectorType
- **Reason**: Each enum constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) both branches of AsyncTestConfig.build() (detectAll block + excludes block), and (5) DetectorRegistry constructor. Adding a value here in isolation breaks the system.
- **Reason**: Adding or removing a constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) the resolution line in AsyncTestConfig.build() ((detectAll || flag) && !excludes.contains(TYPE)), and (5) DetectorRegistry constructor. Adding a value here in isolation compiles and detects nothing. The lock is on the constant set, not the file: editing javadoc on existing constants cannot break that invariant and needs no ceremony.

## Mirrored — Keep In Sync

### se.deversity.asynctest.DetectorType
- **Rule**: Free to change, but every mirror must change in the same commit.
- **Mirrors**: se.deversity.asynctest.AsyncTest, se.deversity.asynctest.AsyncTestConfig, se.deversity.asynctest.DetectorRegistry, se.deversity.asynctest.spi.LegacyDetectorFactories, META-INF/services/se.deversity.asynctest.spi.DetectorFactory
- **Reason**: A detector is only reachable from the public API when all of these agree. The enum constant is the name users type in @AsyncTest(excludes=...); the annotation attribute, the config field and its Builder default carry it through resolution; the registry constructor instantiates it; and the SPI factory plus its services entry are what detectAll loads. Adding the constant alone compiles and silently detects nothing.
- **Mirrors**: se.deversity.asynctest.AsyncTest, se.deversity.asynctest.AsyncTestConfig, se.deversity.asynctest.DetectorRegistry, se.deversity.asynctest.spi.LegacyDetectorFactories, META-INF/async-test/builtin-detector-factories
- **Reason**: A detector is only reachable from the public API when all of these agree. The enum constant is the name users type in @AsyncTest(excludes=...); the annotation attribute, the config field and its Builder default carry it through resolution; the registry constructor instantiates it; and the SPI factory plus its entry in the built-in factory list are what detectAll loads. Adding the constant alone compiles and silently detects nothing.
- **Enforced by**: se.deversity.asynctest.spi.AllDetectorsSpiCoverageTest

## Context & Focus
Expand Down
2 changes: 1 addition & 1 deletion async-test-lib/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ you are editing here.
<project_guardrails>
<locked_files>
<file path="se.deversity.asynctest.DetectorType">
<reason>Each enum constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) both branches of AsyncTestConfig.build() (detectAll block + excludes block), and (5) DetectorRegistry constructor. Adding a value here in isolation breaks the system.</reason>
<reason>Adding or removing a constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) the resolution line in AsyncTestConfig.build() ((detectAll || flag) &amp;&amp; !excludes.contains(TYPE)), and (5) DetectorRegistry constructor. Adding a value here in isolation compiles and detects nothing. The lock is on the constant set, not the file: editing javadoc on existing constants cannot break that invariant and needs no ceremony.</reason>
</file>
</locked_files>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,24 @@ public void awaitDone(Duration timeout) {
}

/**
* Get result.
*
* @return the future's resolved value, or {@code null} if it has not completed
* successfully yet (or completed exceptionally)
*/
public @Nullable T getResult() { return result.get(); }

/**
* Get error.
*
* @return the exception the future completed with, or {@code null} if it has not
* completed exceptionally
*/
public @Nullable Throwable getError() { return error.get(); }

/**
* Is complete.
*
* @return {@code true} once the observed future has completed, successfully or not
*/
public boolean isComplete() { return complete; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,12 @@ private AsyncTestConfig(Builder b) {
licenseMockMode = b.licenseMockMode;
}

/** Builds a config from an {@link AsyncTest} annotation instance. */
/**
* Builds a config from an {@link AsyncTest} annotation instance.
*
* @param ann the annotation instance to read the declared values from
* @return the resolved configuration for this run
*/
public static AsyncTestConfig from(AsyncTest ann) {
return from(ann, ann.threads());
}
Expand All @@ -540,6 +545,10 @@ public static AsyncTestConfig from(AsyncTest ann) {
* other annotation fields.
*
* @since 1.6.0
*
* @param ann the annotation instance to read the declared values from
* @param threadsOverride thread count to use instead of {@link AsyncTest#threads()}, as supplied by a parameterised template
* @return the resolved configuration for this run
*/
public static AsyncTestConfig from(AsyncTest ann, int threadsOverride) {
// Check for global benchmarking system property
Expand Down Expand Up @@ -725,7 +734,9 @@ public static AsyncTestConfig from(AsyncTest ann, int threadsOverride) {
.build();
}

/** {@return a new builder initialised with the library defaults} */
/**
* {@return a new builder initialised with the library defaults}
*/
public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -1768,6 +1779,9 @@ public Builder excludes(DetectorType[] v) {
* {@link #excludes(DetectorType[])} still layers on top.
*
* @since 1.7.0
*
* @param v the detectors to enable exclusively; {@code null} or empty leaves the selection untouched
* @return this builder
*/
public Builder includes(DetectorType[] v) {
if (v != null && v.length > 0) {
Expand All @@ -1776,7 +1790,9 @@ public Builder includes(DetectorType[] v) {
return this;
}

/** {@return the resolved configuration, with preset, includes and excludes applied} */
/**
* {@return the resolved configuration, with preset, includes and excludes applied}
*/
public AsyncTestConfig build() {
if (!includes.isEmpty()) {
// includes wins over detectAll/per-flag setters: force the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,11 @@ public final class AsyncTestContext {
// Exposed via atomicityValidator() so se.deversity.asynctest.telemetry.TelemetryBridge
// can route drained agent field-access events into the live per-test detector.
final @Nullable AtomicityValidator atomicityValidator;

/**
* Creates a AsyncTestContext.
*
* @param cfg the resolved configuration deciding which detectors this context installs
*/
public AsyncTestContext(AsyncTestConfig cfg) {
this.registry = new DetectorRegistry(cfg);
// Mirror registry references so package-private field access still works
Expand Down Expand Up @@ -630,13 +634,19 @@ public AsyncTestContext(AsyncTestConfig cfg) {

// ---- Lifecycle (called by ConcurrencyRunner) ----

/** Installs {@code ctx} into the calling thread's ThreadLocal. */
/**
* Installs {@code ctx} into the calling thread's ThreadLocal.
*
* @param ctx the context to bind to the calling thread; must be paired with an {@code uninstall()} in a {@code finally}
*/
@AICallersOnly({"se.deversity.asynctest.runner.ConcurrencyRunner"})
public static void install(AsyncTestContext ctx) {
CURRENT.set(ctx);
}

/** Removes the context from the calling thread's ThreadLocal. */
/**
* Removes the context from the calling thread's ThreadLocal.
*/
@AIIdempotent(reason = "ThreadLocal.remove() is documented as a no-op when the thread has no value set; the install/uninstall symmetry rule (CLAUDE.md) tolerates extra uninstalls. ConcurrencyRunner relies on this in its outermost-finally cleanup.")
public static void uninstall() {
CURRENT.remove();
Expand Down Expand Up @@ -682,7 +692,11 @@ public static long replaySeed() {
return ctx == null ? 0L : ctx.currentRoundSeed;
}

/** Internal: set by {@code ConcurrencyRunner} before each invocation round. */
/**
* Internal: set by {@code ConcurrencyRunner} before each invocation round.
*
* @param seed the seed for this round, so a reported interleaving can be replayed
*/
public void setReplaySeedForRound(long seed) {
this.currentRoundSeed = seed;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public static void fireTestFailed(Throwable cause) {
* <p>Severity is parsed from the report text using {@link IssueSeverity} markers
* (emoji or keyword). Reports with no recognisable marker default to {@link IssueSeverity#HIGH}.
*
* @param detectorName the detector name
* @param detectorName the reporting detector, as it appears in the report
* @param report the report content
*/
public static void fireDetectorReport(String detectorName, String report) {
Expand Down
Loading
Loading