55class SuiteBinPackingTest < Minitest ::Test
66 def setup
77 @config = CI ::Queue ::Configuration . new (
8- suite_max_duration : 120_000 ,
8+ minimum_max_chunk_duration : 120_000 ,
9+ maximum_max_chunk_duration : 300_000 ,
910 suite_buffer_percent : 10 ,
1011 timing_fallback_duration : 100.0
1112 )
@@ -67,7 +68,8 @@ def test_splits_suite_when_over_max_duration
6768 end
6869
6970 def test_applies_buffer_when_splitting
70- @config . suite_max_duration = 100_000
71+ @config . minimum_max_chunk_duration = 100_000
72+ @config . maximum_max_chunk_duration = 100_000
7173 @config . suite_buffer_percent = 10
7274
7375 tests = create_mock_tests ( [ 'TestSuite#test_1' , 'TestSuite#test_2' ] )
@@ -186,7 +188,8 @@ def test_chunk_id_format
186188 end
187189
188190 def test_suite_exactly_at_max_duration_gets_split
189- @config . suite_max_duration = 100_000
191+ @config . minimum_max_chunk_duration = 100_000
192+ @config . maximum_max_chunk_duration = 100_000
190193 @config . suite_buffer_percent = 10
191194
192195 tests = create_mock_tests ( [ 'TestSuite#test_1' , 'TestSuite#test_2' ] )
@@ -204,7 +207,8 @@ def test_suite_exactly_at_max_duration_gets_split
204207 end
205208
206209 def test_suite_exactly_at_effective_max_fits_in_one_chunk
207- @config . suite_max_duration = 100_000
210+ @config . minimum_max_chunk_duration = 100_000
211+ @config . maximum_max_chunk_duration = 100_000
208212 @config . suite_buffer_percent = 10
209213
210214 tests = create_mock_tests ( [ 'TestSuite#test_1' , 'TestSuite#test_2' ] )
@@ -282,7 +286,8 @@ def test_test_count_is_set_correctly
282286 end
283287
284288 def test_large_suite_creates_multiple_chunks
285- @config . suite_max_duration = 100_000
289+ @config . minimum_max_chunk_duration = 100_000
290+ @config . maximum_max_chunk_duration = 100_000
286291 @config . suite_buffer_percent = 10
287292
288293 # Create a suite that will need many chunks
@@ -462,7 +467,7 @@ def test_dynamic_max_duration_calculation
462467 # Total duration = 40,000ms
463468 # Parallel jobs = 4
464469 # Calculated base_max_duration = 40,000 / 4 = 10,000ms
465- # However, 10,000ms < configured @max_duration (120,000ms), so floor logic applies
470+ # However, 10,000ms < configured minimum_max_chunk_duration (120,000ms), so floor logic applies
466471 # Actual max_duration used = max(10,000, 120,000) = 120,000ms
467472 # With 10% buffer, effective_max = 120,000 * 0.9 = 108,000ms
468473 # All 4 tests fit in one chunk: 4 * 10,000 = 40,000ms < 108,000ms
@@ -482,7 +487,7 @@ def test_dynamic_max_duration_falls_back_to_configured_when_no_env_var
482487
483488 chunks = order_with_timing ( tests , timing_data )
484489
485- # Should use configured max_duration (120,000ms default)
490+ # Should use configured minimum_max_chunk_duration (120,000ms default)
486491 # So test should fit in one chunk
487492 assert_equal 1 , chunks . size
488493 end
@@ -497,7 +502,7 @@ def test_dynamic_max_duration_uses_floor_when_calculated_value_too_small
497502 chunks = order_with_timing ( tests , timing_data )
498503
499504 # Calculated max = 1000 / 100 = 10ms (too small)
500- # Should use configured max_duration (120,000ms) as floor
505+ # Should use configured minimum_max_chunk_duration (120,000ms) as floor
501506 # So test should fit in one chunk
502507 assert_equal 1 , chunks . size
503508 ensure
@@ -518,7 +523,7 @@ def test_dynamic_max_duration_with_large_parallelism
518523 # Total duration = 20 * 5000 = 100,000ms
519524 # Parallel jobs = 10
520525 # Calculated base_max_duration = 100,000 / 10 = 10,000ms
521- # However, 10,000ms < configured @max_duration (120,000ms), so floor logic applies
526+ # However, 10,000ms < configured minimum_max_chunk_duration (120,000ms), so floor logic applies
522527 # Actual max_duration used = max(10,000, 120,000) = 120,000ms
523528 # With 10% buffer, effective_max = 120,000 * 0.9 = 108,000ms
524529 # All 20 tests fit in one chunk: 20 * 5,000 = 100,000ms < 108,000ms
@@ -530,7 +535,7 @@ def test_dynamic_max_duration_with_large_parallelism
530535 end
531536
532537 def test_dynamic_max_duration_exceeds_default_max_causes_splits
533- # Set up scenario where computed chunk size > default max_duration
538+ # Set up scenario where computed chunk size > default minimum_max_chunk_duration
534539 ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '2'
535540
536541 # Create tests with large total duration
@@ -544,7 +549,7 @@ def test_dynamic_max_duration_exceeds_default_max_causes_splits
544549 # Total duration = 10 * 30,000 = 300,000ms
545550 # Parallel jobs = 2
546551 # Calculated base_max_duration = 300,000 / 2 = 150,000ms
547- # 150,000ms > configured @max_duration (120,000ms), so use calculated value
552+ # 150,000ms > configured minimum_max_chunk_duration (120,000ms), so use calculated value
548553 # Actual max_duration used = max(150,000, 120,000) = 150,000ms
549554 # With 10% buffer, effective_max = 150,000 * 0.9 = 135,000ms
550555 # Each test is 30,000ms, so we can fit 4 per chunk (4 * 30,000 = 120,000 < 135,000)
@@ -560,6 +565,169 @@ def test_dynamic_max_duration_exceeds_default_max_causes_splits
560565 ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
561566 end
562567
568+ def test_dynamic_max_duration_capped_at_maximum
569+ # Set up scenario where computed chunk size would exceed maximum_max_chunk_duration
570+ ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '1'
571+
572+ # Create tests with very large total duration
573+ tests = create_mock_tests ( ( 1 ..10 ) . map { |i | "TestSuite#test_#{ i } " } )
574+ timing_data = ( 1 ..10 ) . each_with_object ( { } ) do |i , hash |
575+ hash [ "TestSuite#test_#{ i } " ] = 80_000.0 # Each test is 80 seconds
576+ end
577+
578+ chunks = order_with_timing ( tests , timing_data )
579+
580+ # Total duration = 10 * 80,000 = 800,000ms
581+ # Parallel jobs = 1
582+ # Calculated base_max_duration = 800,000 / 1 = 800,000ms
583+ # 800,000ms > configured maximum_max_chunk_duration (300,000ms), so cap it
584+ # Actual max_duration used = min(800,000, 300,000) = 300,000ms
585+ # With 10% buffer, effective_max = 300,000 * 0.9 = 270,000ms
586+ # Each test is 80,000ms, so we can fit 3 per chunk (3 * 80,000 = 240,000 < 270,000)
587+ # With 10 tests, we'll get 4 chunks (3+3+3+1)
588+ test_suite_chunks = chunks . select { |c | c . suite_name == 'TestSuite' }
589+ assert_equal 4 , test_suite_chunks . size , 'Should cap at maximum_max_chunk_duration'
590+
591+ # Verify chunks respect the capped max_duration
592+ test_suite_chunks [ 0 ..2 ] . each do |chunk |
593+ assert_equal 3 , chunk . test_ids . size , 'First 3 chunks should have 3 tests each'
594+ assert_equal 240_000.0 , chunk . estimated_duration
595+ end
596+ assert_equal 1 , test_suite_chunks [ 3 ] . test_ids . size , 'Last chunk should have 1 test'
597+ assert_equal 80_000.0 , test_suite_chunks [ 3 ] . estimated_duration
598+ ensure
599+ ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
600+ end
601+
602+ def test_dynamic_max_duration_within_range
603+ # Set up scenario where computed chunk size falls within the configured range
604+ ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '5'
605+
606+ tests = create_mock_tests ( ( 1 ..10 ) . map { |i | "TestSuite#test_#{ i } " } )
607+ timing_data = ( 1 ..10 ) . each_with_object ( { } ) do |i , hash |
608+ hash [ "TestSuite#test_#{ i } " ] = 20_000.0 # Each test is 20 seconds
609+ end
610+
611+ chunks = order_with_timing ( tests , timing_data )
612+
613+ # Total duration = 10 * 20,000 = 200,000ms
614+ # Parallel jobs = 5
615+ # Calculated base_max_duration = 200,000 / 5 = 40,000ms
616+ # 40,000ms < minimum_max_chunk_duration (120,000ms), so use minimum
617+ # Actual max_duration used = max(40,000, 120,000) = 120,000ms
618+ # With 10% buffer, effective_max = 120,000 * 0.9 = 108,000ms
619+ # Each test is 20,000ms, so we can fit 5 per chunk (5 * 20,000 = 100,000 < 108,000)
620+ # With 10 tests, we'll get 2 chunks (5+5)
621+ test_suite_chunks = chunks . select { |c | c . suite_name == 'TestSuite' }
622+ assert_equal 2 , test_suite_chunks . size , 'Should create 2 chunks with minimum floor applied'
623+
624+ test_suite_chunks . each do |chunk |
625+ assert_equal 5 , chunk . test_ids . size , 'Each chunk should have 5 tests'
626+ assert_equal 100_000.0 , chunk . estimated_duration
627+ end
628+ ensure
629+ ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
630+ end
631+
632+ def test_range_based_max_duration_with_custom_values
633+ # Test with custom minimum and maximum values
634+ @config . minimum_max_chunk_duration = 60_000
635+ @config . maximum_max_chunk_duration = 180_000
636+
637+ ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '2'
638+
639+ tests = create_mock_tests ( ( 1 ..10 ) . map { |i | "TestSuite#test_#{ i } " } )
640+ timing_data = ( 1 ..10 ) . each_with_object ( { } ) do |i , hash |
641+ hash [ "TestSuite#test_#{ i } " ] = 20_000.0 # Each test is 20 seconds
642+ end
643+
644+ chunks = order_with_timing ( tests , timing_data )
645+
646+ # Total duration = 10 * 20,000 = 200,000ms
647+ # Parallel jobs = 2
648+ # Calculated base_max_duration = 200,000 / 2 = 100,000ms
649+ # 100,000ms is within range [60,000, 180,000], so use it
650+ # With 10% buffer, effective_max = 100,000 * 0.9 = 90,000ms
651+ # Each test is 20,000ms, so we can fit 4 per chunk (4 * 20,000 = 80,000 < 90,000)
652+ # With 10 tests, we'll get 3 chunks (4+4+2)
653+ test_suite_chunks = chunks . select { |c | c . suite_name == 'TestSuite' }
654+ assert_equal 3 , test_suite_chunks . size , 'Should create 3 chunks with calculated max_duration'
655+
656+ assert_equal 4 , test_suite_chunks [ 0 ] . test_ids . size , 'First chunk should have 4 tests'
657+ assert_equal 80_000.0 , test_suite_chunks [ 0 ] . estimated_duration
658+ assert_equal 4 , test_suite_chunks [ 1 ] . test_ids . size , 'Second chunk should have 4 tests'
659+ assert_equal 80_000.0 , test_suite_chunks [ 1 ] . estimated_duration
660+ assert_equal 2 , test_suite_chunks [ 2 ] . test_ids . size , 'Third chunk should have 2 tests'
661+ assert_equal 40_000.0 , test_suite_chunks [ 2 ] . estimated_duration
662+ ensure
663+ ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
664+ end
665+
666+ def test_maximum_max_duration_prevents_oversized_chunks
667+ # Test that maximum prevents chunks from being too large
668+ @config . minimum_max_chunk_duration = 50_000
669+ @config . maximum_max_chunk_duration = 100_000
670+
671+ ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '1'
672+
673+ tests = create_mock_tests ( ( 1 ..5 ) . map { |i | "TestSuite#test_#{ i } " } )
674+ timing_data = ( 1 ..5 ) . each_with_object ( { } ) do |i , hash |
675+ hash [ "TestSuite#test_#{ i } " ] = 40_000.0 # Each test is 40 seconds
676+ end
677+
678+ chunks = order_with_timing ( tests , timing_data )
679+
680+ # Total duration = 5 * 40,000 = 200,000ms
681+ # Parallel jobs = 1
682+ # Calculated base_max_duration = 200,000 / 1 = 200,000ms
683+ # 200,000ms > maximum_max_chunk_duration (100,000ms), so cap it
684+ # Actual max_duration used = min(200,000, 100,000) = 100,000ms
685+ # With 10% buffer, effective_max = 100,000 * 0.9 = 90,000ms
686+ # Each test is 40,000ms, so we can fit 2 per chunk (2 * 40,000 = 80,000 < 90,000)
687+ # With 5 tests, we'll get 3 chunks (2+2+1)
688+ test_suite_chunks = chunks . select { |c | c . suite_name == 'TestSuite' }
689+ assert_equal 3 , test_suite_chunks . size , 'Should cap at maximum and create smaller chunks'
690+
691+ assert_equal 2 , test_suite_chunks [ 0 ] . test_ids . size
692+ assert_equal 2 , test_suite_chunks [ 1 ] . test_ids . size
693+ assert_equal 1 , test_suite_chunks [ 2 ] . test_ids . size
694+ ensure
695+ ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
696+ end
697+
698+ def test_minimum_max_duration_prevents_undersized_chunks
699+ # Test that minimum prevents chunks from being too small
700+ @config . minimum_max_chunk_duration = 200_000
701+ @config . maximum_max_chunk_duration = 500_000
702+
703+ ENV [ 'BUILDKITE_PARALLEL_JOB_COUNT' ] = '20'
704+
705+ tests = create_mock_tests ( ( 1 ..10 ) . map { |i | "TestSuite#test_#{ i } " } )
706+ timing_data = ( 1 ..10 ) . each_with_object ( { } ) do |i , hash |
707+ hash [ "TestSuite#test_#{ i } " ] = 20_000.0 # Each test is 20 seconds
708+ end
709+
710+ chunks = order_with_timing ( tests , timing_data )
711+
712+ # Total duration = 10 * 20,000 = 200,000ms
713+ # Parallel jobs = 20
714+ # Calculated base_max_duration = 200,000 / 20 = 10,000ms
715+ # 10,000ms < minimum_max_chunk_duration (200,000ms), so use minimum
716+ # Actual max_duration used = max(10,000, 200,000) = 200,000ms
717+ # With 10% buffer, effective_max = 200,000 * 0.9 = 180,000ms
718+ # Each test is 20,000ms, so we can fit 9 per chunk (9 * 20,000 = 180,000 = 180,000)
719+ # With 10 tests, we'll get 2 chunks (9+1)
720+ test_suite_chunks = chunks . select { |c | c . suite_name == 'TestSuite' }
721+ assert_equal 2 , test_suite_chunks . size , 'Should use minimum and create larger chunks'
722+
723+ assert_equal 9 , test_suite_chunks [ 0 ] . test_ids . size , 'First chunk should have 9 tests'
724+ assert_equal 180_000.0 , test_suite_chunks [ 0 ] . estimated_duration
725+ assert_equal 1 , test_suite_chunks [ 1 ] . test_ids . size , 'Second chunk should have 1 test'
726+ assert_equal 20_000.0 , test_suite_chunks [ 1 ] . estimated_duration
727+ ensure
728+ ENV . delete ( 'BUILDKITE_PARALLEL_JOB_COUNT' )
729+ end
730+
563731 private
564732
565733 def create_mock_tests ( test_ids )
0 commit comments