Skip to content

Bug-hunt fixes: safeWrite, trim, write/close race, statistics - #189

Merged
bernardladenthin merged 44 commits into
mainfrom
ccr-90fc5f6a-vs4s0f
Oct 22, 2026
Merged

bernardladenthin merged 44 commits into
mainfrom
ccr-90fc5f6a-vs4s0f

Conversation

@bernardladenthin

@bernardladenthin bernardladenthin commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Four library fixes from the bug hunt (44 commits; ids, reproductions and red/green tests in BUGHUNT-FOUND.md): safeWrite could be bypassed by a concurrent write(int) (the instance-wide clone flag is gone, cloning is a parameter of the private write path; F02); a concurrent trim() let a reader see a half-emptied buffer (it now copies straight from the FIFO into blocks of at most maxAllocationSize and leaves the count unchanged; F73); a write racing close() could land after the close (the closed check runs under bufferLock, close() sets the flag under the same lock and waits for a write or trim in progress; F149); a trim() whose block allocation failed with an OutOfMemoryError lost the bytes it had already copied while available() still counted them -- every block is now allocated before the FIFO is touched, through the one package-private seam allocateTrimChunk(int), which is how the test provokes the failure (F268). The statistics count the I/O of a trim start signal (F212), and the clampToMaxInt Javadoc and the maxAllocationSize default are corrected (F110).
  • Tests that could pass without checking anything are tightened: the jqwik properties now run in every build (their class name matched none of Surefire's include patterns), the concurrent safeWrite test surfaces background failures, a test that never reached the buffer is removed, a new test pins that close() waits for a write on another thread, a jcstress WriteRacingCloseRace is added, and the close-during-trim tests describe the current trim().
  • Formal verification: a third JPF harness (WriteClose.jpf, writer, closer and reader in three threads) model-checks the order of write and close, which the existing harness cannot see; under RAC the two stress loops and the jqwik tries are scaled down (RAC: spurious ClassCastException from a callee's signals clause at the call site, and a stack trace per call for skipped Arrays.equalArrays clauses (from streambuffer) OpenJML/OpenJML#1050 prints a stack trace per System.arraycopy), and the two suspected OpenJML RAC defects are recorded with their upstream reference.
  • Docs: read(byte[], off, len) waiting for all len bytes is now stated in the Javadoc and the README, with the waitForAnyData() + available() pattern; the trim signals are documented as released under the buffer lock (a release() must not wait for this buffer), and the README's maxAllocationSize section says what happens when an allocation fails during a trim.
  • The branch carries main (PR Drop the ArrayDeque spec workaround, OpenJML 21.0.29 has the fix #188 merged into it, conflicts in CHANGELOG.md / CLAUDE.md resolved by keeping both sides' entries).

Test plan

  • Affected unit / integration tests pass locally: StreamBufferTest, StreamBufferProperties, StreamBufferLincheckTest (307/307), PIT 198/198 (mutationThreshold held), spotbugs:check, spotless:check, the javadoc jar; the OpenJML ESC/RAC and JPF jobs run in the separate formal-verification workflow.
  • CI is green on this branch (head 1204a58: Build, Test, vmlens, Report/PIT, smoke, shared files, code style, OpenJML ESC + RAC, CodeQL, osv; the two GPG jobs are red by design on a non-main branch, SonarCloud's single finding on positionAtCurrentBufferEntry += toCopy is a false positive -- the update runs under bufferLock).
  • Docs / CHANGELOG updated: CHANGELOG.md, README.md, CLAUDE.md, TODO.md, src/test/jpf/README.md, BUGHUNT-FOUND.md / BUGHUNT-CHECKED.md.

Related issues / PRs

Checklist

  • I have read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • My commits follow Conventional Commits -- they are imperative one-line subjects in the repository's existing style, not type(scope): prefixed.
  • No security-sensitive changes

Generated by Claude Code

claude and others added 30 commits October 20, 2026 23:41
writeSingleByte() switched the shared field ignoreSafeWrite on around its
own write. A write(byte[]) on another thread that took the lock inside that
window skipped the safeWrite clone and stored the caller's array by
reference, so a later change to the array reached the reader. The skip is
now a parameter of the write path instead of a field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
BUGHUNT-FOUND.md records every bug found on this working branch with its
file, severity, reproduction test, status and commit. BUGHUNT-CHECKED.md
lists every production class that was read in full, with the blob hash it
was checked at, so a later pass re-checks only files that changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
trim() drained the buffer through its own input stream and wrote the
bytes back, so the volatile availableBytes fell to zero and rose again
under bufferLock. waitForAtLeast reads it twice without the lock and
could return fewer bytes than requested on an open stream; read() then
reported the end of a stream that still held data. trim now copies the
entries directly and leaves the count untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Add the missing anchors the overview links point at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
The Javadoc of clampToMaxInt still explained the clamp through the trim()
loop that read the buffer back through its own input stream; since 89d2173
trim() copies straight from the deque and the only caller is available().
The README named Integer.MAX_VALUE as the maxAllocationSize default; since
f88be77 it is MAX_ARRAY_LENGTH (Integer.MAX_VALUE - 8), and the setter caps
larger values to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
A write that raced close() from another thread passed requireNonClosed()
before taking bufferLock, while closeAll() set the flag without the lock.
The write could then append after close() had returned and still return
normally, while a reader in between had already reported the end of the
stream. The check now runs under bufferLock and closeAll() sets the flag
under it, so such a write is either buffered before the close or fails
with "Stream closed".

The test parks a second writer on the buffer lock from the trim-start
signal and closes while it waits; it fails without the change. A jcstress
test for the same race is added for the opt-in profile run.

Also document that read(byte[], off, len) waits for all len bytes, the
design since the first version, and how to read what is available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Everything the bug hunt left open - decided but not implemented, waiting
for a decision, and the CI and hardware runs - is listed in TODO.md with a
pointer to its ledger entry, so it survives the end of the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Every measurement and CI or hardware run the bug hunt left open, with the
commands, the hypothesis, what the result decides and an answer field, so
an agent can run them all at once and answer in the repository. TODO.md
points to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
All four points ran on a throwaway VM (8 vCPU).

- S1: jcstress 604/604; WriteRacingCloseRace never shows the forbidden
  "1, -1" in 3.54 billion samples, so F149 holds.
- S2: vmlens green (19 interleavings).
- S3: ESC 14/14, JPF green, RAC green for 287 of 288 tests including the
  F149 test and the F73 trim. The excluded test is the 2-million-write
  concurrent trim test, which under RAC prints a stack trace per arraycopy
  (21 GB of log) and needs about two hours; the openjml-rac job would time
  out on it without any contract being violated.
- S4: a throwaway JPF harness with separate writer, closer and reader
  threads finds no error with the fix and finds the race before F149.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bvDWEYcnzbZvnHgzS2KuX
JpfStreamBufferHarness writes and closes from one thread, so it could not
see bug-hunt finding F149 (a write passing the closed check outside the
lock and adding its bytes after the reader saw the end). The new harness
runs writer, closer and reader in three threads: no errors over 75,438
states with the fix, and against the classes before F149 it reports
"reader got 1 bytes, but 2 writes returned normally" after 3,956 states.
The jpf-interleavings job runs and gates both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
… timeout

Under RAC every System.arraycopy and clone prints a ~10 KB stack trace for
an uncompiled model method of the bundled JDK specification, so the
concurrent-trim test's 2 million writes took 7,344 s and 21 GB of log and
the safeWrite test 753 s, with every contract holding (measured). The
jml-rac profile sets streambuffer.test.rac, and both loops run 5,000 and
2,000 iterations there: 30 s and 24 s under RAC; the default build keeps
2,000,000 and 200,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
StreamBufferProperties matched none of Surefire's default include
patterns, so its 5 properties ran in no build, though CLAUDE.md and the
jml-rac profile counted on them. The default patterns plus
**/*Properties.java are listed now: 299 -> 304 tests, all green. Under RAC
they run with 100 tries instead of 1000 (434 s -> 21 s); the full RAC
suite, 293 tests, takes 4 min 19 s on 4 cores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
…t item

S1-S4 are measured (BUGHUNT-MEASUREMENTS.md): jcstress, vmlens, ESC, RAC
and JPF are green with the F149 and F73 changes, so the item goes. Two
OpenJML suspicions from the RAC run are written down to be verified and
filed with the awaited release: a signals clause evaluated for an
exception of another type, and a stack trace per call for an uncompiled
model method of the bundled JDK spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Both are confirmed, identically on OpenJML 21.0.28 and on a dev-21 build
(529ac454da).

- signals cast: RAC's call-site check casts the thrown exception to the
  type of a signals clause without an instanceof test when the clause's
  predicate folds to false for the actual arguments (waitForAtLeast(1L)).
  It is only reported, never thrown, even under rac=exception. Located in
  JmlAssertionAdder.applyHelper (21.0.28 l. 12326/12341, dev-21
  l. 12581/12596). Minimal reproducer and counter-checks included.
- stack trace per call: every System.arraycopy/clone prints the skipped
  Arrays.equalArrays clause with a full stack trace (1000 calls, 1000
  traces) from runtime/Utils.java:296-304; compiling with -quiet removes
  it while real violations are still thrown.

Two issue drafts in the style of OpenJML#1012 are included; nothing was
reported upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bvDWEYcnzbZvnHgzS2KuX
Both were confirmed on 21.0.28 and dev-21 and filed together on
2026-10-19 as OpenJML/OpenJML#1050. TODO.md and the measurement record
now say so instead of "suspected, not yet filed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bvDWEYcnzbZvnHgzS2KuX
The stress-loop scaling and the reduced jqwik tries under -Pjml-rac exist
only because of the per-call stack trace filed as #1050. The test helper,
the profile comment, CLAUDE.md and the OpenJML upgrade checklist in
TODO.md now name the issue, so the workaround is dropped when a release
fixes it and not before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
S1-S4 are measured and acted on (JPF write/close harness, RAC scaling,
jqwik includes), and the two OpenJML defects are filed as #1050; the
brief with all results stays in the history at 5d4819b, which TODO.md
names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
A review of the whole branch found no defect, only text the fixes had left
behind:

- requireNonClosed() said it must not be called under the lock; since F149
  it is, on purpose. The Javadoc says so now, and @GuardedBy("bufferLock")
  states it for Error Prone.
- isTrimRunning and shouldTrim() explained the flag by trim's own stream
  reads and writes, which F73 removed. They now name what is left: a trim
  signal whose release() writes on the trim thread (bufferLock is reentrant).
- close() and the README "Thread Safety" section did not say that close()
  waits for a write or trim in progress (F149); one sentence each.
- StreamBufferTest still described the removed ignoreSafeWrite field and
  trim's read/write phases. Four tests are renamed after what they check
  (write_wrappingStreamThrows_bufferStaysUsable,
  trim_triggeredByWrite_flagIsResetAfterwards,
  trim_ofSeveralChunks_flagIsResetAndDataIntact,
  trim_withSafeWrite_allDataPreserved), their comments and the section's
  requirement list rewritten; no assertion changed.
- src/test/jpf/README.md: maxDepth 127, as measured (was 130).
- The ledger: F02, F73 and F149 get a "Nachgemessen" line with the results
  of the measurement brief (jcstress, vmlens, ESC 14/14, RAC, JPF), and
  BUGHUNT-CHECKED names the former open suspicion as F149.

mvn test 304/304, spotbugs 0, package incl. javadoc without warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Since F73 trim() copies straight from the FIFO and does no stream I/O,
so the two isTrimRunning guards around the statistics no longer kept
any internal I/O out of the counters. The only I/O they still excluded
was a write or read a trim start signal makes in its release() on the
trim thread: such a write raised availableBytes but neither
totalBytesWritten nor maxObservedBytes, such a read lowered
availableBytes without counting, contrary to the getter Javadoc, the
README and the invariant written == read + available. The guards are
removed; the one in shouldTrim() stays and still blocks a nested trim.

The new StatisticsTests cases were red before the change ("Expected:
is <12L> but: was <9L>" for the hook write, "Expected: is <1L> but:
was <0L>" for the hook read) and are green with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
claude added 10 commits October 20, 2026 23:44
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
The F212 entry was added without its row in the overview list and
without the anchor the list links to, unlike every other entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
safeWrite_concurrentSingleByteWrite_arrayStillCloned only called
Waiter.resume(), which in ConcurrentUnit 0.4.6 never rethrows (only
await() does), so a reader that died early left the mutated-byte count
at zero and the test passed without checking anything. Both background
threads now record any Throwable in an AtomicReference, the writer and
the reader count their bytes, and the test asserts no failure and
bytes read == 8 * array writes + single bytes written. The older
concurrentReadWrite stress test now calls await() after resume(), so
the cause of a dead reader reaches the test thread, and its comment no
longer claims resume() re-raises.

With an IOException injected into the read path the old version stayed
green and the new one is red ("Expected: is null but: was
<java.io.IOException: injected reader failure>", "Expected: is
<1812184L> but: was <0L>"); without the injection it is green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
write_wrappingStreamThrows_bufferStaysUsable (formerly
trim_ignoreSafeWriteFlagResetDuringWriteException_streamRecoverable)
overrode getOutputStream() with a wrapper that threw before it called
the real stream, so the act step never entered StreamBuffer and the
asserted message was the test's own. It was already vacuous before
F73: trim() wrote through the private os field and never saw the
wrapper. The branch renamed it and kept it; it is removed instead.

Its remaining recovery steps are covered more strictly by
trim_withSafeWriteEnabled_preservesDataIntegrity and
trim_withSafeWrite_allDataPreserved, and a failure inside the real
trim, with isTrimRunning reset and the stream recovering, by the two
trim_signalReleaseException* tests. Test-only change; StreamBufferTest
289/289 green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
F149 has two halves: requireNonClosed() under bufferLock in
writeFromArray, and streamClosed set under the same lock in closeAll().
The only deterministic test calls close() from the trim-start signal on
the thread that already holds the lock, so it stays green when only the
closeAll() half is reverted; only the opt-in jcstress race and the
scheduled JPF harness would see that regression.

close_fromAnotherThreadDuringAWrite_waitsUntilTheWriteIsBuffered starts
the closer as a thread of its own inside the signal, records its state,
isClosed() and the buffered count there, and asserts after the write
that the closer was BLOCKED, the stream still open with 2 bytes, and
closed with 2 bytes afterwards.

With the lock removed from closeAll() alone it is red in 3 of 3 runs
("Expected: is <BLOCKED> but: was <TERMINATED>", "Expected: is <false>
but: was <true>") while the existing test stays green; with the fix it
is green in 5 of 5 runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Since F73 trim() copies straight from the FIFO and never reads or
writes through the streams, but the texts of the close-during-trim
tests still described a read phase, a write-back through os.write()
and a requireNonClosed() check inside trim. A failure of one of them
would have sent the reader after a mechanism that no longer exists.

trim_closeDuringTrim_writeBackDoesNotThrowAndPreservesData is renamed
to trim_closeDuringTrim_triggeringWriteDoesNotThrowAndPreservesData;
its Javadoc, comments, display name and assertion message now say what
it guards: a trim hook closes the stream re-entrantly inside trim's own
critical section, and the triggering write, which already passed
requireNonClosed(), must not throw nor lose any of the 9 bytes. The
description of trim_closeCalledDuringTrim_handlesGracefully, the trim
signal tests and the robustness header drop the stale source line
numbers and trim phases; requireNonClosed() is called private and
checked under bufferLock. Text only, the assertions are unchanged;
StreamBufferTest 290/290 green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
BUGHUNT-CHECKED.md counts a class as checked only while its blob hash
still matches HEAD, yet no line named a StreamBuffer.java blob after
F73: the F149 fix, the Javadoc follow-up and F212 were never recorded,
and the 5e0f51d statistics line still explained isTrimRunning by
trim's own stream reads and writes, which F73 removed.

That line is replaced by one for blob cbb7f64 that states the post-F73
facts: trim() copies straight from the FIFO, the only I/O during a trim
is a trim start signal's release() on the trim thread, the statistics
guards that hid it were F212, and isTrimRunning now only blocks a
nested trim in shouldTrim(). A second line records the re-check of the
F149 write/close path at the same blob. Documentation only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
[Unreleased] gains a Fixed entry for the trim start signal I/O that the
statistics left out, and Changed entries for the test work of the
branch: the jqwik properties running in every build, the safeWrite
test surfacing background failures, the removed wrapping-stream test,
the new close-on-another-thread pin, the close-during-trim test texts,
the third JPF harness and the RAC scaling of the stress loops. TODO.md
was checked and needs no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
The Unreleased section named the CI fixes, the statistics fix and the
test work, but not the three user-visible StreamBuffer fixes the bug
hunt made on this branch: the safeWrite bypass by a concurrent
write(int), the half-emptied buffer a reader could see during trim(),
and the write that could land after a close() from another thread.
The documented read(byte[], off, len) semantics are listed under
Changed, since consumers may have relied on the undocumented form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
The branch was rebased onto main (two commits there: the dependency,
plugin and shared-pin bumps), which gave every commit of the branch a
new hash. The bug-hunt ledgers, the measurement notes, the TODO and
the JPF harness documentation cite commits of this branch by hash;
each citation now names the rebased commit with the same change
(matched by author date and subject, 1:1 for every non-merge commit).
Nothing else changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
@coveralls

coveralls commented Oct 21, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 100.0% — ccr-90fc5f6a-vs4s0f into main. No base build found for main.

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.89%. Comparing base (920fc7d) to head (1204a58).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #189      +/-   ##
============================================
+ Coverage     96.99%   98.89%   +1.90%     
- Complexity      116      119       +3     
============================================
  Files             1        1              
  Lines           266      271       +5     
  Branches         29       28       -1     
============================================
+ Hits            258      268      +10     
+ Misses            2        0       -2     
+ Partials          6        3       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

PR #188 (OpenJML 21.0.29 fixups v3: no bundled spec deleted at install any
more) merged first; the CHANGELOG and CLAUDE.md entries of both sides are
kept, main's corrected facts replacing the branch's "ArrayDeque.jml still
deleted" bullet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
…e spec-deletion re-try from TODO.md

close() takes bufferLock since the write/close fix, and a trim start or end
signal is released under that lock: a release() that waits for a read, write
or close of this buffer would hold close() on another thread behind it. The
Javadoc of close() and of the two add*Signal methods say so.

TODO.md's "on every OpenJML upgrade" bullet still asked to re-try dropping
"the two spec-file deletions"; since PR #188 (fixups v3) the setup action
deletes none. Both found by the review of PR #189.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
The consolidation allocated its chunks one at a time, each after the
entries of the previous chunk had already been polled from the deque, so
an OutOfMemoryError on a later chunk left the buffer with those entries
gone while availableBytes still counted them: a reader then failed on an
empty deque or read the wrong bytes.

trim() now allocates every chunk first and copies afterwards, so the
error propagates out of the write with the buffer exactly as that write
left it, and the next write trims again. The one allocation goes through
the package-private seam allocateTrimChunk(int), which the new test
overrides to fail the second allocation: red against the old loop
(element count 1 instead of 3), green after the rework. Suite 307/307,
javadoc, SpotBugs and PIT clean; no JML change (trim() is not declared
in the spec file, the seam is outside the ESC list).

README (maxAllocationSize), CHANGELOG and the bug-hunt ledger updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@bernardladenthin
bernardladenthin merged commit bd85a23 into main Oct 22, 2026
30 of 34 checks passed

This branch had an error being deployed

1 failed and 1 active deployments
maven-central — 1204a58a Deployed Oct 21, 2026 by bernardladenthin via Verify GPG signing key (no secrets printed) #344
startgate — 1204a58a Deployed Oct 21, 2026 by bernardladenthin via Start gate (abort window) #344
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.

3 participants