@@ -369,6 +369,66 @@ 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 , align : 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, with middle spans evenly distributed in between.
378+ Positions are computed by evenly dividing total_size across num_spans,
379+ then rounded down to the nearest multiple of align. Bytes that cannot be
380+ written due to alignment rounding are carried over to subsequent spans,
381+ so that the full device can be covered when total_size equals dev_size.
382+
383+ Args:
384+ dev_size: Total device size in bytes
385+ total_size: Total number of bytes to write across all spans.
386+ Use dev_size for full device coverage, or a smaller value
387+ to leave gaps between spans.
388+ num_spans: Number of spans to create
389+ align: Block size in bytes to align span positions to.
390+ All span positions are rounded down to the nearest multiple of align.
391+ Span sizes are reduced if necessary to avoid overlapping the next
392+ span's position, with unwritten bytes carried forward to later spans.
393+ When total_size equals dev_size, spans are contiguous and cover
394+ the full device with no gaps.
395+
396+ Returns:
397+ List of (position, size) tuples, one per span.
398+ Spans are guaranteed to:
399+ - Not overlap
400+ - Have first span at position 0
401+ - Have last span ending at dev_size
402+ - Be evenly distributed across device
403+ """
404+ # Positions are computed by evenly dividing total_size, then aligned down.
405+ # Using total_size * i // num_spans avoids losing the remainder that would
406+ # occur if we divided total_size by num_spans first.
407+ positions = [(total_size * i // num_spans // align ) * align for i in range (num_spans )]
408+
409+ # Compute sizes with carry: bytes trimmed due to alignment are carried
410+ # forward so later spans can make up the difference
411+ result : list [tuple [int , int ]] = []
412+ carry = 0
413+ per_span = total_size // num_spans
414+ for i , position in enumerate (positions ):
415+ if i == len (positions ) - 1 :
416+ size = dev_size - position
417+ else :
418+ budget = per_span + carry
419+ size = min (budget , positions [i + 1 ] - position )
420+ carry = budget - size
421+ assert position + size <= dev_size , \
422+ f"Span { i } at position={ position } size={ size } extends past dev_size={ dev_size } "
423+ if i > 0 :
424+ prev_pos , prev_size = result [- 1 ]
425+ assert position >= prev_pos + prev_size , \
426+ f"Span { i } at position { position } overlaps with previous span ending at { prev_pos + prev_size } "
427+ result .append ((position , size ))
428+
429+ return result
430+
431+
372432def partially_populate_device (vm : VM , dev_path : str , dev_size : int , num_spans : int = 3 , skip_spans : list [int ] = []) \
373433 -> list [StreamSpan ]:
374434 """
@@ -402,55 +462,25 @@ def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: i
402462 skip_spans: List of span indices to skip (no data generated).
403463 Skipped spans still exist in returned list with checksum=None.
404464 (default: [])
465+ Span alignment is controlled by the --write-volume-align pytest option.
405466
406467 Returns:
407468 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
413469 """
414470 logging .info (f"Generate { dev_path } content" )
415- stream_size = min (dev_size , config .write_volume_cap ) // num_spans
471+ total_size = min (dev_size , config .write_volume_cap )
416472
417473 # Validate skip_spans
418474 assert all (0 <= i < num_spans for i in skip_spans ), \
419475 f"Invalid span index in skip_spans: must be 0 <= i < { num_spans } "
420476
477+ layout = compute_span_layout (dev_size , total_size , num_spans , config .write_volume_align )
421478 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 )
479+ for i , (position , size ) in enumerate (layout ):
480+ span = StreamSpan (position = position , size = size )
446481 if i not in skip_spans :
447482 span .generate (vm , dev_path , seed = 1000 + i )
448483 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 } "
454484
455485 return spans
456486
0 commit comments