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
961 changes: 0 additions & 961 deletions .claude/skills/java-tdd-guide/SKILL.md

This file was deleted.

20 changes: 20 additions & 0 deletions .claude/skills/java-tdd-guide/SKILL.pointer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: java-tdd-guide-pointer
description: Marker file — the canonical Java TDD skill lives in the sibling workspace repo at .claude/skills/java-tdd-guide/SKILL.md and must be in the Claude session scope for the harness to load it.
---

# Pointer — Java TDD Guide

The canonical skill content lives in the `workspace` sibling repo:

`../../../../workspace/.claude/skills/java-tdd-guide/SKILL.md`

For Claude sessions opened against this repo to find the skill, the
`workspace` repo must also be in the session's repository scope. The
standard remote-execution setup adds it automatically.

If a session reports the skill as missing, verify the session scope
includes `bernardladenthin/workspace` and retry.

This file exists so human readers and any future drift-detection tooling
can see the dependency from this repo to the canonical skill.
87 changes: 6 additions & 81 deletions CLAUDE.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TODO — streambuffer

Open work items for this repo. Cross-cutting tracking lives in
[`../workspace/crossrepostatus.md`](../workspace/crossrepostatus.md);
items here are streambuffer-specific or are this repo's slice of a
cross-cutting initiative.

## Open

- **jqwik pin policy** — see [`../workspace/policies/jqwik-prompt-injection.md`](../workspace/policies/jqwik-prompt-injection.md). `jqwik.version ≤ 1.9.3` is mandatory.

- **`@VisibleForTesting` audit.** No usages currently. Walk the production tree for package-private/protected methods or fields that exist purely so tests can reach them, and either annotate (`com.google.common.annotations.VisibleForTesting`) or move into the test source tree.

- **Null-safety refinement.** JSpecify + NullAway are enforced at compile time in **strict JSpecify mode** with the following extra options: `CheckOptionalEmptiness`, `AcknowledgeRestrictiveAnnotations`, `AcknowledgeAndroidRecent`, `AssertsEnabled` (see `pom.xml`). The package carries an explicit `@NullMarked` via `package-info.java`. The production code currently has no `@Nullable` markers because every value is non-null by construction (constructors reject `null`, no `return null` sites). Open follow-up: as new public API surfaces are added, evaluate whether `@Nullable` or `Optional<T>` would be more precise than the implicit non-null default.

- **SpotBugs `effort=Max` + `threshold=Low`** — ✅ **enforced at the gate** (`4374dea` + `e7e254a`). `pom.xml` `<effort>Max</effort>` + `<threshold>Low</threshold>`; `spotbugs:check` is part of `mvn verify` and fails on any unsuppressed finding. All findings were fixed at source (added `toString()`, contextful exception messages) — no project-wide suppressions. sb was the first sibling repo to reach the Max+Low gate.

- **No LogCaptor smoke test needed** — this module has no logging code (`org.slf4j.*` not used in `src/main/java/`). If logging is ever introduced, add a LogCaptor smoke test at the same time so the binding/configuration is exercised in tests.

- **Cross-repo code-quality TODOs** — see [`../workspace/policies/code-quality-todos.md`](../workspace/policies/code-quality-todos.md) for the canonical `@VisibleForTesting` design-fit review, package hierarchy review, and class/method naming review. This module is single-package and has no `@VisibleForTesting` usages; the package and naming reviews remain open.

## Done (kept for history)

- **Error Prone bug-pattern promotions to `ERROR`** — `ad95d66` (12 patterns promoted).
- **`javac -Werror` + `-Xlint:all,-serial,-options,-classfile,-processing`** — `7a4fbf0`. ElementType.MODULE blocker resolved by the module-level `@NullMarked` move.
- **`-parameters` javac arg** — `912f14b`.
- **`--release N`** instead of `-source N -target N` — `912f14b`.
- **Mutation-testing threshold enforcement (PIT)** — 100 % over the whole package.
- **Checker Framework as a second static-nullness pass** — `5a9be1b`.
- **JPMS `module-info.java`** — exports `net.ladenthin.streambuffer`; two-execution `maven-compiler-plugin` pattern (release 8 sources, release 9 module-info); the resulting jar carries `module-info.class` at its root and is backward-compatible with Java 8 classpath consumers. Module-level `@NullMarked` was intentionally NOT added — the per-package `package-info.java` annotation already covers the same nullness scope.
- **Banned-API enforcement** — Maven Enforcer (`c0148c8`); ArchUnit `Thread.sleep` / `System.exit` / `new Random` bans (`eaf4337`).
- **ArchUnit additions** — public-fields-final (`5dd816d`), internal-JDK banned-imports (`de29bd4`), `noTestFrameworksInProduction` + `noPackageCycles` (`bbdb505`). Full `layeredArchitecture` is N/A: this module is a single-package library.
- **Abstract the Java and test writing guidelines to a workspace-level shared layer.** Workspace version chain at [`../workspace/guides/src/CODE_WRITING_GUIDE-8.md`](../workspace/guides/src/CODE_WRITING_GUIDE-8.md) and [`../workspace/guides/test/TEST_WRITING_GUIDE-8.md`](../workspace/guides/test/TEST_WRITING_GUIDE-8.md). Canonical TDD skill at [`../workspace/.claude/skills/java-tdd-guide/SKILL.md`](../workspace/.claude/skills/java-tdd-guide/SKILL.md). This repo has no project-specific writing-guide supplements (production code is a single class).
- **Standardised CLAUDE.md template** — [`../workspace/templates/CLAUDE.md.template`](../workspace/templates/CLAUDE.md.template).
16 changes: 8 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ SPDX-License-Identifier: Apache-2.0
<errorprone.version>2.49.0</errorprone.version>
<nullaway.version>0.13.4</nullaway.version>
<jspecify.version>1.0.0</jspecify.version>
<checker.version>4.1.0</checker.version>
<spotless.version>3.5.1</spotless.version>
<palantir-java-format.version>2.66.0</palantir-java-format.version>
<checker.version>4.2.0</checker.version>
<spotless.version>3.6.0</spotless.version>
<palantir-java-format.version>2.91.0</palantir-java-format.version>
<spotbugs.version>4.9.8.3</spotbugs.version>
<fb-contrib.version>7.6.4</fb-contrib.version>
<fb-contrib.version>7.7.4</fb-contrib.version>
<findsecbugs.version>1.14.0</findsecbugs.version>
<archunit.version>1.4.2</archunit.version>
<awaitility.version>4.3.0</awaitility.version>
Expand Down Expand Up @@ -230,7 +230,7 @@ SPDX-License-Identifier: Apache-2.0
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
<version>3.5.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand All @@ -250,7 +250,7 @@ SPDX-License-Identifier: Apache-2.0
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.25.1</version>
<version>1.25.3</version>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
Expand Down Expand Up @@ -538,8 +538,8 @@ SPDX-License-Identifier: Apache-2.0
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
<effort>Default</effort>
<threshold>Default</threshold>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<includeTests>false</includeTests>
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
Expand Down
70 changes: 54 additions & 16 deletions src/main/java/net/ladenthin/streambuffer/StreamBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -265,7 +266,7 @@ public long getMaxAllocationSize() {
*/
public void setMaxAllocationSize(final long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxAllocationSize must be positive");
throw new IllegalArgumentException("maxAllocationSize must be positive but was " + maxSize);
}
this.maxAllocationSize = maxSize;
}
Expand Down Expand Up @@ -309,7 +310,8 @@ public int getBufferElementCount() {
*/
public void addSignal(Semaphore semaphore) {
if (semaphore == null) {
throw new NullPointerException("Semaphore cannot be null");
throw new NullPointerException(
"Semaphore cannot be null (addSignal; " + signals.size() + " signal(s) already registered)");
}
signals.add(semaphore);
}
Expand All @@ -334,7 +336,8 @@ public boolean removeSignal(Semaphore semaphore) {
*/
public void addTrimStartSignal(Semaphore semaphore) {
if (semaphore == null) {
throw new NullPointerException("Semaphore cannot be null");
throw new NullPointerException("Semaphore cannot be null (addTrimStartSignal; " + trimStartSignals.size()
+ " trim-start signal(s) already registered)");
}
trimStartSignals.add(semaphore);
}
Expand All @@ -358,7 +361,8 @@ public boolean removeTrimStartSignal(Semaphore semaphore) {
*/
public void addTrimEndSignal(Semaphore semaphore) {
if (semaphore == null) {
throw new NullPointerException("Semaphore cannot be null");
throw new NullPointerException("Semaphore cannot be null (addTrimEndSignal; " + trimEndSignals.size()
+ " trim-end signal(s) already registered)");
}
trimEndSignals.add(semaphore);
}
Expand Down Expand Up @@ -405,11 +409,11 @@ public static boolean validateOffsetAndLengthToRead(byte[] b, int off, int len)
* @throws IndexOutOfBoundsException if the offset or length is not invalid
*/
public static boolean validateOffsetAndLengthToWrite(byte[] b, int off, int len) {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
Objects.requireNonNull(b, "validateOffsetAndLengthToWrite: byte array b must not be null");
if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException(
EXCEPTION_MESSAGE_VALIDATE_OFFSET_AND_LENGTH_TO_WRITE_INDEX_OUT_OF_BOUNDS_EXCEPTION);
EXCEPTION_MESSAGE_VALIDATE_OFFSET_AND_LENGTH_TO_WRITE_INDEX_OUT_OF_BOUNDS_EXCEPTION + " (b.length="
+ b.length + ", off=" + off + ", len=" + len + ")");
} else if (len == 0) {
return false;
}
Expand Down Expand Up @@ -480,15 +484,17 @@ private void trim() throws IOException {
* every byte that trim already drained.
*/
while (!tmpBuffer.isEmpty()) {
// Deque.pollFirst is declared @Nullable; the !isEmpty guard above
// makes it non-null here in practice, but neither NullAway's flow
// analyzer nor the Checker Framework Nullness Checker bridge that
// loop-guard reasoning, so we make the non-null contract explicit.
// Deque.pollFirst() may return null per the JDK contract; the
// !isEmpty guard above makes it non-null here in practice, but
// neither NullAway's flow analyzer nor the Checker Framework
// Nullness Checker bridge that loop-guard reasoning, so we make
// the non-null contract explicit.
final byte[] chunk = tmpBuffer.pollFirst();
if (chunk == null) {
throw new java.util.NoSuchElementException(
"tmpBuffer.pollFirst() returned null despite a successful isEmpty() check; "
+ "indicates a concurrent modification of tmpBuffer.");
"tmpBuffer.pollFirst() returned null despite a successful isEmpty() check"
+ " (remaining=" + tmpBuffer.size()
+ ", indicates a concurrent modification of tmpBuffer).");
}
buffer.add(chunk);
availableBytes += chunk.length;
Expand Down Expand Up @@ -763,7 +769,7 @@ void updateMaxObservedBytesIfNeeded(long availableBytes) {
*/
private void requireNonClosed() throws IOException {
if (streamClosed) {
throw new IOException("Stream closed.");
throw new IOException("Stream closed (availableBytes=" + availableBytes + ").");
}
}

Expand All @@ -781,7 +787,9 @@ private void requireNonClosed() throws IOException {
*/
public long waitForAtLeast(final long bytes) throws InterruptedException {
// we can only wait for a positive number of bytes
assert bytes > 0 : "Number of bytes are negative or zero : " + bytes;
if (bytes <= 0) {
throw new IllegalArgumentException("Number of bytes are negative or zero : " + bytes);
}

// if we haven't enough bytes, the loop starts and wait for enough bytes
while (bytes > availableBytes) {
Expand Down Expand Up @@ -1074,4 +1082,34 @@ public InputStream getInputStream() {
public OutputStream getOutputStream() {
return os;
}

/**
* Identity-shaped snapshot of the buffer's externally observable state.
*
* <p>Reports the bytes available to read, the current Deque element count,
* the closed flag, and the {@code safeWrite} setting. Acquires
* {@link #bufferLock} so the four numbers reflect the same instant; never
* holds the lock for more than a constant-time read.
*
* <p>Intended for log lines and debugger displays; do not parse it.
*
* @return a human-readable single-line snapshot
*/
@Override
public String toString() {
final long availableBytesSnapshot;
final int bufferSizeSnapshot;
final boolean closedSnapshot;
final boolean safeWriteSnapshot;
synchronized (bufferLock) {
availableBytesSnapshot = availableBytes;
bufferSizeSnapshot = buffer.size();
closedSnapshot = streamClosed;
safeWriteSnapshot = safeWrite;
}
return "StreamBuffer[available=" + availableBytesSnapshot
+ ", elements=" + bufferSizeSnapshot
+ ", closed=" + closedSnapshot
+ ", safeWrite=" + safeWriteSnapshot + "]";
}
}
21 changes: 12 additions & 9 deletions src/main/java/net/ladenthin/streambuffer/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
*
* <p>JSpecify {@code @NullMarked} is declared at module level in
* {@code module-info.java} and applies transitively to every package:
* every parameter, return value, and field is non-null unless explicitly
* annotated {@code @Nullable}. NullAway and the Checker Framework Nullness
* Checker both enforce this at compile time via the configured Error Prone
* compiler plugin (see {@code pom.xml}). The annotation lives only in
* {@code module-info.java} so that {@code @NullMarked} is not referenced
* from any source compiled at {@code --release 8}, which avoids the
* unsuppressible {@code unknown enum constant ElementType.MODULE}
* classfile-read warning that javac emits for any source at release 8
* that resolves the JSpecify {@code @NullMarked} annotation type.
* every parameter, return value, and field is non-null unless it carries
* an explicit JSpecify nullable-marker annotation. NullAway and the
* Checker Framework Nullness Checker both enforce this at compile time
* via the configured Error Prone compiler plugin (see {@code pom.xml}).
* The annotation lives only in {@code module-info.java} so that
* {@code @NullMarked} is not referenced from any source compiled at
* {@code --release 8}, which avoids the unsuppressible {@code unknown
* enum constant ElementType.MODULE} classfile-read warning that javac
* emits for any source at release 8 that resolves the JSpecify
* {@code @NullMarked} annotation type. Production code currently
* declares no nullable members of its own — every annotation appears in
* the test sources only.
*/
package net.ladenthin.streambuffer;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
Expand Down Expand Up @@ -58,6 +59,33 @@ public class StreamBufferArchitectureTest {
.dependOnClassesThat()
.resideInAPackage("java.util.logging..");

/**
* Test-framework classes must not appear in production code. Currently
* vacuous because {@link #mainCodeStaysLeaf} already constrains the
* dependency set to JDK + a handful of annotation packages, but kept as
* an explicit regression guard so a future refactor cannot accidentally
* carry over a JUnit / jqwik / ArchUnit type into {@code src/main}.
*/
@ArchTest
static final ArchRule noTestFrameworksInProduction = noClasses()
.that()
.resideInAPackage("net.ladenthin.streambuffer..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("org.junit..", "net.jqwik..", "com.tngtech.archunit..");

/**
* No package cycles between sub-packages. Vacuous today on this
* single-package module; acts as a forward-looking guard so a future
* sub-package extraction cannot introduce a circular dependency without
* breaking the build.
*/
@ArchTest
static final ArchRule noPackageCycles = slices().matching("net.ladenthin.streambuffer.(*)..")
.should()
.beFreeOfCycles()
.allowEmptyShould(true);

/**
* Production code must not import unsupported / internal JDK packages.
* These are not part of the Java SE API and may change or disappear without notice.
Expand Down
Loading
Loading