The helper returns normally, and its ByteArrayInputStream test produces ABCDEFGH. Replace that source with another InputStream containing exactly the same bytes, and the helper returns an empty array without attempting a read. Both calls look successful to code that checks only for an exception. Only one result contains the required message.
The underlying mistake is treating InputStream.available() returning zero as an end-of-file signal: an availability estimate has become an undocumented completion rule. A related helper allocates its entire destination from the estimate and creates a zero-length array. Another allocates eight bytes correctly but assumes one read will fill them. Each mistake needs its own evidence because changing buffer allocation alone does not repair completion. The Java SE 21 contract describes available() as an estimate of bytes readable without blocking and explicitly warns that it is not correct to use that estimate to allocate a buffer intended to contain all stream data.
This playbook uses Java Standard Edition (SE) 21 input/output (I/O) APIs and a finite, in-process source. The literal expected sequence is ABCDEFGH. The custom source always reports zero availability and supplies at most two bytes per bulk read. An execution on 21 October 2026 using Microsoft OpenJDK 21.0.12.1+1-LTS on Linux reproduced all ten diagnostic cases successfully.
The release decision remains specific: accept the expected bytes after the declared end-of-file (EOF) boundary, or accept a correctly rejected failure. Repair the availability loop and single-read assumption. Keep exact-length frame completion as a separate claim. No network, external service, timeout mechanism or cancellation behavior participates in this experiment.
Define what complete input means to this caller
Start the review with a sentence the caller can enforce: “For this operation, the source is finite, EOF terminates its byte sequence, and the accepted test result must equal the literal bytes ABCDEFGH.” This separates the acquisition boundary from the content requirement. The helper collects what the source supplies; the acceptance check decides whether that sequence is the one required.
An EOF-delimited finite source permits a reader to continue until a positive-length read returns -1. Java SE 21 documents -1 as the InputStream read result when the end of the stream has been reached; for a nonempty byte array, a bulk read otherwise attempts to read at least one byte. A known-length frame has a different boundary: the caller requires a specified number of bytes and may intentionally leave subsequent bytes unread. An unknown live stream supplies neither premise merely because its type is InputStream. Its owner must explain what completes an operation before a whole-input helper is selected.
This distinction changes how failures are interpreted. Ordinary EOF is a valid acquisition boundary for the finite helper. If only six bytes arrive where eight specific bytes were expected, the caller’s equality check must reject the result. A reader that knows an eight-byte frame length can instead reject that shortfall while acquiring the frame. Neither policy should be inferred from a momentary availability value.
Write down whether empty input is valid, who owns the source, and whether the helper starts at the beginning or at an already advanced position. In this laboratory, each case creates its own source. The main successful case starts at position zero; the empty case deliberately contains no bytes. No case depends on a previous test’s consumption.
The broader habit of requiring observable results also appears in Refonte Learning’s Java validation evidence. That neighboring article concerns java.util.stream validation and per-record evidence rather than java.io.InputStream, so it is useful as a review-practice comparison, not as authority for byte-stream semantics. Here the review object is a byte sequence. Keep that object explicit in the test names, expected values and release decision so a successful method return cannot quietly replace the agreed contract.
Build a source that exposes the interface contract
The oracle is deliberately small: ABCDEFGH, encoded using US-ASCII. Each expected character identifies one byte in a known position. A missing suffix, duplicated chunk, reordered pair or inserted zero byte changes the expected sequence. There is no parser, generated fixture population or external response whose correctness must first be established.
Chunked clones the supplied array, stores a current position, and counts positive-length read attempts in reads. Its available() method always returns zero. Its bulk-read override copies no more than two bytes, even when the destination has room for four or eight. These choices are independent: zero availability exposes the stopping rule, while short reads expose the assumption that destination capacity determines how much one call returns. Java SE 21 explicitly permits a bulk read to return fewer than the requested maximum.
The zero-availability behavior is not exotic relative to the abstraction. The base InputStream.available() implementation itself is specified to return zero, while subclasses can provide more informative estimates. The generic contract therefore cannot interpret zero as proof that no future read can return data. The synthetic override makes that fact visible in the fixture rather than depending on an implementation detail hidden from the reviewer.
The optional failAt value makes the error case deterministic. With failAt=2, the first bulk call supplies AB; the next throws IOException before advancing again. The source also handles a zero-length request explicitly, returning zero without increasing its read counter. That lets the allocation case distinguish requesting nothing from discovering that nothing remains.
The complete source appears in the regression section and must be saved as AvailableLab.java. The source used for the recorded run has SHA-256 digest b8a78980aaa9f2870819392c12e7d7cd547f56ec4617116f44923f3127af891a. Preserve that source alongside its result. Reformatting or changing line endings can change the digest even when the Java logic is equivalent; record the changed file as a separate artifact rather than assigning it the previous identity.
The execution record for the source printed below reports PASS_10_CASES, with start time 2026-10-21T14:18:24.722291141Z and finish time 2026-10-21T14:18:24.766354710Z. These timestamps identify this run, not a performance benchmark. Java SE 21 is the documentation baseline; Microsoft OpenJDK 21.0.12.1+1-LTS identifies the tested runtime. A documentation version label should not be treated as a publication date.
Do not let one convenient subclass define the interface
The Java SE 21 ByteArrayInputStream documentation specifies that available() returns its number of remaining bytes, concretely count - pos. That behavior explains why the legacy loop succeeds with this particular control. Its exact count is useful for understanding this subclass, but it does not establish a generic InputStream completion rule.
That is the testing trap. ByteArrayInputStream is an excellent deterministic fixture for many purposes, but here one of its subclass-specific conveniences supplies exactly the information that the defective caller incorrectly expects every InputStream to provide.
Keep the convenient control because it reproduces the misleading confidence. Pair it with the custom source, using identical payload and helper code. The useful test question becomes whether the helper still satisfies its declared contract when the fixture stops supplying convenient information that the broader abstraction does not promise.
This is also why a passing unit test should be read in terms of the behaviors it challenged. “Works with ByteArrayInputStream” is a smaller claim than “implements the generic InputStream contract needed by this caller.” The second claim needs a source capable of separating availability from EOF and buffer capacity from read completion.
Run the availability-based loop on both sources
The legacy helper creates a staging output and enters its loop only while input.available()>0. Inside, it reads one byte, breaks if that read reports EOF, and appends otherwise. The inner EOF check looks reasonable during a quick review. The defect sits before it: the loop condition can prevent the read that would provide meaningful completion evidence.
J1 supplies a ByteArrayInputStream over the eight literal bytes. The helper returns ABCDEFGH. That result establishes that the tested combination of helper and subclass delivered the expected content. It gives no evidence about what happens when availability does not track the source’s remaining length.
J2 changes the source to Chunked and preserves the payload. The observed result is empty, with position=0 and reads=0. Those two fields are decisive. The source did not reject a request, reach EOF during a read, or return an unexpected short chunk. The helper never asked it for data. All eight bytes remain behind the gate that was supposed to decide whether reading should continue.
The behavior is consistent with the Java SE distinction between availability and read completion: available() can report zero as an estimate, while a real read is what returns data or ultimately reports -1 at EOF.
The harness marks J2 as passing because this is the expected unsafe outcome. A diagnostic assertion can pass while the candidate it examines is unsuitable for release. The pass confirms that the test reproduced the defect under controlled conditions. It does not convert the empty output into an acceptable representation of ABCDEFGH.
Preserve J1 and J2 together before changing the helper. J1 explains why the original tests gave confidence; J2 supplies the counterexample that invalidates the generic claim. Removing J2 after the repair would discard the precise evidence needed to detect a future return to availability-based completion.
For an adapter review, trace the actual expression controlling entry and exit. A method can contain a correct EOF branch while still making EOF unreachable for legitimate sources. Also inspect what happens after the loop: returning staged bytes unconditionally turns every path out of the loop into apparent success. The repair must make the approved completion boundary the only normal publication path, while retaining error propagation for unsuccessful acquisition.
Separate availability from EOF and total length
The Java SE 21 InputStream contract defines available() as an estimate of bytes that can be read or skipped without blocking. It says the value may be zero and separately notes that zero can also occur when end of stream is detected. Most importantly for the allocation bug, the documentation states that it is never correct to use this return value to allocate a buffer intended to hold all data in the stream.
A positive-length read answers another question. read() returns a byte or -1 at EOF; read(byte[]) returns the number of bytes actually obtained or -1 when there is no more data because EOF has been reached. A bulk read may return fewer bytes than its array capacity. The estimate and the read result therefore cannot be used interchangeably.
In this fixture, available() remains zero before the first byte, between chunks and after the final byte. It therefore cannot distinguish any of those positions. The same value accompanies an untouched eight-byte source and an exhausted source. A decision based only on that value loses the information the caller needs to tell these situations apart.
Do not reinterpret zero as a timer or a reliable indication that a source is closed. available() supplies no waiting duration. The documentation even allows a subclass to throw IOException after close rather than defining zero as a universal closed-state marker. Likewise, treating zero as a general readiness predicate would reject the immediately readable bytes in Chunked. The controlled source makes that reasoning visible without simulating a transport or depending on scheduling.
The important signals in the finite helper are a positive count, EOF, or a reported exception. Positive progress contributes exactly that many bytes. EOF permits the helper to finish acquisition. An exception prevents normal publication. Content validation then decides whether the acquired sequence matches the independently declared expectation. Keeping those steps separate prevents a single convenience value from acquiring several incompatible meanings.
A zero-length buffer cannot demonstrate an empty source
J3 allocates new byte[b.available()]. Because the estimate is zero, the array length is zero. Calling b.read(zero) returns zero and leaves the source position unchanged. The recorded evidence is length=0;read=0;position=0. There were eight source bytes, but the caller supplied no space in which to receive any of them.
The Java SE 21 InputStream.read(byte[]) contract specifies zero-length behavior: when the supplied array has length zero, no bytes are read and zero is returned. Chunked implements that rule explicitly. For a nonempty array, the contract instead attempts to obtain at least one byte unless EOF is already reached. Subclass details still matter: ByteArrayInputStream documents a bulk-read exception to the zero-length rule at EOF, returning -1. J3 uses an unread, nonempty Chunked, so that exception does not affect its result.
The result therefore establishes that a request for no bytes made no progress. It does not establish an empty source, an EOF observation or a complete message. Increasing the destination from zero to a fixed positive size removes this particular obstacle, but the next section shows why allocation and completion still require separate repairs.
J3 also differs from the repaired helper’s nonprogress check. That check follows a read into a nonempty buffer. Treating its zero result as an error prevents the helper from spinning without progress if an implementation violates the expected positive-length behavior. J3 deliberately requests zero bytes, so it is not a recorded execution of that guard. Preserve this distinction when reporting coverage: similar numeric results can belong to different preconditions and require different interpretations.
Show that one bulk read can return a prefix
J4 gives the custom source an eight-byte destination and makes one bulk read. The source returns two. The observed content, restricted to the returned count, is AB; the diagnostic string is AB;read=2. There is enough destination capacity for the entire payload, yet the call supplies only its first pair. That is the exact counterexample needed to reject a one-read completion assumption.
The Java SE contract is explicit: read(byte[], int, int) reads up to the requested length, and an attempt to read that many bytes can return a smaller number. The actual count is the authoritative description of what was placed in the destination by that operation.
A short positive result is useful progress. The acquisition mistake occurs when the caller treats it as the finished message. For this finite fixture, more bytes remain and another read can obtain them. Rejecting the source merely because it supplied fewer bytes than the buffer could hold would impose an additional requirement that this helper has no reason to demand.
Review both the count and the slice passed downstream. The valid contribution is the prefix from index zero through index n-1. Java’s documentation states that elements after the returned byte count are unaffected by the read. Appending the entire destination would therefore include positions that this read did not fill. Reusing a buffer makes this especially easy to miss because later short reads must not cause earlier buffer contents to be appended again.
The canonical diagnostic uses Arrays.copyOf(once,n) before displaying J4’s content. That makes the distinction between array capacity and received length explicit. An eight-slot array is storage; the return value identifies which slots contain newly acquired bytes. Neither the capacity nor the absence of an exception substitutes for the literal sequence comparison.
Another tempting repair is to keep reading only while each call fills the buffer. Applied to this custom source and a four-byte buffer, that rule would stop after the first two-byte contribution. It would reproduce the truncation through a different condition. For the chosen finite contract, every positive count is appended, and only the declared completion or failure condition ends acquisition.
J4’s passing assertion confirms the prefix behavior. The acceptable response is to repair the caller’s accumulation logic while retaining the test as a regression control. Do not change the fixture to return eight bytes simply to restore the original assumption. The small chunk size is what makes this test useful during reviews of adapters that otherwise see only convenient in-memory sources.
Read the finite source to its declared completion boundary
The repaired complete helper uses a four-byte working buffer and stages each positive contribution locally. It keeps reading until -1, then returns a copy of the accumulated bytes. The Java SE 21 ByteArrayOutputStream documentation describes a buffer that grows as bytes are written and specifies that toByteArray() creates a newly allocated array containing the valid accumulated bytes. The helper’s control flow determines when those staged bytes become a returned result.
Trace the supplied fixture through that flow. Each successful bulk call contributes at most two bytes. The four contributions form AB, CD, EF and GH; the following read reaches EOF. J5 records ABCDEFGH;position=8. The sequence comes from accumulating each returned slice, and the normal return is reached through the EOF branch. The report does not separately record a J5 EOF-read counter.
Three checks keep the staging logic explicit. A negative cap is rejected before reading. A zero count from the nonempty working buffer raises IOException. A chunk exceeding the remaining permitted staging size raises another IOException before it is appended. These branches prevent specified misuse or invalid progress from being converted into a normal completed result.
The comparison n>cap-out.size() checks the remaining allowance. The helper maintains the invariant that staged size does not exceed a nonnegative cap. In the main control, the cap is 64 and the payload is eight bytes. The value supplies room for the demonstration without making availability part of allocation or stopping.
The cap is deliberately local. ByteArrayOutputStream grows dynamically as needed; nothing in that class turns the application’s value of 64 into a built-in stream limit. The helper enforces the policy before appending a chunk that would exceed the permitted staged payload size.
Acquisition still does not know the required message. If a finite source ended normally after ABCDEF, this helper would return those six bytes; the literal expectation would reject them. The repaired loop establishes how acquisition finishes, while the external comparison establishes whether its result satisfies this operation.
Keep ownership and resource limits explicit
The helper does not close its caller-owned input. The caller must manage that lifetime and decide what happens after an error. This is an ownership choice in the example, not a property inferred from InputStream.
Staging also has a precise scope: the cap limits accepted payload bytes in the local accumulator. It is not an exact heap ceiling, because the working buffer exists separately and toByteArray() allocates a new array containing the accumulated content.
A read occurs before the cap check, so cap rejection can follow additional source consumption. The cap does not set a deadline. Java’s InputStream read contract states that a read may block until data is available, EOF is detected or an exception occurs. Reading to EOF on an input that has no finite completion boundary may therefore wait indefinitely unless the owning layer imposes its own relevant transport policy.
That limitation is essential. The experiment is a finite, in-process source. It does not prove that “read until EOF” is a safe whole-message strategy for an arbitrary socket or persistent connection. Timeouts and cancellation belong to the owner that understands that transport; neither is implemented or tested here.
The ten cases also leave the negative-cap, over-cap and nonprogress branches unexercised. Retain them as reviewed safeguards without inventing additional successful controls. If an application depends on their precise observable behavior, add named cases and record them as separate evidence.
Verify empty input under the same contract
J6 gives the repaired helper an empty source: new Chunked(new byte[0]). The helper still supplies its four-byte buffer and attempts a positive-length read. The source immediately reports EOF, so complete returns an empty array. The recorded observation matches the expected empty string. J6 permits an empty message and requires every byte before the declared finite boundary.
Compare that outcome with J2 before approving either result. Both observations contain no output bytes, but their acquisition histories differ. J2 never requests a byte from a source holding ABCDEFGH. J6 attempts to read a source holding no bytes and reaches its completion boundary. Output size alone erases precisely the distinction that this review needs to retain.
The Java SE InputStream contract distinguishes these states through the read operation itself: -1 means the end of the stream has been reached. In Chunked, a zero-length request returns zero without reading anything. J3 makes that empty request against a nonempty source; J6 supplies a nonempty destination and receives EOF from an empty source. Their equal output lengths therefore represent different acquisition histories.
This is also why an empty result needs an explanation in an ingestion helper. An importer might legitimately receive an empty export, or its surrounding application might require content. Those are different policies. The byte reader can faithfully report an empty finite sequence while the caller separately rejects that sequence under a domain rule. Neither policy should be inferred from an availability estimate.
Keep the empty source in the same test suite as the nonempty source. Removing J6 after repairing J2 would leave an obvious regression opportunity: a developer could reject all empty results and appear to solve the original loss. That change would conceal the invalid completion test while introducing a different error for a permitted empty input.
The correct comparison therefore includes the fixture identity, the helper used and the expected bytes. For J6, the expected sequence has length zero. For J2, the independently declared sequence has length eight. An empty array matches the first oracle and contradicts the second, even when both helper invocations return normally.
J6 establishes this behavior for the supplied finite custom source. It does not establish that every source reporting zero availability is empty, that every empty source has the same error behavior, or that a transport has completed. The acceptance claim remains narrow and useful: under the selected contract, actual EOF before any bytes is a valid empty sequence.
Fail without publishing a partial prefix
J7 introduces a different failure boundary. Its Chunked instance contains the full payload but has failAt=2. The first bulk read delivers AB; the next bulk read throws the controlled IOException. Source position two shows that acquisition began before the failure. It does not describe an untouched input or a completed message.
The crucial publication mechanism is the assignment published=complete(e,64). Java evaluates the helper call before assigning its returned value. Because this invocation throws, the assignment does not happen and published remains null. The recorded observation is published=false;error=IOException;position=2. That combination establishes rejection after progress, with no returned completed result.
The helper’s staging buffer matters here. It accumulates bytes privately and returns a copy only after EOF. In this case, AB remains an internal prefix during the failed invocation. An implementation that writes each chunk directly into a consumer’s completed-results collection would need a different publication arrangement; merely adding the same read loop would not undo those earlier writes.
The negative test catches IOException, the expected failure family, and records its class. It does not treat every exception as successful rejection. An assertion error, a coding mistake or an unexpected unchecked exception must still fail the laboratory. The outer report handler records such failures and rethrows them; that reporting mechanism does not convert them into passing cases.
Preserve diagnostic progress without labelling it complete
The retained evidence identifies the error and the consumed source position. It does not save the staged AB bytes as a completed artifact. Keep that distinction when translating this fixture into application diagnostics: progress can explain a failure without becoming an accepted payload. Also avoid describing this report as a full capture of every intermediate buffer; it records the fields the test actually checks.
Local staging protects the return boundary, but it does not roll back the source. A caller that receives the exception must decide what happens to the already consumed input and its owning resource. The helper provides no reset, replay or recovery promise. Continuing from the current position as though nothing happened would discard that history. The source owner, rather than this helper, also remains responsible for closing it.
One Java detail matters when generalizing the failure case. The default InputStream.read(byte[], int, int) implementation repeatedly invokes single-byte read(). Its Java SE 21 documentation says that if a later single-byte invocation throws after earlier bytes were obtained, the default implementation catches that later IOException and returns the bytes read so far as a short count.
Chunked does not use that default bulk implementation: it overrides read(byte[], int, int) and deliberately throws at its controlled failure boundary. J7 therefore proves the handling of an IOException exposed by this fixture. It does not establish that every possible InputStream subclass will expose a mid-operation lower-level error through an identical bulk-read call.
That distinction is exactly why adapters should preserve the semantics of their actual source instead of claiming more than the interface and experiment show. Approve the observed propagation and publication behavior for the source contract actually tested.
Use readFully only for an explicit frame length
A known-length frame asks a different question from the finite EOF-delimited helper. The caller supplies a required number of bytes and accepts the frame only when that requirement is satisfied.
DataInputStream implements Java’s DataInput contract and exposes readFully. The Java SE 21 DataInputStream documentation points to that general contract, while the DataInput.readFully specification states that the method normally returns only after the requested bytes have been read; EOF before that point produces EOFException, and another I/O failure produces another IOException. It also warns that some but not all destination bytes may already have been updated when an exception is thrown.
J8 requests an eight-byte frame from the eight-byte payload. The destination starts as new byte[8], and the custom source still supplies at most two bytes per bulk read. readFully completes the requested frame despite those short reads. The subsequent assertion checks the content ABCDEFGH, preserving the independent oracle instead of approving the destination merely because its allocated length is eight.
Review the relationship between length and content together. Eight bytes of a different value would satisfy the frame length while failing this fixture’s acceptance rule. Likewise, allocating an eight-byte array supplies storage; it does not establish that eight bytes were received. The post-read comparison remains necessary.
J9 changes the source to the six-byte prefix ABCDEF while retaining the eight-byte requirement. The recorded outcome is EOFException;position=6. That is the exact failure family the readFully contract documents when EOF arrives before all requested bytes. The position establishes the progress made before rejection. The test catches specifically EOFException; a different IOException is not silently accepted as evidence of the expected premature EOF.
A destination array can be partly filled before a frame read fails. Accordingly, code surrounding readFully must keep that array from being released as a completed frame on an exceptional path. The Java documentation explicitly allows partial destination updates before the exception. J9 records the exception and position; it does not retain or assert the partially filled destination. Do not describe its evidence as stronger than those observations.
Compare this with calling complete on the same six-byte finite source. That helper would encounter ordinary EOF after ABCDEF and return those six bytes. It has no parameter declaring eight required bytes. If the caller expected ABCDEFGH, the external content comparison would reject the result. The two approaches place their completion requirements at different boundaries, and neither can infer an unstated expectation.
That operational distinction fits the wider concern of API error-handling foundations, whose scope includes input validation and error handling. The linked article is contextual rather than evidence for the Java contract; the normative byte-reading behavior comes from the Java SE documentation above.
Prove that a completed frame can leave more bytes
J10 requests four bytes from the unchanged eight-byte payload. readFully fills the frame with ABCD. The next single-byte read returns E, producing the recorded observation ABCD;next=E. The four-byte frame is complete, while the underlying input still contains data after that frame. Those statements are compatible because they refer to different boundaries.
The documented readFully(byte[]) obligation is to fill that destination or fail; it does not say that normal return proves EOF immediately follows the frame. J10 supplies the empirical control: after the four requested bytes, another byte is actually read.
This control also prevents a subtle reuse of the availability mistake. The fifth byte is readable, but the custom source’s available() method still returns zero. The evidence comes from actually reading E, not from asking the availability estimate whether a next byte exists.
Returning to that estimate after a successful frame would therefore reintroduce the original defect at a later stage. A frame reader may know that its four bytes are complete without knowing or caring whether more bytes belong to a following frame.
The diagnostic read consumes E. It is appropriate here because the fixture deliberately observes what follows the frame. It is not a general instruction to consume one extra byte after every valid frame. In a sequence of frames, that byte could belong to the next frame, and an unplanned probe would disturb the next consumer’s starting position.
An EOF probe can also block when applied to an input whose next boundary has not arrived. The InputStream contract permits reads to wait until data, EOF or an error becomes available. The finite fixture provides no evidence about such a wait.
Do not replace a protocol boundary with a convenience method
Before using readFully, document who supplies the length and what that length represents. A reviewed format might define a fixed frame size or provide a length through another validated field. This laboratory supplies the lengths directly. It does not validate an external length declaration or establish how an application should interpret the following bytes.
The broader ownership question resembles the concern explored in request-body delivery contracts: define the exact result whose preservation matters before deciding that a transport operation succeeded. That neighboring article concerns HTTP redirects, curl and request bodies, not Java stream acquisition. No HTTP semantics are imported into this laboratory.
Here the Java claim remains narrow. A successful four-byte readFully supports the declared four-byte frame. The subsequent observed E disproves any inference that this success also established whole-stream exhaustion.
If a caller actually requires the entire finite source, use the EOF-delimited policy and compare its complete result. If it requires one frame, preserve the following input for the protocol owner. If neither boundary is known, hold the integration until the owner defines it. A convenience method can implement a boundary, but its name cannot choose that boundary for the application.
Reconcile byte content, length and failure state
Reconciliation starts with the expected bytes, the declared boundary, and the outcome of the attempt. For the main finite source, the oracle is the independent literal ABCDEFGH. Eight returned bytes are insufficient if their content or order differs. An empty result is insufficient when the source still contains the eight declared bytes. A correct prefix is insufficient when the operation failed before its completion boundary.
The laboratory expresses its byte assertions as US-ASCII strings compared with String.equals. It does not call Arrays.equals for those assertions. That representation is exact for this fixed fixture: every payload byte is the ASCII encoding of one declared letter, and the expected text is independently written. Equality therefore establishes the required content and order for these cases. For an arbitrary binary fixture, compare byte arrays directly and retain an unambiguous representation such as hexadecimal for inspection.
The recorded execution produced these observations. The final column interprets them against the appropriate contract; it is separate from whether the diagnostic assertion passed.
Case | Recorded observation | Interpretation |
J1 | ABCDEFGH | Positive control succeeds for ByteArrayInputStream; generic helper remains unsafe. |
J2 | ;position=0;reads=0 | Empty result from an unread nonempty source; repair the availability gate. |
J3 | length=0;read=0;position=0 | Zero-sized allocation establishes no completion evidence. |
J4 | AB;read=2 | One read supplies a prefix; reject it as the complete eight-byte result. |
J5 | ABCDEFGH;position=8 | Accept complete finite output under the EOF contract. |
J6 | Empty string | Accept empty finite input after the helper observes EOF. |
J7 | published=false;error=IOException;position=2 | Accept the rejection behavior; no completed payload was returned. |
J8 | ABCDEFGH | Accept the explicit eight-byte frame. |
J9 | EOFException;position=6 | Accept rejection of the premature eight-byte frame. |
J10 | ABCD;next=E | Accept four-byte frame completion; reject a whole-stream interpretation. |
Read the evidence fields together. J2 records zero read calls as well as an empty value, making the missing acquisition visible. J7 records source progress and the absence of a returned result, preserving the distinction between consumption and publication. J9 names the expected exception rather than reporting an unexplained failure. J10 names the next byte, so its framing conclusion rests on content rather than a remaining-byte estimate.
Some observations are asserted directly; others follow from the displayed code. J6 checks the empty returned value, while the helper’s only successful return is inside the n == -1 branch. Its report does not contain a separate EOF flag. Likewise, J5 records position eight, but position alone would not prove completion in an unknown source. The control flow and fixed source contract supply the remaining context.
An evidence consumer should also reject a report whose overall status, individual case statuses, or error field disagree. Keep disagreement visible rather than selecting whichever field looks favorable.
Require all ten case identities, their expected values, and their observed values when reviewing a rerun. A total of ten successful assertions cannot replace that reconciliation: deleting a negative case and duplicating a positive one would preserve the count while weakening the evidence. Retain the original report even if a later repair passes, and explain any changed expectation before adopting it.
The purpose is a defensible decision about each candidate behavior, not a favorable aggregate label. PASS_10_CASES means the diagnostics agreed with their expected observations. J2, J3 and J4 are successful tests of unsafe assumptions, not approved implementations.
Choose accept, repair, rerun or hold
Choose the disposition from the contract and evidence together. A method returning normally establishes a useful observation, but the caller still needs the promised result.
The wider review habit appears in Refonte Learning’s response acceptance beyond process success, which distinguishes a process-level success observation from the expected response contract. That page is an HTTP/curl comparison only; the Java SE documentation remains the source for InputStream semantics here.
ACCEPT the finite result when the helper reaches the declared EOF boundary and the returned sequence matches the independent expected bytes, including the legitimate empty case. Accept a frame only for its declared length and content. Also accept the tested rejection behavior in J7 and J9: the operation correctly refuses an incomplete success. That decision approves failure handling, not the partial input as a completed message.
REPAIR a completion condition derived from available(), a destination sized from that estimate, or an assumption that one bulk read fills the requested space. Java SE directly warns against whole-input allocation from available(), and its bulk-read contract allows a smaller number of bytes than requested. J1 does not exempt the legacy helper, because J2 demonstrates a source on which the assumption loses the entire payload. J4 requires accumulation to the chosen boundary.
RERUN when the source identity, runtime details, case inventory, or completed execution record is missing. Preserve what exists, restore the evidence prerequisites, and create a new run. Do not relabel a compilation failure or an incomplete report as a successful laboratory merely because earlier preparation results are available. Source SHA-256, runtime version and individual case records make the difference between repeatable evidence and an inherited pass label.
HOLD when nobody can state the message boundary, when the input might remain open indefinitely, or when unexplained behavior prevents a trustworthy decision. Java permits an ordinary InputStream read to block waiting for data, EOF or an exception. More repetitions of this finite fixture cannot resolve an undefined live-stream contract.
This decision vocabulary also prevents a common category error. J9 is not “repair” merely because an exception occurred: under the eight-byte frame contract, its EOFException is the correct rejection behavior. J2 is “repair” even though no exception occurred. The disposition follows the contract, not whether the control flow happened to be exceptional.
Assign the framing contract to its owner
The protocol owner specifies what constitutes one message and where its length comes from. The transport owner specifies blocking, deadlines and cancellation behavior. The helper maintainer implements byte acquisition within those constraints, and the caller owns stream lifetime in this example.
Make those responsibilities explicit in the review. A utility method cannot infer them from availability or invent them by choosing a convenient buffer size. Release approval must identify which contract the repaired path actually implements.
This laboratory intentionally stops before retry policy, cancellation guarantees and socket behavior. Those concerns matter for live I/O, but adding invented answers here would weaken rather than strengthen the conclusion. When those policies are unknown, hold the use case and obtain them from the owning layer.
Release the repaired reader with ten regression cases
Save the complete source below exactly as AvailableLab.java in a fresh working directory. Use a separately installed Java Development Kit supporting Java SE 21, with both javac and java available. The source uses standard APIs and a finite in-process fixture. Running it installs no packages, opens no network connection, and needs no external service or account.
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
public class AvailableLab {
static final byte[] PAYLOAD="ABCDEFGH".getBytes(StandardCharsets.US_ASCII);
static final List<String> CASES=new ArrayList<>();
static class Chunked extends InputStream {
final byte[] bytes;
final int failAt;
int position=0, reads=0;
Chunked(byte[] bytes) { this(bytes,-1); }
Chunked(byte[] bytes,int failAt) {
this.bytes=bytes.clone();
this.failAt=failAt;
}
public int available() { return 0; }
public int read() throws IOException {
byte[] one=new byte[1];
int n=read(one,0,1);
return n<0 ? -1 : one[0]&255;
}
public int read(byte[] b,int off,int len) throws IOException {
Objects.checkFromIndexSize(off,len,b.length);
if(len==0) return 0;
reads++;
if(failAt>=0 && position>=failAt)
throw new IOException("controlled failure");
if(position==bytes.length) return -1;
int n=Math.min(2,Math.min(len,bytes.length-position));
if(failAt>=0) n=Math.min(n,failAt-position);
System.arraycopy(bytes,position,b,off,n);
position+=n;
return n;
}
}
static byte[] legacy(InputStream input) throws IOException {
ByteArrayOutputStream out=new ByteArrayOutputStream();
while(input.available()>0) {
int value=input.read();
if(value<0) break;
out.write(value);
}
return out.toByteArray();
}
static byte[] complete(InputStream input,int cap) throws IOException {
if(cap<0) throw new IllegalArgumentException("negative cap");
ByteArrayOutputStream out=new ByteArrayOutputStream();
byte[] buffer=new byte[4];
for(;;) {
int n=input.read(buffer);
if(n==-1) return out.toByteArray();
if(n==0) throw new IOException("no progress with a nonempty buffer");
if(n>cap-out.size()) throw new IOException("local staging cap exceeded");
out.write(buffer,0,n);
}
}
static String text(byte[] b) {
return new String(b,StandardCharsets.US_ASCII);
}
static String quote(String s) {
return "\""+s.replace("\\","\\\\").replace("\"","\\\"")
.replace("\n","\\n").replace("\r","\\r").replace("\t","\\t")+"\"";
}
static void check(String id,String actual,String expected) {
boolean ok=actual.equals(expected);
CASES.add("{\"id\":"+quote(id)+",\"observed\":"+quote(actual)
+",\"expected\":"+quote(expected)+",\"status\":"
+quote(ok?"PASS":"FAIL")+"}");
if(!ok) throw new AssertionError(id+": "+actual);
}
public static void main(String[] args) throws Exception {
Path dir=Files.createTempDirectory(Path.of("."),"inputstream-evidence-");
String started=Instant.now().toString(), status="FAIL", error="";
try {
check("J1_byte_array_positive_control",
text(legacy(new ByteArrayInputStream(PAYLOAD))),"ABCDEFGH");
Chunked a=new Chunked(PAYLOAD);
byte[] lost=legacy(a);
check("J2_zero_is_not_eof",
text(lost)+";position="+a.position+";reads="+a.reads,
";position=0;reads=0");
Chunked b=new Chunked(PAYLOAD);
byte[] zero=new byte[b.available()];
int n=b.read(zero);
check("J3_available_allocation",
"length="+zero.length+";read="+n+";position="+b.position,
"length=0;read=0;position=0");
Chunked c=new Chunked(PAYLOAD);
byte[] once=new byte[8];
n=c.read(once);
check("J4_single_read_is_short",
text(Arrays.copyOf(once,n))+";read="+n,"AB;read=2");
Chunked d=new Chunked(PAYLOAD);
check("J5_read_until_eof",
text(complete(d,64))+";position="+d.position,
"ABCDEFGH;position=8");
check("J6_empty_source",text(complete(new Chunked(new byte[0]),64)),"");
Chunked e=new Chunked(PAYLOAD,2);
byte[] published=null;
String failure="none";
try { published=complete(e,64); }
catch(IOException ex) { failure=ex.getClass().getSimpleName(); }
check("J7_error_is_not_complete",
"published="+(published!=null)+";error="+failure
+";position="+e.position,
"published=false;error=IOException;position=2");
byte[] full=new byte[8];
new DataInputStream(new Chunked(PAYLOAD)).readFully(full);
check("J8_known_length",text(full),"ABCDEFGH");
Chunked f=new Chunked(Arrays.copyOf(PAYLOAD,6));
failure="none";
try { new DataInputStream(f).readFully(new byte[8]); }
catch(EOFException ex) {failure=ex.getClass().getSimpleName();}
check("J9_premature_eof",failure+";position="+f.position,
"EOFException;position=6");
Chunked g=new Chunked(PAYLOAD);
DataInputStream frame=new DataInputStream(g);
byte[] four=new byte[4];
frame.readFully(four);
check("J10_frame_is_not_whole_stream",
text(four)+";next="+(char)frame.read(),"ABCD;next=E");
if(CASES.size()!=10) throw new AssertionError("case coverage");
status="PASS_10_CASES";
} catch(Exception|Error ex) { error=ex.toString();throw ex; }
finally {
String sha=HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256")
.digest(Files.readAllBytes(Path.of("AvailableLab.java"))));
String report="{\"status\":"+quote(status)
+",\"started_utc\":"+quote(started)
+",\"finished_utc\":"+quote(Instant.now().toString())
+",\"java\":"+quote(System.getProperty("java.runtime.version"))
+",\"vendor\":"+quote(System.getProperty("java.vendor"))
+",\"os\":"+quote(System.getProperty("os.name"))
+",\"source_sha256\":"+quote(sha)+",\"error\":"+quote(error)
+",\"cases\":["+String.join(",",CASES)+"]}";
Files.writeString(dir.resolve("results.json"),report+"\n");
System.out.println(dir.resolve("results.json"));
}
}
}
Compile and run from the directory containing that exact source:
javac --release 21 AvailableLab.java
java AvailableLabTreat a nonzero command as a failed or incomplete execution, not as a completed evidence package. Successful execution prints the path to a newly created results.json inside an inputstream-evidence-* directory.
Keep the generated evidence directory together with the exact source and command results. The report identifies the runtime, vendor, operating system, source digest and run timestamps. A new execution produces its own record; never copy an earlier run's timestamps into that record as evidence of a later execution.
The source digest identifies file bytes, so line-ending or formatting edits can produce a different value. Record that new identity instead of attributing the original digest to modified code.
The recorded run reports PASS_10_CASES. That statement means the ten diagnostic observations matched their expectations, including the unsafe controls. It does not mean ten implementations were approved. J1 through J4 preserve the reasons for rejecting tempting shortcuts; J5 through J10 establish the repaired behavior and framing distinctions within the fixture's scope.
Retain the custom source when moving the repair into regression testing. Replacing every input with ByteArrayInputStream would remove the condition that exposed the original dependency. Preserve the two-byte bulk limit, permanent zero availability, literal content oracle, empty input, controlled failure and frame controls.
Review future fixture changes against those obligations before accepting a new green result. A test suite that quietly changes Chunked.available() to expose the remaining length could remain green while deleting the most important adversarial property.
The negative cases catch their expected exception classes locally. The outer handler records unexpected exceptions or errors and rethrows them, allowing failed execution to remain visible. Do not broaden a negative test until any exception satisfies it: an unrelated programming error must not impersonate the intended premature-EOF or controlled-I/O rejection.
The ten cases do not exercise the staging-cap rejection or nonprogress rejection branches. Those defensive checks remain visible in the source, but this report provides no executed claim for them. If an application depends on their exact behavior, add separately identified cases and retain their own evidence.
Likewise, this suite supplies no transport timeout, cancellation, reliability or performance result. Stop the release claim at the behavior actually tested, and preserve any unresolved deployment contract as a hold rather than silently extending this local result.
Practice contract-driven development with the Software Engineering Program
The transferable skill in this laboratory is specifying what a successful operation must prove. Define the boundary, write the expected content independently, challenge convenient assumptions, and retain failures long enough to explain them. These habits make a small adapter review useful to the engineers who later maintain its callers.
The exercise is complete when another engineer can explain both the accepted bytes and the deliberately rejected alternatives.
That work belongs within broader software engineering practice: implementation, testing and ownership decisions should tell a consistent story. The linked article covers wider software-engineering themes rather than serving as evidence for this Java I/O contract. A practical exercise is to ask another reviewer to identify the accepted result using only the written contract and evidence. If the reviewer needs undocumented knowledge about the fixture, improve the explanation before expanding the helper’s use.
Refonte Learning’s Software Engineering Program lists three months of study and 12–14 hours per week. Its advertised curriculum includes full-stack development, cloud computing and microservices, real-time data processing, software performance optimization, application security, scalable software solutions, and a capstone project. The page describes an internship as a potential opportunity.
These broader engineering topics provide useful settings for practising explicit input and failure contracts. The listed curriculum does not specify Java SE 21 or this InputStream exercise. For structured study across the programme’s advertised software-engineering scope, explore the programme and carry one review question into each implementation: which observed boundary and exact result justify calling this operation complete?
