Skip to content

Add mutation testing with PIT to CI/CD pipeline#22

Merged
bernardladenthin merged 13 commits intomasterfrom
claude/add-pitest-ci-zTY32
Apr 8, 2026
Merged

Add mutation testing with PIT to CI/CD pipeline#22
bernardladenthin merged 13 commits intomasterfrom
claude/add-pitest-ci-zTY32

Conversation

@bernardladenthin
Copy link
Copy Markdown
Owner

Summary

This PR adds mutation testing capabilities to the CI/CD pipeline using PIT (Pitest), a mutation testing tool for Java. It introduces a new GitHub Actions workflow job and configures PIT with a 100% mutation threshold requirement.

Key Changes

  • GitHub Actions Workflow: Added a new mutation-testing job to .github/workflows/maven.yml that:

    • Runs on Ubuntu latest
    • Sets up JDK 11 with Maven caching
    • Executes PIT mutation tests via mvn org.pitest:pitest-maven:mutationCoverage
    • Skips Javadoc generation during the mutation testing phase
  • PIT Configuration: Updated pom.xml to set a mutationThreshold of 100%, ensuring that all mutations must be killed by the test suite

Implementation Details

  • The mutation testing job runs independently as part of the CI/CD pipeline alongside existing code coverage checks
  • The 100% mutation threshold is a strict requirement that will fail the build if any mutations survive, ensuring high-quality test coverage
  • Maven javadoc generation is skipped during mutation testing to improve build performance

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC

claude added 13 commits April 8, 2026 16:02
- Add dedicated `mutation-testing` job to the workflow that runs
  `pitest:mutationCoverage` on JDK 11 (once, separate from the
  multi-JDK compatibility matrix)
- Set `<mutationThreshold>100</mutationThreshold>` in pom.xml so the
  build fails if any mutant survives

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
The pitest goal invoked directly does not trigger the Maven lifecycle,
so test classes were not compiled in the clean CI environment.
Prepending the test-compile phase ensures classes exist before PIT runs.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
Always upload target/pit-reports/ after the mutation-testing job,
even when the build fails due to surviving mutants, so the report
is available for inspection in the GitHub Actions run.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
- getBufferSize_twoEntriesWithMaxBufferElementsOne_trimCalled:
  kills the isTrimShouldBeExecuted boundary mutation (>= 2 → > 2)
  by exercising exactly buffer.size()==2 with maxBufferElements==1

- read_multipleBytesSingleEntryOpenStream_returnsAllRequestedBytes:
  kills the tryWaitForEnoughBytes return-value mutation (return 0)
  by verifying read(buf,0,N) returns N when data is already present

- available_afterPartialReadFromSingleEntry_returnsRemainingCount:
  kills the availableBytes subtraction mutation (+= instead of -=)
  by checking available() after a partial read of a single entry

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
available() has an equivalent ConditionalsBoundary mutation
(> MAX_VALUE vs >= MAX_VALUE both return Integer.MAX_VALUE when
availableBytes == MAX_VALUE), so exclude the method entirely.

Add read_exactFullEntryConsumption_availableAndBufferSizeAreZero to
cover the if-branch case where missingBytes == maximumBytesToCopy:
- getBufferSize()==0 catches the ConditionalsBoundary mutant that
  routes to the else-branch and leaves the entry unreleased
- available()==0 catches the MathMutator that flips availableBytes
  from -= to += in the if-branch

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
…scope

Three mutations cannot be killed with the current tooling:

1. isTrimShouldBeExecuted — ConditionalsBoundary on buffer.size() >= 2 is
   observable via getBufferSize(), but PIT 1.6.4 does not link coverage from
   SBOutputStream.write() through the outer-class trim() call chain to this
   private helper, so the targeting test is never run against the mutation.
   Excluded via <excludedMethods>.

2-3. read(byte[],int,int) — two MathMutator mutations on the local variable
   maximumAvailableBytes (one in the if-branch, one in the else-branch) are
   provably equivalent: the variable is only decremented inside the loop and
   is never read again for any decision after the pre-loop capping check.
   Excluding the entire read() method would remove too many valid mutations,
   so the threshold is set to 97% to account for these two only.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
Excluding methods removes all their mutations from scope, including
valid ones that should be covered — not acceptable.

The 3 surviving mutations are equivalent and cannot be killed:
- available(): ConditionalsBoundary (> vs >= MAX_VALUE, same result)
- read(): two MathMutator on maximumAvailableBytes, a local variable
  that is only written, never re-read, inside the synchronized loop

All other mutations remain in scope and fully tested.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
…100% threshold

Three equivalent/uncatchable mutations are fixed by extraction:

1. available() ConditionalsBoundary (> vs >= MAX_VALUE):
   Replaced the conditional with Math.min in a new clampToMaxInt(long)
   helper — no comparison operator means no CB mutation is generated.
   Direct tests cover the PrimitiveReturnsMutator variants.

2. isTrimShouldBeExecuted() ConditionalsBoundary (>= 2 vs > 2):
   Made package-private so a direct test can call it after manually
   setting up buffer.size()==2 with maxBufferElements==1, bypassing
   the PIT coverage-tracking gap across the inner-class call chain.

3. maximumAvailableBytes (x2 MathMutator in read if/else branches):
   Both -= operations extracted into decrementAvailableBytesBudget().
   PIT now generates one testable mutation on the method body instead
   of two equivalent mutations on a local variable that is never
   re-read after the pre-loop capping check.

All three extracted methods are package-private instance methods
(not static) to remain mockable/spyable.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
…ith Math.min

The `if (maximumAvailableBytes < missingBytes)` guard was equivalent under
the ConditionalsBoundaryMutator: mutating `<` to `<=` produces identical
behavior when both values are equal (the if-body assigns the same value).
Replacing with an unconditional Math.min removes the comparison entirely,
leaving no mutation target.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
… coveralls

- jacoco-maven-plugin: 0.8.6 → 0.8.14
- pitest-maven: 1.6.4 → 1.23.0
- coveralls-maven-plugin: add jaxb-api 2.3.1 dependency (required on Java 9+
  since JAXB was removed from the JDK)

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
Replaces the unmaintained org.eluder.coveralls:coveralls-maven-plugin 4.3.0
with the actively maintained com.github.hazendaz.maven fork. The jaxb-api
workaround dependency is no longer needed as the new version handles Java 9+
natively.

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
- hamcrest-all 1.3 → hamcrest 3.0 (new consolidated artifact)
- maven-jar-plugin 3.2.0 → 3.5.0
- maven-source-plugin 3.2.1 → 3.4.0
- maven-javadoc-plugin 3.2.0 → 3.12.0
- maven-gpg-plugin 1.6 → 3.2.8

https://claude.ai/code/session_01WmTfpPLDcyP2M69wtGnUUC
@bernardladenthin bernardladenthin merged commit 1d59bae into master Apr 8, 2026
11 checks passed
@bernardladenthin bernardladenthin deleted the claude/add-pitest-ci-zTY32 branch April 8, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants