@@ -369,6 +369,64 @@ def validate(self, vm: VM, dev: str) -> None:
369369 expected_flags = f'--expected-checksum { self .checksum } ' if self .checksum is not None else ''
370370 randstream (vm , f'validate { expected_flags } --position { self .position } --size { self .size } { dev } ' )
371371
372+ def compute_span_layout (dev_size : int , total_size : int , num_spans : int , block_size : int ) -> list [tuple [int , int ]]:
373+ """
374+ Compute the positions and sizes of spans across a device.
375+
376+ Spans are distributed such that the first span starts at 0 and the last
377+ span ends at dev_size. Positions are computed right-to-left: each span's
378+ start is rounded UP to the nearest multiple of align so the span sits
379+ entirely within its slot. Bytes skipped by rounding are carried leftward
380+ to the previous span, which absorbs them in its budget.
381+
382+ When total_size equals dev_size, spans are contiguous and cover the full
383+ device with no gaps. When total_size is less than dev_size, spans are
384+ evenly spread across the device with gaps between them.
385+
386+ Args:
387+ dev_size: Total device size in bytes
388+ total_size: Total number of bytes to write across all spans.
389+ Use dev_size for full device coverage, or a smaller value
390+ to leave gaps between spans.
391+ num_spans: Number of spans to create
392+ block_size: Block size in bytes to align span starts to.
393+ All span starts are rounded up to the nearest multiple of align.
394+ Bytes skipped by the rounding are carried to the previous span.
395+
396+ Returns:
397+ List of (position, size) tuples, one per span.
398+ Spans are guaranteed to:
399+ - Not overlap
400+ - Have first span starting at position 0
401+ - Have last span ending at dev_size
402+ - Sum of span sizes equals total_size
403+ """
404+ per_span = total_size // num_spans
405+ result : list [tuple [int , int ]] = []
406+ # Seed the remainder into the last span so it is distributed via carry
407+ carry = total_size % num_spans
408+ end = dev_size
409+ for i in range (num_spans - 1 , - 1 , - 1 ):
410+ budget = per_span + carry
411+ if i == 0 :
412+ # First span always starts at 0; any leftover space before the
413+ # next span becomes a gap (partial coverage only)
414+ start = 0
415+ size = min (budget , end )
416+ else :
417+ # Round start UP: this may shrink the span relative to budget;
418+ # the trimmed bytes are carried left to the previous span
419+ ideal_start = end - budget
420+ start = ((ideal_start + block_size - 1 ) // block_size ) * block_size
421+ size = end - start
422+ carry = budget - size
423+ result .append ((start , size ))
424+ end = start
425+ result .reverse ()
426+ assert sum (s for _ , s in result ) == total_size
427+ return result
428+
429+
372430def partially_populate_device (vm : VM , dev_path : str , dev_size : int , num_spans : int = 3 , skip_spans : list [int ] = []) \
373431 -> list [StreamSpan ]:
374432 """
@@ -402,55 +460,25 @@ def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: i
402460 skip_spans: List of span indices to skip (no data generated).
403461 Skipped spans still exist in returned list with checksum=None.
404462 (default: [])
463+ Span alignment is controlled by the --write-volume-align pytest option.
405464
406465 Returns:
407466 List of StreamSpan objects representing the generated spans.
408- Spans are guaranteed to:
409- - Not overlap
410- - Have first span at position 0
411- - Have last span ending at dev_size
412- - Be evenly distributed across device
413467 """
414468 logging .info (f"Generate { dev_path } content" )
415- stream_size = min (dev_size , config .write_volume_cap ) // num_spans
469+ total_size = min (dev_size , config .write_volume_cap )
416470
417471 # Validate skip_spans
418472 assert all (0 <= i < num_spans for i in skip_spans ), \
419473 f"Invalid span index in skip_spans: must be 0 <= i < { num_spans } "
420474
475+ layout = compute_span_layout (dev_size , total_size , num_spans , config .write_volume_align )
421476 spans : list [StreamSpan ] = []
422-
423- # Calculate positions for regularly distributed spans
424- # First span always at position 0, last span always ends at dev_size
425- if num_spans == 1 :
426- positions = [0 ]
427- elif num_spans == 2 :
428- positions = [0 , dev_size - stream_size ]
429- else :
430- # For 3+ spans: distribute evenly across available space
431- # Available space is dev_size - size (last span can't extend past dev_size)
432- available_space = dev_size - stream_size
433- positions = [0 ] # First span always at 0
434- # Distribute middle spans evenly
435- for i in range (1 , num_spans - 1 ):
436- position = (available_space * i ) // (num_spans - 1 )
437- positions .append (position )
438- positions .append (dev_size - stream_size ) # Last span always ends at dev_size
439-
440- # Generate spans with consistency checks
441- prev_end = - 1
442- for i , position in enumerate (positions ):
443- # Assert no overlap
444- assert position >= prev_end , f"Span { i } at position { position } overlaps with previous span ending at { prev_end } "
445- span = StreamSpan (position = position , size = stream_size )
477+ for i , (position , size ) in enumerate (layout ):
478+ span = StreamSpan (position = position , size = size )
446479 if i not in skip_spans :
447480 span .generate (vm , dev_path , seed = 1000 + i )
448481 spans .append (span )
449- prev_end = position + stream_size
450-
451- # Final assert: last span must not extend past dev_size
452- assert spans [- 1 ].position + spans [- 1 ].size <= dev_size , \
453- f"Last span extends past device: position={ spans [- 1 ].position } , size={ spans [- 1 ].size } , dev_size={ dev_size } "
454482
455483 return spans
456484
0 commit comments