Race Condition Test Author
ASecurityBuild deterministic race-condition tests - identify shared mutable state, drive interleavings via barriers / latches / manual scheduling; use ThreadSanitizer (clang `-fsanitize=thread`) for C/C++ data race detection; run the Go race detector end-to-end (`go test -race`, GORACE tuning, `-count`/`-cpu` stress, goroutine-leak gating with go.uber.org/goleak - references/go.md); use jcstress (`@JCStressTest` + `@Actor` + `@Outcome`) for JVM stress; use Loom virtual-thread interleavings for paralle...
- 10 stars
- 0 votes
- 0 copies
- 1 view
- Added October 12, 2026
Security analysis
100/100Pro scans all 14 files and shows the line behind each finding
npx -y skills add testland/qa --skill race-condition-test-author --agent claude-codeAre you the author of Race Condition Test Author?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/testland-race-condition-test-author)---
name: race-condition-test-author
description: "Build deterministic race-condition tests - identify shared mutable state, drive interleavings via barriers / latches / manual scheduling; use ThreadSanitizer (clang `-fsanitize=thread`) for C/C++ data race detection; run the Go race detector end-to-end (`go test -race`, GORACE tuning, `-count`/`-cpu` stress, goroutine-leak gating with go.uber.org/goleak - references/go.md); use jcstress (`@JCStressTest` + `@Actor` + `@Outcome`) for JVM stress; use Loom virtual-thread interleavings for parallel testing. Use when a defect only reproduces under load on shared in-process state (cache, counter, connection pool, lazy-init singleton), when writing the regression test for a race-condition incident before the fix lands, or when adding `-race` to a Go CI matrix."
metadata:
keywords: "race-condition, data-race, thread-sanitizer, jcstress, concurrency-testing"
version: "1.2.14"
---
# race-condition-test-author
Race conditions are the canonical "works on my machine, breaks under
load" bug. Tests must drive shared-state access deterministically
(barriers, latches) AND non-deterministically (sanitizers, stress)
to expose them.
## When to use
- Service handles concurrent requests on shared in-process state
(caches, counters, connection pools).
- Race-condition incident retro: build a regression test before
fixing.
- Pre-release smoke: TSan-instrumented binary catches data races
that compile clean.
## Step 1 - Identify shared mutable state
Code-review checklist:
- Static / global / module-level mutable variables
- Singleton instances with mutable fields
- Cache structures with read-and-modify ("get-then-set" without
CAS)
- Per-request state stored on shared objects (mutable request
context)
- Connection pools / object pools without internal lock
- Lazy-init patterns ("if not initialized, initialize")
For each: ask "what if two threads / goroutines / async tasks hit
this concurrently?"
## Step 2 - Deterministic interleaving via barriers
```python
import threading
def test_lazy_init_thread_safe():
target = LazyService() # has private _instance + lazy_get()
barrier = threading.Barrier(parties=2)
results = [None, None]
def worker(idx):
barrier.wait() # both threads stop here, then race
results[idx] = target.lazy_get()
t1 = threading.Thread(target=worker, args=(0,))
t2 = threading.Thread(target=worker, args=(1,))
t1.start(); t2.start()
t1.join(); t2.join()
assert results[0] is results[1], "Lazy init created two instances under race"
```
Barrier ensures both threads start the contended section at the
same time - much higher probability of triggering the race than
naive `threading.Thread()`.
## Step 3 - ThreadSanitizer for C/C++/Go
Per the [ThreadSanitizer docs], TSan detects data races at runtime
with ~5-15× overhead. For C/C++:
```bash
clang -fsanitize=thread -g -O1 program.c -o program
./program
```
For Go, the native data race detector:
```bash
go test -race ./...
go run -race main.go
```
The full Go workflow - GORACE options (`log_path`, `halt_on_error`,
`history_size`), stress amplification via `-count`/`-cpu`, `go vet`
loop-capture checks, goroutine-leak gating with goleak, and the CI
matrix - is in [references/go.md](references/go.md).
Output for a detected race:
```
WARNING: DATA RACE
Read at 0x... by goroutine 7:
main.read+0x...
main.go:42
Previous write at 0x... by goroutine 6:
main.write+0x...
main.go:38
```
Per the [ThreadSanitizer docs], adaptive delay injection
(`TSAN_OPTIONS=enable_adaptive_delay=1`) helps surface races at
synchronization points.
## Step 4 - jcstress for JVM
Per the [jcstress docs], jcstress is "the experimental harness ... for
the correctness of concurrency support in the JVM."
```java
@JCStressTest
@Outcome(id = "0, 0", expect = ACCEPTABLE, desc = "Initial values")
@Outcome(id = "1, 1", expect = ACCEPTABLE, desc = "Both writes seen")
@Outcome(id = "0, 1", expect = ACCEPTABLE, desc = "Saw partial")
@Outcome(id = "1, 0", expect = FORBIDDEN, desc = "Reordered - bug")
@State
public class CounterTest {
int x, y;
@Actor
public void writer() { x = 1; y = 1; }
@Actor
public void reader(II_Result r) {
r.r1 = y;
r.r2 = x;
}
}
```
`@Actor` methods run concurrently on different threads; `@Outcome`
classifies observed `(r1, r2)` pairs. `FORBIDDEN` outcomes indicate
a memory-model violation (in this case, reordering allowed under JMM
unless `volatile` or `final`).
Run:
```bash
java -jar jcstress.jar -m quick CounterTest
```
Per the [jcstress docs]: tests are probabilistic; longer runs find
more reorderings.
## Step 5 - Loom virtual-thread interleavings (Java 21+)
Java 21+ Project Loom enables cheap virtual threads. Use to test
many-concurrent-task interleavings without OS thread cost:
```java
@Test
void test_handles_10000_concurrent_orders() throws Exception {
var orderService = new OrderService();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> orderService.place(i)))
.toList();
for (var f : futures) f.get();
}
assertEquals(10_000, orderService.totalProcessed());
}
```
Virtual threads run M:N on OS threads - the JVM scheduler interleaves
them aggressively, exposing schedule-dependent races.
## Step 6 - Property-based + concurrency
Combine Hypothesis (Python) / fast-check (JS) with concurrency:
```python
from hypothesis import given, strategies as st
import threading
@given(operations=st.lists(st.tuples(st.sampled_from(["read", "write"]), st.integers()), min_size=10, max_size=100))
def test_counter_property_under_concurrency(operations):
counter = ThreadSafeCounter()
threads = []
for op, val in operations:
if op == "write":
threads.append(threading.Thread(target=lambda v=val: counter.set(v)))
else:
threads.append(threading.Thread(target=counter.get))
for t in threads: t.start()
for t in threads: t.join()
# Property: final value is one of the written values
assert counter.get() in [v for op, v in operations if op == "write"]
```
Cross-ref `qa-property-based` plugin for property-based test
authoring patterns.
## Step 7 - CI integration
```yaml
# Go
- name: Run race detector
run: go test -race ./...
# C/C++
- name: TSan build + test
run: |
cmake -B build -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1"
cmake --build build
./build/test_runner
# Java
- name: jcstress quick run
run: java -jar jcstress.jar -m quick -r results/
```
Note the latency cost: `go test -race` ~3× slower; jcstress quick
mode ~minutes per test class.
## Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| `time.sleep(0.001)` to "force" interleaving | Non-deterministic; flake | Use barriers (Step 2) |
| Run race tests once and assume green | Probabilistic; some races take hours | Multiple runs OR longer runs OR sanitizers |
| Skip TSan in CI for "release" builds | Race in release; CI passed without `-race` | TSan in CI for at least one matrix dimension (Step 7) |
| Depend on assertion in worker thread | Thread death silent; main thread sees pass | Use futures + assert from main |
| Test only the bug-causing race, not similar | Other shared state has same pattern; bugs ship | Code-review checklist (Step 1) |
## Limitations
- TSan finds only data races (unsynchronized concurrent access),
not higher-level race conditions (correct-but-non-atomic
multi-step operations).
- jcstress is JVM-only; no equivalent for non-JVM stacks.
- Go's `-race` overhead is real (~5-10× memory); CI matrix
budget required.
- Loom virtual threads (Java 21+) require recent JDK; older
versions need OS threads.
## References
- [ThreadSanitizer docs] - clang `-fsanitize=thread`, runtime
options, adaptive delay
- [jcstress docs] - `@JCStressTest`, `@Actor`, `@Outcome` outcomes
- ThreadSanitizer C++ Manual - github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
- [references/go.md](references/go.md) - Go race detector + goleak
workflow (GORACE, stress, CI matrix)
- `deadlock-detection-harness` -
sister skill for deadlock-specific patterns
- `async-ordering-tests` -
sister skill for async/await ordering
[ThreadSanitizer docs]: https://clang.llvm.org/docs/ThreadSanitizer.html
[jcstress docs]: https://github.com/openjdk/jcstress
Files in this skill
- .tessl-plugin/plugin.json
- SKILL.md
- evals/config-singleton-closed-as-not-reproducible/criteria.json
- evals/config-singleton-closed-as-not-reproducible/task.md
- evals/double-charge-that-ci-was-green-through/criteria.json
- evals/double-charge-that-ci-was-green-through/task.md
- evals/ingest-pods-oomkilled-while-the-detector-is-clean/criteria.json
- evals/ingest-pods-oomkilled-while-the-detector-is-clean/task.md
- evals/nightly-ledger-total-comes-up-short/criteria.json
- evals/nightly-ledger-total-comes-up-short/task.md
- evals/three-findings-before-the-friday-release-cut/criteria.json
- evals/three-findings-before-the-friday-release-cut/task.md
- references/go.md
- references/goleak-filter-options.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.