@@ -258,10 +258,33 @@ mod tests {
258258 /// ~512 KB block (caught by the per-block size bound below).
259259 const COALESCE_INPUT_BLOCKS : usize = 64 * 8 ;
260260
261+ /// Deterministic, per-block-unique fill for the block with the given
262+ /// `batch_serial`. The bytes vary by position (a tiny LCG seeded by the
263+ /// serial) so two distinct blocks never share a byte pattern — this is what
264+ /// lets the full-stream oracle catch reorder/drop/duplicate bugs, not just
265+ /// total-byte conservation. Both the source and the independently-built
266+ /// expected stream call this, so the only way the streams match is if
267+ /// coalesce concatenates every block exactly once, in order.
268+ fn block_fill ( batch_serial : u64 ) -> Vec < u8 > {
269+ // Seed off the serial; +1 avoids a degenerate all-zero seed for serial 0.
270+ let mut state = batch_serial. wrapping_mul ( 0x9E37_79B9_7F4A_7C15 ) . wrapping_add ( 1 ) ;
271+ ( 0 ..COALESCE_INPUT_BLOCK_BYTES )
272+ . map ( |_| {
273+ // SplitMix64-style step for a well-mixed per-byte sequence.
274+ state = state. wrapping_add ( 0x9E37_79B9_7F4A_7C15 ) ;
275+ let mut z = state;
276+ z = ( z ^ ( z >> 30 ) ) . wrapping_mul ( 0xBF58_476D_1CE4_E5B9 ) ;
277+ z = ( z ^ ( z >> 27 ) ) . wrapping_mul ( 0x94D0_49BB_1331_11EB ) ;
278+ // Keep the low byte; truncation is intentional (we want a u8 fill).
279+ ( ( z ^ ( z >> 31 ) ) & 0xFF ) as u8
280+ } )
281+ . collect ( )
282+ }
283+
261284 /// Source emitting `remaining` fixed-size `DecompressedBlock`s via a shared
262285 /// atomic counter (safe for Serial single-worker execution). Each block's
263- /// `bytes` is `COALESCE_INPUT_BLOCK_BYTES` of a deterministic fill so the
264- /// sink can verify byte preservation without an ordering assumption .
286+ /// `bytes` is a per-block-unique `block_fill(batch_serial)` so the sink can
287+ /// reconstruct and verify the exact concatenated byte stream .
265288 #[ derive( Clone ) ]
266289 struct BlockSource {
267290 remaining : Arc < AtomicU64 > ,
@@ -283,10 +306,7 @@ mod tests {
283306 if n == 0 {
284307 return Ok ( StepOutcome :: Finished ) ;
285308 }
286- let block = DecompressedBlock {
287- batch_serial : n,
288- bytes : vec ! [ 0xCD ; COALESCE_INPUT_BLOCK_BYTES ] ,
289- } ;
309+ let block = DecompressedBlock { batch_serial : n, bytes : block_fill ( n) } ;
290310 match ctx. outputs . push ( block) {
291311 Ok ( ( ) ) => {
292312 self . remaining . fetch_sub ( 1 , AtomicOrd :: AcqRel ) ;
@@ -302,10 +322,12 @@ mod tests {
302322 }
303323
304324 /// Sink recording the byte length of every emitted block (so the test can
305- /// bound the per-block size) plus the running total.
325+ /// bound the per-block size) plus the exact concatenated byte stream (so the
326+ /// test can compare it against an independently-built expected stream).
306327 #[ derive( Clone ) ]
307328 struct SizeRecordingSink {
308329 sizes : Arc < Mutex < Vec < usize > > > ,
330+ stream : Arc < Mutex < Vec < u8 > > > ,
309331 }
310332 impl Step for SizeRecordingSink {
311333 type Input = DecompressedBlock ;
@@ -322,13 +344,12 @@ mod tests {
322344 fn try_run ( & mut self , ctx : & mut StepCtx < ' _ , Self > ) -> io:: Result < StepOutcome > {
323345 match ctx. input . pop ( ) {
324346 Some ( block) => {
325- // Every byte must be the source's fill — proving the
326- // concatenation neither drops nor corrupts bytes.
327- assert ! (
328- block. bytes. iter( ) . all( |& b| b == 0xCD ) ,
329- "coalesced block carries unexpected bytes"
330- ) ;
347+ // Record the per-block size for the memory bound, and append
348+ // the bytes verbatim so the test can compare the full stream
349+ // against the expected concatenation (catches reorder /
350+ // drop / duplicate, not just total-byte conservation).
331351 self . sizes . lock ( ) . expect ( "sink mutex" ) . push ( block. bytes . len ( ) ) ;
352+ self . stream . lock ( ) . expect ( "stream mutex" ) . extend_from_slice ( & block. bytes ) ;
332353 Ok ( StepOutcome :: Progress )
333354 }
334355 None if ctx. input . is_drained ( ) => Ok ( StepOutcome :: Finished ) ,
@@ -342,16 +363,18 @@ mod tests {
342363
343364 #[ test]
344365 fn coalesce_flushes_at_threshold_and_preserves_bytes ( ) {
345- let remaining = Arc :: new ( AtomicU64 :: new ( u64:: try_from ( COALESCE_INPUT_BLOCKS ) . unwrap ( ) ) ) ;
366+ let n_blocks = u64:: try_from ( COALESCE_INPUT_BLOCKS ) . unwrap ( ) ;
367+ let remaining = Arc :: new ( AtomicU64 :: new ( n_blocks) ) ;
346368 let sizes = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
369+ let stream = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
347370
348371 let coalesce = CoalesceBytes :: new ( COALESCE_THRESHOLD_BYTES , 4 * 1024 ) ;
349372
350373 let builder = PipelineBuilder :: new ( ) ;
351374 builder
352375 . chain ( BlockSource { remaining : Arc :: clone ( & remaining) } )
353376 . chain ( coalesce)
354- . chain ( SizeRecordingSink { sizes : Arc :: clone ( & sizes) } )
377+ . chain ( SizeRecordingSink { sizes : Arc :: clone ( & sizes) , stream : Arc :: clone ( & stream ) } )
355378 . into_sink_marker ( ) ;
356379
357380 let pipeline = builder. build ( ) . unwrap ( ) ;
@@ -365,6 +388,26 @@ mod tests {
365388 // Byte conservation: every input byte reaches the sink exactly once.
366389 assert_eq ! ( total_out, total_in, "coalesce dropped or duplicated bytes" ) ;
367390
391+ // Byte-stream identity: independently build the expected concatenation in
392+ // the source's emission order (`BlockSource` counts the shared atomic
393+ // DOWN from `n_blocks` to 1, so it emits `batch_serial = N, N-1, …, 1`)
394+ // and require the sink's flat byte stream to match it exactly. Because
395+ // each block's fill is per-block-unique (`block_fill`), this fails if any
396+ // block is reordered, dropped, or duplicated — a far stronger oracle than
397+ // the aggregate-count check above.
398+ let expected_stream: Vec < u8 > = ( 1 ..=n_blocks) . rev ( ) . flat_map ( block_fill) . collect ( ) ;
399+ let actual_stream = stream. lock ( ) . expect ( "stream mutex" ) . clone ( ) ;
400+ assert_eq ! (
401+ actual_stream. len( ) ,
402+ expected_stream. len( ) ,
403+ "coalesced stream length differs from expected"
404+ ) ;
405+ assert ! (
406+ actual_stream == expected_stream,
407+ "coalesced byte stream diverges from the expected in-order concatenation \
408+ (reorder/drop/duplicate)"
409+ ) ;
410+
368411 // Memory bound: the step flushes once `pending` reaches the threshold,
369412 // then resets. Each emitted block therefore carries between
370413 // `threshold` and `threshold + (MAX_BATCHES_PER_LOCK - 1) * input` —
0 commit comments