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