Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
23 changes: 12 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<artifactId>hamcrest</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -87,7 +87,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<version>3.5.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand All @@ -102,7 +102,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
Expand All @@ -114,7 +114,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
Expand All @@ -128,7 +128,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<version>3.12.0</version>
<configuration>
<source>${java.version}</source>
</configuration>
Expand All @@ -144,7 +144,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-artifacts</id>
Expand All @@ -156,14 +156,14 @@
</executions>
</plugin>
<plugin>
<groupId>org.eluder.coveralls</groupId>
<groupId>com.github.hazendaz.maven</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<version>4.3.0</version>
<version>5.0.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<version>0.8.14</version>
<executions>
<execution>
<id>prepare-agent</id>
Expand All @@ -183,14 +183,15 @@
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.6.4</version>
<version>1.23.0</version>
<configuration>
<targetClasses>
<param>net.ladenthin.streambuffer.*</param>
</targetClasses>
<targetTests>
<param>net.ladenthin.streambuffer.*</param>
</targetTests>
<mutationThreshold>100</mutationThreshold>
</configuration>
</plugin>
</plugins>
Expand Down
37 changes: 27 additions & 10 deletions src/main/java/net/ladenthin/streambuffer/StreamBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ private void trim() throws IOException {
* Checks if a trim should be performed.
* @return <code>true</code> if a trim should be performed, otherwise <code>false</code>.
*/
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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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)
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/net/ladenthin/streambuffer/StreamBufferTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2065,4 +2065,127 @@ public void read_closedStreamWithTwoBytes_readArrayReturnsBothBytes() throws IOE
assertThat(dest[1], is((byte) 20));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="read if-branch: exact full-entry consumption">
@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));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="isTrimShouldBeExecuted direct">
@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));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="clampToMaxInt direct">
@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));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="decrementAvailableBytesBudget direct">
@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));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="isTrimShouldBeExecuted size-two boundary">
@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));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="tryWaitForEnoughBytes open-stream return">
@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}));
}
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="available after partial read from single entry">
@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));
}
// </editor-fold>
}
Loading