Skip to content

Commit 7745420

Browse files
Update javadoc
1 parent 99214f0 commit 7745420

16 files changed

Lines changed: 95 additions & 231 deletions

File tree

README.md

Lines changed: 56 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
![Selective Test Runner](docs/images/logo.png)
22

3-
A Maven plugin that tracks which production classes each test touches at the bytecode level, then uses Git to detect what changed and runs only the affected tests. Zero annotations. Zero config changes to your tests. Just add the plugin and watch your feedback loop shrink.
3+
A Maven plugin that tracks which production classes each test touches at the bytecode level, then uses Git to detect what changed and runs only the affected tests. No annotations, no config changes to your tests.
44

55
## Why?
66

7-
Large Maven projects waste minutes (or hours) re-running thousands of tests when only a handful of source files changed. This plugin fixes that:
7+
Large Maven projects spend a lot of time re-running tests when only a few source files changed. This plugin addresses that:
88

9-
- **Bytecode-level precision**: instruments every method entry via a Java agent, so it catches dependencies that static analysis misses (reflection, polymorphism, lambdas).
10-
- **Git-aware**: diffs your working tree against the last commit, last tag, or last full run to find changed files.
11-
- **Zero test changes**: works with JUnit 4, JUnit 5, and TestNG out of the box. No annotations, no base classes, no test rewrites.
12-
- **Safe by default**: when in doubt, runs everything. Missing coverage data? Full run. Git error? Full run. The plugin never silently skips tests.
13-
- **Multi-module ready**: supports reactors with shared coverage maps and concurrent-safe writes under `mvn -T`.
9+
- Instruments every method entry via a Java agent, catching dependencies that static analysis misses (reflection, polymorphism, lambdas)
10+
- Diffs your working tree against the last commit, last tag, or last full run to find changed files
11+
- Works with JUnit 4, JUnit 5, and TestNG without requiring annotations, base classes, or test rewrites
12+
- Falls back to a full run when something is uncertain (missing coverage data, git errors, etc.)
13+
- Supports multi-module reactors with shared coverage maps and concurrent writes under `mvn -T`
1414

1515
## Quick start
1616

@@ -39,27 +39,13 @@ Then run your build as usual:
3939
mvn verify
4040
```
4141

42-
**First run:** all tests execute and coverage is recorded. **Every run after:** only tests affected by your changes are selected. That's it.
42+
On the first run all tests execute and coverage is recorded. On subsequent runs only tests affected by your changes are selected.
4343

4444
## How it works
4545

46-
```
47-
process-test-classes test verify
48-
┌──────────────────────┐ ┌─────────────────────┐ ┌───────────────────┐
49-
│ │ │ │ │ │
50-
git diff ──> │ collect: attach │ │ Surefire runs │ │ report: merge │
51-
changed files │ Java agent to │ │ only selected │ │ coverage dump │
52-
│ │ Surefire's argLine │ │ tests │ │ into shared map │
53-
│ │ │ │ │ │ │
54-
└──────> │ select: intersect │ │ Agent records │ │ Emit JSON report │
55-
│ changes with │ │ which classes │ │ + console summary│
56-
│ coverage map │ │ each test touches │ │ │
57-
└──────────────────────┘ └─────────────────────┘ └───────────────────┘
58-
```
59-
60-
1. **Collect**: attaches a Java agent to Surefire's forked JVM. The agent instruments every method entry in your production and test classes using ASM bytecode rewriting.
61-
2. **Select**: uses JGit to detect changed `.java` files, resolves them to compiled classes (including inner classes), looks up the coverage map to find which tests touch those classes, and sets Surefire's `test` filter.
62-
3. **Report**: merges the per-module coverage dump into the shared coverage map (JSON) and writes a human-readable summary.
46+
1. **Collect** (`process-test-classes`): attaches a Java agent to Surefire's forked JVM. The agent instruments method entries in your production and test classes using ASM.
47+
2. **Select** (`process-test-classes`): uses JGit to detect changed `.java` files, resolves them to compiled classes (including inner classes), looks up the coverage map to find which tests touch those classes, and sets Surefire's `test` filter.
48+
3. **Report** (`verify`): merges the per-module coverage dump into the shared coverage map (JSON) and prints a summary.
6349

6450
## Configuration
6551

@@ -86,64 +72,60 @@ mvn verify
8672
| Parameter | Default | Description |
8773
|-----------|---------|-------------|
8874
| `baseline` | `lastCommit` | Git baseline for change detection. `lastCommit` = HEAD vs HEAD~1, `lastTag` = HEAD vs most recent tag, `lastFullRun` = working tree vs the commit recorded in the coverage map. |
89-
| `fullRunInterval` | `50` | Force a periodic full run every N incremental builds. Set to `0` to disable. |
90-
| `includes` | *(auto)* | Comma-separated package prefixes to instrument. Auto-detected from your source roots if omitted. |
75+
| `fullRunInterval` | `50` | Force a full run every N incremental builds. Set to `0` to disable. |
76+
| `includes` | *(auto)* | Comma-separated package prefixes to instrument. Auto-detected from source roots if omitted. |
9177
| `excludes` | *(empty)* | Comma-separated package prefixes to exclude from instrumentation. |
9278
| `failOnEmptySelection` | `false` | If `true`, fail the build when no tests match the changed classes instead of falling back to a full run. |
9379
| `coverageMapPath` | `<reactor-root>/target/.test-impact/coverage.json` | Override the coverage map location. |
9480

9581
## Baseline strategies
9682

97-
Choose the right baseline for your workflow:
98-
99-
| Strategy | Best for | How it works |
83+
| Strategy | Use case | How it works |
10084
|----------|----------|--------------|
101-
| `lastCommit` | **CI / pull requests** | Diffs HEAD against HEAD~1. Each push re-evaluates. |
102-
| `lastTag` | **Release pipelines** | Diffs HEAD against the most recent Git tag by timestamp. |
103-
| `lastFullRun` | **Local development** | Diffs the working tree against the commit hash recorded in the coverage map from the last full test run. |
85+
| `lastCommit` | CI / pull requests | Diffs HEAD against HEAD~1. Each push re-evaluates. |
86+
| `lastTag` | Release pipelines | Diffs HEAD against the most recent Git tag by timestamp. |
87+
| `lastFullRun` | Local development | Diffs the working tree against the commit hash recorded in the coverage map from the last full test run. |
10488

10589
## Multi-module reactors
10690

107-
The plugin works out of the box with multi-module Maven projects, including parallel builds (`mvn -T`):
91+
The plugin supports multi-module Maven projects, including parallel builds (`mvn -T`):
10892

109-
- **Shared coverage map** at the reactor root (`target/.test-impact/coverage.json`)
110-
- **Per-module dumps**: each module's Surefire JVM writes its own binary dump
111-
- **Concurrency-safe merges**: `report` uses a JVM monitor + OS-level `FileLock` for safe concurrent writes
112-
- **Dependency-aware filtering**: `select` uses `MavenSession.getProjectDependencyGraph()` to only consider changes in upstream modules
93+
- Shared coverage map at the reactor root (`target/.test-impact/coverage.json`)
94+
- Each module's Surefire JVM writes its own binary dump
95+
- `report` uses a JVM monitor + OS-level `FileLock` for concurrent writes
96+
- `select` uses `MavenSession.getProjectDependencyGraph()` to only consider changes in upstream modules
11397

114-
## Safety guarantees
98+
## Fallback behaviour
11599

116-
The plugin is designed to **never silently skip tests**. It falls back to a full run when:
100+
The plugin falls back to a full run when:
117101

118102
- No coverage map exists (first run)
119103
- Coverage map version doesn't match the plugin version
120104
- Coverage map is older than `fullRunInterval` builds
121105
- Git change detection fails
122-
- The change set is empty (ambiguous state)
106+
- The change set is empty
123107
- No tests intersect with the changed classes
124108

125-
This means you can adopt the plugin incrementally with confidence. The worst case is running all tests, same as without the plugin.
109+
The worst case is running all tests, same as without the plugin.
126110

127111
## Supported test frameworks
128112

129113
The agent detects test methods by annotation:
130114

131115
| Framework | Annotations |
132116
|-----------|------------|
133-
| **JUnit 5** | `@Test`, `@ParameterizedTest`, `@RepeatedTest`, `@TestFactory`, `@TestTemplate` |
134-
| **JUnit 4** | `@Test` |
135-
| **TestNG** | `@Test` |
117+
| JUnit 5 | `@Test`, `@ParameterizedTest`, `@RepeatedTest`, `@TestFactory`, `@TestTemplate` |
118+
| JUnit 4 | `@Test` |
119+
| TestNG | `@Test` |
136120

137-
No configuration needed: all three are detected automatically.
138-
139-
## Goals reference
121+
## Goals
140122

141123
| Goal | Phase | Description |
142124
|------|-------|-------------|
143-
| `test-impact:collect` | `process-test-classes` | Attaches the Java agent to Surefire's `argLine` for bytecode instrumentation |
144-
| `test-impact:select` | `process-test-classes` | Detects changed sources and sets Surefire's `test` filter to impacted tests only |
145-
| `test-impact:report` | `verify` | Merges coverage dump into the shared map, generates JSON report and console summary |
146-
| `test-impact:invalidate` | *(manual)* | Clears the coverage map and all per-module state for a clean rebuild |
125+
| `test-impact:collect` | `process-test-classes` | Attaches the Java agent to Surefire's `argLine` |
126+
| `test-impact:select` | `process-test-classes` | Detects changed sources and sets Surefire's `test` filter |
127+
| `test-impact:report` | `verify` | Merges coverage dump into the shared map and generates a report |
128+
| `test-impact:invalidate` | *(manual)* | Clears the coverage map and all per-module state |
147129

148130
To reset the coverage map and force a full rebuild:
149131

@@ -153,9 +135,9 @@ mvn test-impact:invalidate
153135

154136
## Requirements
155137

156-
- **Java** 11+
157-
- **Maven** 3.9+
158-
- **Git** repository (for change detection)
138+
- Java 11+
139+
- Maven 3.9+
140+
- Git repository
159141

160142
## Building from source
161143

@@ -166,43 +148,34 @@ mvn clean verify
166148
```
167149

168150
This produces three artifacts:
169-
- `selective-test-runner-core-1.0.0-SNAPSHOT.jar`: build-tool-agnostic core (agent, change detection, impact resolution, coverage persistence)
170-
- `selective-test-runner-core-1.0.0-SNAPSHOT-agent.jar`: the shaded agent JAR (ASM relocated) used as `-javaagent` in the forked Surefire JVM
151+
- `selective-test-runner-core-1.0.0-SNAPSHOT.jar`: the core library (agent, change detection, impact resolution, coverage persistence)
152+
- `selective-test-runner-core-1.0.0-SNAPSHOT-agent.jar`: the shaded agent JAR (ASM relocated)
171153
- `test-impact-maven-plugin-1.0.0-SNAPSHOT.jar`: the Maven plugin
172154

173155
## Contributing
174156

175-
Contributions are welcome! Here's how to get started:
176-
177-
1. **Fork** the repository and create a feature branch from `main`
178-
2. **Build & test** locally with `mvn clean verify`
179-
3. **Keep changes focused**: one feature or fix per pull request
180-
4. **Add tests** for new functionality
181-
5. **Open a pull request** against `main` with a clear description of what and why
157+
1. Fork the repository and create a feature branch from `main`
158+
2. Build and test locally with `mvn clean verify`
159+
3. One feature or fix per pull request
160+
4. Add tests for new functionality
161+
5. Open a pull request against `main`
182162

183163
### Project structure
184164

185165
```
186-
selective-test-runner-core/ # Build-tool-agnostic core
187-
agent/ # Java agent: instrumentation, coverage recording
188-
change/ # Git change detection, source-to-class resolution
189-
store/ # Coverage map persistence (JSON)
190-
resolve/ # Impact analysis, test selection logic
191-
report/ # JSON + console report generation
192-
common/ # Shared utilities (paths, dump reader)
193-
194-
test-impact-maven-plugin/ # Maven plugin (thin wrapper over core)
195-
mojo/ # Maven plugin goals (collect, select, report, invalidate)
196-
common/ # Maven-specific utilities (reactor scope)
166+
selective-test-runner-core/
167+
agent/ Java agent, coverage recording
168+
change/ Git change detection, source-to-class resolution
169+
store/ Coverage map persistence (JSON)
170+
resolve/ Impact analysis, test selection
171+
report/ Report generation
172+
common/ Shared utilities
173+
174+
test-impact-maven-plugin/
175+
mojo/ Maven goals (collect, select, report, invalidate)
176+
common/ Maven-specific utilities
197177
```
198178

199-
### Areas where help is appreciated
200-
201-
- **Integration test selection**: Failsafe support with endpoint-flow modeling
202-
- **Gradle port**: adapt the agent and selection logic for Gradle builds
203-
- **Performance benchmarks**: real-world numbers on large open source projects
204-
- **Documentation**: usage guides, example projects
205-
206179
## License
207180

208-
This project is open source. See the [LICENSE](LICENSE) file for details.
181+
See the [LICENSE](LICENSE) file for details.

selective-test-runner-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<packaging>jar</packaging>
1616

1717
<name>Selective Test Runner :: Core</name>
18-
<description>Build-tool-agnostic core: bytecode agent, change detection, impact resolution, and coverage persistence.</description>
18+
<description>Core library for bytecode-level test impact analysis.</description>
1919

2020
<dependencies>
2121
<dependency>

selective-test-runner-core/src/main/java/io/github/mirkoalicastro/agent/CoverageAgent.java

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@
22

33
import java.lang.instrument.Instrumentation;
44

5-
/**
6-
* Java agent entry point. Installs a {@link CoverageTransformer} that: - injects {@link
7-
* CoverageRecorder#touch(String)} at the entry of every method in production classes; - wraps test
8-
* methods (JUnit 4/5, TestNG) with begin/end calls.
9-
*
10-
* <p>System properties: testimpact.dump = output file for collected entries (required for
11-
* collection) testimpact.includes = comma-separated package prefixes to include (default: all
12-
* non-system) testimpact.excludes = comma-separated package prefixes to exclude
13-
*/
5+
/** Java agent entry point. Installs the bytecode transformer for coverage collection. */
146
public final class CoverageAgent {
157

168
private CoverageAgent() {}
@@ -25,12 +17,8 @@ public static void agentmain(String args, Instrumentation inst) {
2517

2618
private static void install(Instrumentation inst) {
2719
CoverageRecorder.enable();
28-
// canRetransform=true is required for our injection to survive third-party agents
29-
// that call Instrumentation.retransformClasses(). In particular, Mockito 5's inline
30-
// mock maker retransforms classes used with @InjectMocks and @Mock — retransform
31-
// restarts the transformer chain from the original class bytes and only invokes
32-
// retransform-capable transformers, so a non-retransform-capable transformer's
33-
// contributions are silently dropped on every retransformation.
20+
// canRetransform=true so our instrumentation survives retransformClasses() calls
21+
// from other agents (e.g. Mockito 5's inline mock maker).
3422
inst.addTransformer(
3523
new CoverageTransformer(
3624
System.getProperty("testimpact.includes", ""),

selective-test-runner-core/src/main/java/io/github/mirkoalicastro/agent/CoverageRecorder.java

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,23 @@
1414
import java.util.concurrent.ConcurrentHashMap;
1515

1616
/**
17-
* Per-test class-touch recorder. Loadable from the system class loader, thread-safe.
17+
* Per-test class-touch recorder. Thread-safe, loaded from the system class loader.
1818
*
19-
* <p>Lifecycle: beginTest(id) — pushes a new test context onto the calling thread; finalises any
20-
* previous unfinished context for that thread (covers thrown tests). touch(cls) — records a
21-
* class-ref into the calling thread's current context. endTest() — finalises the calling thread's
22-
* current context.
23-
*
24-
* <p>Aggregated entries are written to the file at system property {@code testimpact.dump} during
25-
* JVM shutdown. Any contexts still live at shutdown are finalised first (this catches the last test
26-
* on a thread when it threw).
27-
*
28-
* <p>Dump format (DataOutput, simple binary): int numTests for each test: UTF testId int numClasses
29-
* for each class: UTF classRef
19+
* <p>Aggregated entries are written to {@code testimpact.dump} during JVM shutdown.
3020
*/
3121
public final class CoverageRecorder {
3222

3323
private CoverageRecorder() {}
3424

3525
private static final ThreadLocal<TestContext> CURRENT = new ThreadLocal<>();
3626

37-
/**
38-
* Per-thread buffer for touches that arrive before any test context is active (e.g. class loading
39-
* during field initialisation in the test constructor). Drained into the next {@link #beginTest}
40-
* on the same thread.
41-
*/
27+
/** Touches arriving before any test context; drained into the next beginTest. */
4228
private static final ThreadLocal<Set<String>> PENDING = ThreadLocal.withInitial(HashSet::new);
4329

44-
/** Live contexts indexed by thread, used for shutdown finalisation. */
30+
/** Live contexts, finalised at shutdown. */
4531
private static final Map<Thread, TestContext> LIVE = new ConcurrentHashMap<>();
4632

47-
/** Aggregated entries: testId -> set of touched class refs. */
33+
/** testId -> touched class refs. */
4834
private static final Map<String, Set<String>> ENTRIES = new HashMap<>();
4935

5036
private static volatile boolean enabled = false;
@@ -110,7 +96,7 @@ private static void shutdown() {
11096
flush();
11197
}
11298

113-
/** Flush accumulated data to the configured dump file. Safe to call repeatedly. */
99+
/** Writes accumulated data to the configured dump file. */
114100
public static synchronized void flush() {
115101
String dump = System.getProperty("testimpact.dump");
116102
if (dump == null || dump.isEmpty()) return;

0 commit comments

Comments
 (0)