Repository navigation
ci: gate pull requests on coverage and mutation score of the lines they change - #38
Merged
Merged
Conversation
added 4 commits
October 2, 2026 10:35
…ey change Two new merge-gating jobs in build.yml, both scoped to the main-source lines a pull request adds or modifies (scripts/diff-quality.py): - coverage: runs the suite under Kover and fails when fewer than 90% of the changed executable lines are covered. koverVerify also holds the merged total to a floor so it cannot slide. The merged total is around 80% today, so a 90% total gate would fail every pull request. - mutation: runs PIT on the changed classes and fails when the tests detect fewer than 80% of the mutants on covered changed lines. Survivors are listed in the job summary. tools/pitest-filters is a small PIT plugin (not published): CHANGED_LINES drops mutants off the changed lines before they run, and KOTLIN_NULL_CHECKS drops mutants that only remove a compiler-inserted Intrinsics null check. Without the line filter, PR #7's classes took over 35 minutes locally and never finished; with it they take under 6.
…caught -x spotlessCheck resolves against the -p project, which has no such task; pitest never runs spotless, so the flag goes. The job's own first run then found a survivor in this change: the Kotlin null-check filter test compared mutants that share an id, which is all MutationDetails equality looks at.
…loor to 83% Kover skips a project without a Kotlin plugin, so tools/pitest-filters (plain Java) dropped out of the merged report and the changed-lines gate saw 0/0 lines on this very pull request. The floor is CI's measured total, 83.2%, rounded down.
…for mutations Replaces scripts/diff-quality.py with off-the-shelf pieces: - coverage: diff-cover reads Kover's JaCoCo-format report against the merge base and fails under 90%; the source roots are discovered from the tree. - mutation: the CHANGED_LINES filter now reads git diff --unified=0 directly, so one ./gradlew pitest covers every project (untouched ones skip in a second), and PIT's testStrengthThreshold enforces 80%. A short inline step lists the survivors in the job summary, which PIT's console output does not. The only custom code left is tools/pitest-filters (CHANGED_LINES, KOTLIN_NULL_CHECKS).
rgamba
marked this pull request as ready for review
October 2, 2026 17:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two new merge-gating jobs in
build.yml, both behind the existingjvm-buildfan-in, so no ruleset change is needed:coverageruns the suite under Kover. diff-cover then fails the PR when fewer than 90% of the executable main-source lines it adds or modifies are covered, and lists the uncovered lines in the job summary.koverVerifyalso holds the merged total to a floor of 83% (CI measured 83.2%), so it can't slide.mutationanswers whether the tests actually check the code, not only run it. PIT makes small breaking edits to the lines the PR changes (flips a condition, deletes a call, returns a constant) and reruns the covering tests against each one. PIT's owntestStrengthThresholdfails the job when the tests detect fewer than 80% of the mutants on covered lines. Surviving mutants are listed in the job summary: each one is a change to the code that no test noticed.Key decisions and trade-offs
pipx) handles changed-line coverage, and PIT's built-in threshold handles the mutation score. The source roots diff-cover needs are discovered from the tree, so a new project is picked up without editing the workflow.tools/pitest-filters, a small PIT plugin (not published):CHANGED_LINESreadsgit diff --unified=0and drops every mutant off the added or modified lines before PIT plans any. Open-source PIT can only narrow to whole classes. The engine's tests are end-to-end and poll for workflow state, so a broken engine makes them wait until they time out: mutating the full classes PR feat: cooperative in-flight cancellation checkpoint (opt-in) #7 touched ran for over 35 minutes locally without finishing. With the filter it takes 5 min 36 s. A project the diff doesn't touch has nothing left to mutate, and PIT skips it in about a second, so one./gradlew pitestcovers every project.KOTLIN_NULL_CHECKSdrops mutants that only remove a null check the Kotlin compiler inserted (Intrinsics::checkNotNull…). No test can catch those, and open-source PIT doesn't filter them.jvm-build-jackson. Each failure is its own signal, they start immediately instead of waiting on the matrix, and coverage instrumentation stays out of the builds the release relies on.testutils/traceis excluded from coverage. Only the formal workflow's trace-validation job runs it (under-PskipperTraceDir), so here it would always read as untested.testutilschanges are mutation-tested only by testutils' own tests, while most of its coverage comes from the engine's tests. Those mutants show up as uncovered, and PIT leaves uncovered mutants out of the score.CHANGED_LINESmatch files by package and file name, so they rely on every source file sitting in the directory its package names. All 264 main sources do today. A file that didn't would be skipped, not wrongly scored.Versions: Kover 0.9.1, diff-cover 10.6.0, gradle-pitest-plugin 1.19.0, PIT 1.30.0, pitest-junit5-plugin 1.2.2.
Testing
Calibrated against PR #7 (cooperative cancellation, ~150 changed lines in the engine core, unchanged since it merged):
WorkflowExecutionTaskHandler.kt(341–342, 424–425).ActionExecutor.kt:125andWorkflowExecutor.kt:420: removing the cancellationSkipperCounter::incbreaks no test.FeatureGate.kt:54:Keys.enabledByDefaultcan always returnfalsewithout any test noticing.pitestfails withTest strength score of 75 is below threshold of 80.MutationDetails.equalslooks at, so it couldn't tell them apart. Fixed.FiltersTest).spotlessCheckandactionlintpass.