diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index fabb9f0..afe18da 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -36,3 +36,27 @@ jobs:
with:
files: target/site/jacoco/jacoco.xml
continue-on-error: true
+
+ mutation-testing:
+ runs-on: ubuntu-latest
+ name: Mutation Testing with PIT
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Run mutation tests with PIT
+ run: mvn -B test-compile org.pitest:pitest-maven:mutationCoverage -Dmaven.javadoc.skip=true --file pom.xml
+
+ - name: Upload PIT report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: pit-reports
+ path: target/pit-reports/
diff --git a/pom.xml b/pom.xml
index 7e6a82b..bb865c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,8 +65,8 @@
org.hamcrest
- hamcrest-all
- 1.3
+ hamcrest
+ 3.0
test
@@ -87,7 +87,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.0.0-M5
+ 3.5.5
org.apache.maven.plugins
@@ -102,7 +102,7 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.2.0
+ 3.5.0
@@ -114,7 +114,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.2.1
+ 3.4.0
attach-sources
@@ -128,7 +128,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.2.0
+ 3.12.0
${java.version}
@@ -144,7 +144,7 @@
org.apache.maven.plugins
maven-gpg-plugin
- 1.6
+ 3.2.8
sign-artifacts
@@ -156,14 +156,14 @@
- org.eluder.coveralls
+ com.github.hazendaz.maven
coveralls-maven-plugin
- 4.3.0
+ 5.0.0
org.jacoco
jacoco-maven-plugin
- 0.8.6
+ 0.8.14
prepare-agent
@@ -183,7 +183,7 @@
org.pitest
pitest-maven
- 1.6.4
+ 1.23.0
net.ladenthin.streambuffer.*
@@ -191,6 +191,7 @@
net.ladenthin.streambuffer.*
+ 100
diff --git a/src/main/java/net/ladenthin/streambuffer/StreamBuffer.java b/src/main/java/net/ladenthin/streambuffer/StreamBuffer.java
index 2fb3aa6..e500f2c 100644
--- a/src/main/java/net/ladenthin/streambuffer/StreamBuffer.java
+++ b/src/main/java/net/ladenthin/streambuffer/StreamBuffer.java
@@ -306,7 +306,7 @@ private void trim() throws IOException {
* Checks if a trim should be performed.
* @return true if a trim should be performed, otherwise false.
*/
- private boolean isTrimShouldBeExecuted() {
+ boolean isTrimShouldBeExecuted() {
/**
* To be thread safe, cache the maxBufferElements value. May the method
* {@link #setMaxBufferElements(int)} was invoked from outside by another thread.
@@ -315,6 +315,28 @@ private boolean isTrimShouldBeExecuted() {
return (maxBufferElements > 0) && (buffer.size() >= 2) && (buffer.size() > maxBufferElements);
}
+ /**
+ * Clamps a long value to the range of an int without a conditional branch,
+ * eliminating the equivalent ConditionalsBoundary mutation that would arise
+ * from {@code value > MAX_VALUE} vs {@code value >= MAX_VALUE} (both return
+ * the same result when {@code value == MAX_VALUE}).
+ * Package-private for direct unit testing.
+ */
+ int clampToMaxInt(long value) {
+ return (int) Math.min(value, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Decrements a byte-budget counter by the given amount.
+ * Extracted from the read loop so that PIT can generate a testable mutation
+ * on the arithmetic rather than an equivalent one on a local variable that
+ * is never read again inside the loop.
+ * Package-private for direct unit testing.
+ */
+ long decrementAvailableBytesBudget(long current, long decrement) {
+ return current - decrement;
+ }
+
/**
* This method mustn't be called in a synchronized context, the variable is
* volatile.
@@ -360,10 +382,7 @@ private long tryWaitForEnoughBytes(final long bytes) throws InterruptedException
private class SBInputStream extends InputStream {
@Override
public int available() throws IOException {
- if (availableBytes > Integer.MAX_VALUE) {
- return Integer.MAX_VALUE;
- }
- return (int) availableBytes;
+ return clampToMaxInt(availableBytes);
}
@Override
@@ -446,9 +465,7 @@ public int read(final byte b[], final int off, final int len) throws IOException
}
// cap missingBytes to the actually available bytes
- if (maximumAvailableBytes < missingBytes) {
- missingBytes = (int) Math.min(maximumAvailableBytes, Integer.MAX_VALUE);
- }
+ missingBytes = (int) Math.min(maximumAvailableBytes, (long) missingBytes);
// some or enough bytes are available, lock and modify the FIFO
synchronized (bufferLock) {
@@ -471,7 +488,7 @@ public int read(final byte b[], final int off, final int len) throws IOException
System.arraycopy(first, positionAtCurrentBufferEntry, b,
copiedBytes + off, maximumBytesToCopy);
copiedBytes += maximumBytesToCopy;
- maximumAvailableBytes -= maximumBytesToCopy;
+ maximumAvailableBytes = decrementAvailableBytesBudget(maximumAvailableBytes, maximumBytesToCopy);
availableBytes -= maximumBytesToCopy;
missingBytes -= maximumBytesToCopy;
// remove the first element from the buffer
@@ -485,7 +502,7 @@ public int read(final byte b[], final int off, final int len) throws IOException
// add the offset
positionAtCurrentBufferEntry += missingBytes;
copiedBytes += missingBytes;
- maximumAvailableBytes -= missingBytes;
+ maximumAvailableBytes = decrementAvailableBytesBudget(maximumAvailableBytes, missingBytes);
availableBytes -= missingBytes;
// set missing bytes to zero
// we reach the end of the current buffer (b)
diff --git a/src/test/java/net/ladenthin/streambuffer/StreamBufferTest.java b/src/test/java/net/ladenthin/streambuffer/StreamBufferTest.java
index 8e972d8..48e58b3 100644
--- a/src/test/java/net/ladenthin/streambuffer/StreamBufferTest.java
+++ b/src/test/java/net/ladenthin/streambuffer/StreamBufferTest.java
@@ -2065,4 +2065,127 @@ public void read_closedStreamWithTwoBytes_readArrayReturnsBothBytes() throws IOE
assertThat(dest[1], is((byte) 20));
}
//
+
+ //
+ @Test
+ public void read_exactFullEntryConsumption_availableAndBufferSizeAreZero() throws IOException {
+ // arrange — write 3 bytes as a single entry; after the internal read() call consumes byte 0,
+ // positionAtCurrentBufferEntry=1, missingBytes=2, maximumBytesToCopy=3-1=2 → exactly equal.
+ // This hits the if-branch boundary: missingBytes >= maximumBytesToCopy (both == 2).
+ StreamBuffer sb = new StreamBuffer();
+ sb.getOutputStream().write(new byte[]{1, 2, 3});
+
+ // act
+ byte[] dest = new byte[3];
+ int bytesRead = sb.getInputStream().read(dest, 0, 3);
+
+ // assert
+ assertThat(bytesRead, is(3));
+ assertThat(dest, is(new byte[]{1, 2, 3}));
+ // ConditionalsBoundary mutant (>= → >): routes to else-branch → entry NOT removed → bufferSize = 1
+ assertThat(sb.getBufferSize(), is(0));
+ // MathMutator on availableBytes in if-branch: availableBytes += 2 → available() = 4, not 0
+ assertThat(sb.getInputStream().available(), is(0));
+ }
+ //
+
+ //
+ @Test
+ public void isTrimShouldBeExecuted_bufferSizeTwoMaxElementsOne_returnsTrue() throws IOException {
+ // arrange — disable trim while writing so we can control buffer.size() independently
+ StreamBuffer sb = new StreamBuffer();
+ sb.setMaxBufferElements(0);
+ sb.getOutputStream().write(new byte[]{1});
+ sb.getOutputStream().write(new byte[]{2});
+ // buffer.size() == 2; now enable trim condition
+ sb.setMaxBufferElements(1);
+
+ // act + assert — original: (2 >= 2) && (2 > 1) = true
+ // mutant: (2 > 2) && (2 > 1) = false → mutation killed
+ assertThat(sb.isTrimShouldBeExecuted(), is(true));
+ }
+ //
+
+ //
+ @Test
+ public void clampToMaxInt_valueAboveMaxInt_returnsMaxInt() {
+ StreamBuffer sb = new StreamBuffer();
+ assertThat(sb.clampToMaxInt((long) Integer.MAX_VALUE + 1), is(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void clampToMaxInt_valueEqualToMaxInt_returnsMaxInt() {
+ StreamBuffer sb = new StreamBuffer();
+ assertThat(sb.clampToMaxInt((long) Integer.MAX_VALUE), is(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void clampToMaxInt_smallValue_returnsValue() {
+ StreamBuffer sb = new StreamBuffer();
+ assertThat(sb.clampToMaxInt(42L), is(42));
+ }
+ //
+
+ //
+ @Test
+ public void decrementAvailableBytesBudget_subtractsDecrement() {
+ // original: current - decrement = 9 - 4 = 5
+ // mutant: current + decrement = 9 + 4 = 13 → mutation killed
+ StreamBuffer sb = new StreamBuffer();
+ assertThat(sb.decrementAvailableBytesBudget(9L, 4L), is(5L));
+ }
+ //
+
+ //
+ @Test
+ public void getBufferSize_twoEntriesWithMaxBufferElementsOne_trimCalled() throws IOException {
+ // arrange
+ StreamBuffer sb = new StreamBuffer();
+ sb.setMaxBufferElements(1);
+
+ // act — exactly two separate entries: buffer.size() == 2 > maxBufferElements == 1
+ // original: buffer.size() >= 2 → true → trim fires
+ // mutant: buffer.size() > 2 → false → trim skipped → getBufferSize() stays 2
+ sb.getOutputStream().write(new byte[]{1});
+ sb.getOutputStream().write(new byte[]{2});
+
+ // assert
+ assertThat(sb.getBufferSize(), is(1));
+ }
+ //
+
+ //
+ @Test
+ public void read_multipleBytesSingleEntryOpenStream_returnsAllRequestedBytes() throws IOException {
+ // arrange — write 5 bytes as one entry; stream left open
+ StreamBuffer sb = new StreamBuffer();
+ sb.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
+
+ // act — tryWaitForEnoughBytes(4) takes the "already enough" path and must return availableBytes (4)
+ // mutant returns 0 → read(b, 0, 5) short-circuits and returns only 1 (the first byte)
+ byte[] dest = new byte[5];
+ int bytesRead = sb.getInputStream().read(dest, 0, 5);
+
+ // assert
+ assertThat(bytesRead, is(5));
+ assertThat(dest, is(new byte[]{1, 2, 3, 4, 5}));
+ }
+ //
+
+ //
+ @Test
+ public void available_afterPartialReadFromSingleEntry_returnsRemainingCount() throws IOException {
+ // arrange — 10 bytes as a single deque entry
+ StreamBuffer sb = new StreamBuffer();
+ sb.getOutputStream().write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+ // act — read first 5 bytes (1 via read() then 4 via the partial-copy else branch)
+ byte[] dest = new byte[5];
+ int bytesRead = sb.getInputStream().read(dest, 0, 5);
+
+ // assert — 5 bytes must remain; mutant does availableBytes += 4 instead of -= 4 → reports 13
+ assertThat(bytesRead, is(5));
+ assertThat(sb.getInputStream().available(), is(5));
+ }
+ //
}