Code coverage of tomato tests: the JVM (Kotlin, Java, Scala)
On the JVM nothing has to be rebuilt. The JaCoCo agent instruments classes as they load and writes its data from a shutdown hook. The JVM runs shutdown hooks on SIGTERM by itself, so this works even if the application does nothing special about the signal.
Run it
JACOCO=0.8.13
curl -fsSLO https://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/$JACOCO/org.jacoco.agent-$JACOCO-runtime.jar
curl -fsSLO https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/$JACOCO/org.jacoco.cli-$JACOCO-nodeps.jar
# Gradle's start script (from installDist) passes JAVA_OPTS to the JVM.
export JAVA_OPTS="-javaagent:$PWD/org.jacoco.agent-$JACOCO-runtime.jar=destfile=$PWD/coverage/blackbox.exec,includes=demo.*"
tomato run
java -jar org.jacoco.cli-$JACOCO-nodeps.jar report coverage/blackbox.exec \
--classfiles build/install/payments/lib/payments.jar \
--sourcefiles src/main/kotlin \
--xml coverage/jacoco.xml --html coverage/html
tomato.yml keeps its command: ./build/install/payments/bin/payments; the
start script picks up JAVA_OPTS from the environment tomato passes on.
Notes
includes=demo.*limits measurement to your packages. Without it, every library class that loads is measured too, and the run is slower.- A plain
java -jarcommand does not readJAVA_OPTS. UseJAVA_TOOL_OPTIONSinstead: every JVM reads it, whatever starts it. - Keep your own shutdown hooks short. They run alongside JaCoCo’s, and tomato sends SIGKILL five seconds after SIGTERM. Stop the consumer, join it for a few seconds at most, close connections.
- Point
--classfilesat what you shipped: the jar from the build, so the report matches the bytecode that ran. - Per-scenario dumps. The agent can serve its data over TCP
(
output=tcpserver), andjacococli dumpcollects it without stopping the service. That is more plumbing than most suites need. - Kotlin compiles to classes JaCoCo understands, including data classes and
when; JaCoCo filters most of the compiler-generated code from the counts.
In CI
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with: { distribution: temurin, java-version: 17 }
- run: ./gradlew test installDist
- uses: actions/upload-artifact@v7
with: { name: build, path: build/install/payments/ }
blackbox:
needs: build
runs-on: ubuntu-latest
env:
JACOCO: "0.8.13"
JAVA_OPTS: -javaagent:${{ github.workspace }}/jacoco/agent.jar=destfile=${{ github.workspace }}/coverage/blackbox.exec,includes=demo.*
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with: { name: build, path: build/install/payments/ }
- uses: actions/setup-java@v6
with: { distribution: temurin, java-version: 17 }
- run: |
chmod +x build/install/payments/bin/payments
mkdir -p jacoco
curl -fsSL -o jacoco/agent.jar "https://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/$JACOCO/org.jacoco.agent-$JACOCO-runtime.jar"
curl -fsSL -o jacoco/cli.jar "https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/$JACOCO/org.jacoco.cli-$JACOCO-nodeps.jar"
- run: tomato run
- if: always()
run: |
java -jar jacoco/cli.jar report coverage/blackbox.exec \
--classfiles build/install/payments/lib/payments.jar --sourcefiles src/main/kotlin \
--xml coverage/jacoco.xml