@@ -558,13 +558,14 @@ func TestRunStagesPoolResize(t *testing.T) {
558558}
559559
560560func TestRunStagesUntilStopsBeforeNextStage (t * testing.T ) {
561- for _ , mode := range []loadgen.Mode {loadgen .ModeConcurrent , loadgen .ModeConversationPool } {
561+ for _ , mode := range []loadgen.Mode {loadgen .ModeConcurrent , loadgen .ModeConversationPool , loadgen . ModeConstant , loadgen . ModePoisson } {
562562 t .Run (string (mode ), func (t * testing.T ) {
563563 addr := startMockServer (t )
564564 gen := & loadgen.Generator {
565565 Target : "http://" + addr + "/v1" ,
566566 Model : "test-model" ,
567567 Mode : mode ,
568+ Rate : 100 ,
568569 Dataset : dataset .NewSynthetic (32 , 10 , 1 , 4.0 ),
569570 Recorder : recorder .NewMemory (),
570571 }
@@ -586,6 +587,128 @@ func TestRunStagesUntilStopsBeforeNextStage(t *testing.T) {
586587 }
587588}
588589
590+ func TestRateStagesLimitsDrainAndUniqueIDs (t * testing.T ) {
591+ for _ , mode := range []loadgen.Mode {loadgen .ModeConstant , loadgen .ModePoisson } {
592+ t .Run (string (mode ), func (t * testing.T ) {
593+ addr := startMockServer (t )
594+ rec := recorder .NewMemory ()
595+ defer rec .Close ()
596+ gen := & loadgen.Generator {
597+ Target : "http://" + addr + "/v1" , Model : "test-model" ,
598+ Mode : mode , Rate : 1000 , MaxInFlight : 1 ,
599+ // Each stage's zero rampup must override this generator default.
600+ Rampup : time .Hour ,
601+ Dataset : dataset .NewSynthetic (32 , 10 , 2 , 4.0 ), Recorder : rec ,
602+ }
603+ stages := []loadgen.Stage {
604+ {Duration : 5 * time .Second , MaxRequests : 3 },
605+ {Barrier : true , BarrierDrain : true },
606+ {Duration : 5 * time .Second , MaxRequests : 4 },
607+ }
608+ var started , completed , barriers []int
609+ gen .RunStagesUntil (context .Background (), stages , func (i , _ int ) {
610+ started = append (started , i )
611+ }, func (i int ) {
612+ barriers = append (barriers , i )
613+ }, func (i int ) bool {
614+ completed = append (completed , i )
615+ if gen .InFlight () != 0 {
616+ t .Error ("rate stage did not drain before completion callback" )
617+ }
618+ return true
619+ })
620+ if ! reflect .DeepEqual (started , []int {0 , 2 }) || ! reflect .DeepEqual (completed , []int {0 , 2 }) || ! reflect .DeepEqual (barriers , []int {1 }) {
621+ t .Fatalf ("started=%v completed=%v barriers=%v" , started , completed , barriers )
622+ }
623+ rec .Close ()
624+ records := rec .Records ()
625+ // Like concurrent mode, the arrival limit counts conversations;
626+ // every admitted conversation must finish all of its turns.
627+ if len (records ) != 14 {
628+ t .Fatalf ("got %d records, want 14 from seven two-turn conversations" , len (records ))
629+ }
630+ conversations := make (map [string ]int )
631+ requests := make (map [string ]bool )
632+ for _ , r := range records {
633+ if r .Status != "ok" {
634+ t .Errorf ("request %s failed: %s" , r .RequestID , r .Error )
635+ }
636+ if requests [r .RequestID ] {
637+ t .Errorf ("duplicate request ID %s" , r .RequestID )
638+ }
639+ requests [r .RequestID ] = true
640+ conversations [r .ConversationID ]++
641+ }
642+ if len (conversations ) != 7 {
643+ t .Fatalf ("got %d conversations, want 7" , len (conversations ))
644+ }
645+ for id , turns := range conversations {
646+ if turns != 2 {
647+ t .Errorf ("conversation %s has %d turns, want 2" , id , turns )
648+ }
649+ }
650+ })
651+ }
652+ }
653+
654+ func TestRateStageRampup (t * testing.T ) {
655+ addr := startMockServer (t )
656+ rec := recorder .NewMemory ()
657+ defer rec .Close ()
658+ gen := & loadgen.Generator {
659+ Target : "http://" + addr + "/v1" , Model : "test-model" ,
660+ Mode : loadgen .ModeConstant , Rate : 100 ,
661+ Dataset : dataset .NewSynthetic (32 , 10 , 1 , 4.0 ), Recorder : rec ,
662+ }
663+ var starts []time.Time
664+ gen .RunStages (context .Background (), []loadgen.Stage {
665+ {Duration : time .Second , Rampup : 500 * time .Millisecond , MaxRequests : 1 },
666+ {Duration : time .Second , Rampup : 500 * time .Millisecond , MaxRequests : 1 },
667+ }, func (_ , _ int ) { starts = append (starts , time .Now ()) }, nil )
668+ rec .Close ()
669+ records := rec .Records ()
670+ if len (records ) != 2 {
671+ t .Fatalf ("got %d records, want one per stage" , len (records ))
672+ }
673+ sort .Slice (records , func (i , j int ) bool { return records [i ].StartTime < records [j ].StartTime })
674+ for i , r := range records {
675+ delay := r .StartTime - recorder .TimeToFloat (starts [i ])
676+ // Integrated rate gives a first arrival at 100ms; full rate gives 10ms.
677+ if delay < 0.075 {
678+ t .Errorf ("stage %d first arrival after %.3fs, rampup ignored" , i , delay )
679+ }
680+ }
681+ }
682+
683+ func TestRateStageDuration (t * testing.T ) {
684+ for _ , mode := range []loadgen.Mode {loadgen .ModeConstant , loadgen .ModePoisson } {
685+ for _ , limit := range []int {0 , 10000 } {
686+ t .Run (fmt .Sprintf ("%s/limit=%d" , mode , limit ), func (t * testing.T ) {
687+ addr := startMockServer (t )
688+ rec := recorder .NewMemory ()
689+ defer rec .Close ()
690+ gen := & loadgen.Generator {
691+ Target : "http://" + addr + "/v1" , Model : "test-model" ,
692+ Mode : mode , Rate : 100 ,
693+ Dataset : dataset .NewSynthetic (32 , 10 , 1 , 4.0 ), Recorder : rec ,
694+ }
695+ start := time .Now ()
696+ gen .RunStages (context .Background (), []loadgen.Stage {{
697+ Duration : 200 * time .Millisecond , MaxRequests : limit ,
698+ }}, nil , nil )
699+ elapsed := time .Since (start )
700+ rec .Close ()
701+ if len (rec .Records ()) == 0 {
702+ t .Fatal ("expected rate arrivals before the stage deadline" )
703+ }
704+ if elapsed < 180 * time .Millisecond || elapsed > 2 * time .Second {
705+ t .Errorf ("stage lasted %v, want approximately 200ms" , elapsed )
706+ }
707+ })
708+ }
709+ }
710+ }
711+
589712func TestCacheSaltAppearsInRequest (t * testing.T ) {
590713 // Start a tiny HTTP server that captures the request body
591714 var bodies []string
0 commit comments