Serialize Gradle and test runs
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
.intellijPlatform
|
.intellijPlatform
|
||||||
.kotlin
|
.kotlin
|
||||||
build
|
build
|
||||||
|
.gradle-test.lock
|
||||||
|
|||||||
15
AGENTS.md
15
AGENTS.md
@@ -277,12 +277,13 @@ The fast loop for grammar work:
|
|||||||
`ls -l build/classes/java/main/dev/hgh/jai/parser/JaiParser.class
|
`ls -l build/classes/java/main/dev/hgh/jai/parser/JaiParser.class
|
||||||
src/main/gen/dev/hgh/jai/parser/JaiParser.java`. The build wiring in
|
src/main/gen/dev/hgh/jai/parser/JaiParser.java`. The build wiring in
|
||||||
`build.gradle.kts` should prevent both, but `rm -rf build` settles it.
|
`build.gradle.kts` should prevent both, but `rm -rf build` settles it.
|
||||||
- **Concurrent Gradle runs corrupt the test results.** If something else (an
|
- **Concurrent Gradle runs corrupt the test results.** `./jaigradle` and
|
||||||
IDE, an agent's background checker) runs `test` at the same time, one of the
|
`./jaitest` serialize project Gradle work with the atomic `.gradle-test.lock`
|
||||||
two dies with `java.io.EOFException` or
|
lock, including test-result cleanup and retries. Never run `./gradlew`
|
||||||
`NoSuchFileException: .../in-progress-results-generic.bin`, and no XML is
|
directly: it bypasses both the JDK wrapper and this lock. If an interrupted
|
||||||
written. It is infrastructure, not a test failure — `./jaitest` retries once
|
process leaves a lock behind, confirm no Gradle run is active, then remove
|
||||||
automatically. Do not go debugging the test that "failed".
|
`.gradle-test.lock` and rerun. The JUnit wrapper still retries once for
|
||||||
|
unexpected no-XML failures; do not debug a test until the XML exists.
|
||||||
- **A poisoned build cache can make tests silently vanish.** Symptom:
|
- **A poisoned build cache can make tests silently vanish.** Symptom:
|
||||||
`BUILD SUCCESSFUL`, `:compileTestKotlin FROM-CACHE`, `:test NO-SOURCE`, and
|
`BUILD SUCCESSFUL`, `:compileTestKotlin FROM-CACHE`, `:test NO-SOURCE`, and
|
||||||
`build/classes/kotlin/test` is empty. The cache stored an empty output
|
`build/classes/kotlin/test` is empty. The cache stored an empty output
|
||||||
@@ -293,7 +294,7 @@ The fast loop for grammar work:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./jaigradle --stop
|
./jaigradle --stop
|
||||||
rm -rf build .gradle ~/.gradle/caches/build-cache-1
|
rm -rf .gradle-test.lock build .gradle ~/.gradle/caches/build-cache-1
|
||||||
./jaitest
|
./jaitest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
50
gradle-lock.sh
Normal file
50
gradle-lock.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared, process-safe lock for Gradle and IntelliJ platform test runs.
|
||||||
|
# This file is sourced by jaigradle and jaitest; it is not a standalone command.
|
||||||
|
|
||||||
|
JAI_GRADLE_LOCK_DIR="${JAI_GRADLE_LOCK_DIR:-.gradle-test.lock}"
|
||||||
|
JAI_GRADLE_LOCK_PID_FILE="$JAI_GRADLE_LOCK_DIR/pid"
|
||||||
|
|
||||||
|
jai_gradle_lock_acquire() {
|
||||||
|
if [[ "${JAI_GRADLE_LOCK_HELD:-0}" == "1" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local reported=0
|
||||||
|
while ! mkdir "$JAI_GRADLE_LOCK_DIR" 2>/dev/null; do
|
||||||
|
if [[ -f "$JAI_GRADLE_LOCK_PID_FILE" ]]; then
|
||||||
|
local owner
|
||||||
|
owner=$(<"$JAI_GRADLE_LOCK_PID_FILE")
|
||||||
|
if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then
|
||||||
|
rm -rf "$JAI_GRADLE_LOCK_DIR"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# A process can be killed between mkdir and writing its PID. Give that
|
||||||
|
# tiny window a chance to finish, then reclaim the incomplete lock.
|
||||||
|
sleep 1
|
||||||
|
if [[ ! -f "$JAI_GRADLE_LOCK_PID_FILE" ]]; then
|
||||||
|
rm -rf "$JAI_GRADLE_LOCK_DIR"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((reported == 0)); then
|
||||||
|
echo "Waiting for another Gradle/test run in this worktree..." >&2
|
||||||
|
reported=1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "$$" >"$JAI_GRADLE_LOCK_PID_FILE"
|
||||||
|
JAI_GRADLE_LOCK_HELD=1
|
||||||
|
JAI_GRADLE_LOCK_OWNER_PID=$$
|
||||||
|
export JAI_GRADLE_LOCK_HELD JAI_GRADLE_LOCK_OWNER_PID
|
||||||
|
}
|
||||||
|
|
||||||
|
jai_gradle_lock_release() {
|
||||||
|
if [[ "${JAI_GRADLE_LOCK_OWNER_PID:-}" == "$$" ]]; then
|
||||||
|
rm -rf "$JAI_GRADLE_LOCK_DIR"
|
||||||
|
unset JAI_GRADLE_LOCK_OWNER_PID JAI_GRADLE_LOCK_HELD
|
||||||
|
fi
|
||||||
|
}
|
||||||
22
jaigradle
22
jaigradle
@@ -13,11 +13,22 @@
|
|||||||
# and throw UnsupportedClassVersionError on 17.
|
# and throw UnsupportedClassVersionError on 17.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
source ./gradle-lock.sh
|
||||||
|
|
||||||
if command -v mise >/dev/null 2>&1; then
|
# `--stop` is an explicit recovery operation; it must be able to stop a
|
||||||
exec mise exec -- ./gradlew "$@"
|
# daemon even when a stale lock was left by an interrupted run.
|
||||||
|
if [[ "${1:-}" != "--stop" ]]; then
|
||||||
|
jai_gradle_lock_acquire
|
||||||
|
trap jai_gradle_lock_release EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
status=0
|
||||||
|
if command -v mise >/dev/null 2>&1; then
|
||||||
|
mise exec -- ./gradlew "$@" || status=$?
|
||||||
|
else
|
||||||
|
|
||||||
# Fallback: mise is not installed. Try an already-set JAVA_HOME, then any
|
# Fallback: mise is not installed. Try an already-set JAVA_HOME, then any
|
||||||
# JDK 21 that Gradle has auto-provisioned.
|
# JDK 21 that Gradle has auto-provisioned.
|
||||||
if [ ! -x "${JAVA_HOME:-}/bin/java" ]; then
|
if [ ! -x "${JAVA_HOME:-}/bin/java" ]; then
|
||||||
@@ -35,4 +46,9 @@ if [ ! -x "${JAVA_HOME:-}/bin/java" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec ./gradlew "$@"
|
./gradlew "$@" || status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
jai_gradle_lock_release
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
exit "$status"
|
||||||
|
|||||||
12
jaitest
12
jaitest
@@ -4,10 +4,9 @@
|
|||||||
# A green `test` task does not prove tests ran (see AGENTS.md), so this always
|
# A green `test` task does not prove tests ran (see AGENTS.md), so this always
|
||||||
# prints tests/failures/errors per suite and dumps failure messages.
|
# prints tests/failures/errors per suite and dumps failure messages.
|
||||||
#
|
#
|
||||||
# Two concurrent Gradle invocations (e.g. an editor/agent running tests in the
|
# A repository-wide lock serializes Gradle/test runs. Without it, an editor or
|
||||||
# background at the same time) fight over build/test-results and the loser dies
|
# another agent can delete build/test-results while this process is writing JUnit
|
||||||
# with `java.io.EOFException` or a missing in-progress-results bin. That is an
|
# XML, producing EOFException or a missing in-progress-results bin.
|
||||||
# infrastructure failure, not a test failure, so it is retried once.
|
|
||||||
#
|
#
|
||||||
# A `--tests` filter STICKS: Gradle reuses the configuration cache entry from the
|
# A `--tests` filter STICKS: Gradle reuses the configuration cache entry from the
|
||||||
# previous run, so a later unfiltered `./jaitest` silently re-runs just that one
|
# previous run, so a later unfiltered `./jaitest` silently re-runs just that one
|
||||||
@@ -18,6 +17,11 @@
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")" || exit 1
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
source ./gradle-lock.sh
|
||||||
|
jai_gradle_lock_acquire
|
||||||
|
trap jai_gradle_lock_release EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
extra=()
|
extra=()
|
||||||
if [[ " $* " != *" --tests "* ]]; then
|
if [[ " $* " != *" --tests "* ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user