package runner import ( "fmt" "sync" "time" "cast-loadtest/internal/config" "cast-loadtest/internal/sender" "cast-loadtest/internal/stats" ) func Run(cfg config.Config) { fmt.Println("========================================") fmt.Println(" SMS API LOAD TESTER (Go) ") fmt.Println("========================================") fmt.Println() fmt.Println("--- API Configuration ---") fmt.Printf(" Target: %s\n", cfg.APIURL) fmt.Printf(" API Key: %s...%s\n", cfg.APIKey[:9], cfg.APIKey[len(cfg.APIKey)-4:]) fmt.Printf(" Sender ID: %s\n", cfg.SenderID) fmt.Printf(" Recipient: %s\n", cfg.Recipient) fmt.Println() fmt.Println("--- Message Configuration ---") fmt.Printf(" SMS Parts: %d\n", cfg.MsgParts) fmt.Printf(" Max Chars: %d\n", cfg.MsgParts*140) fmt.Println() fmt.Println("--- Test Parameters ---") fmt.Printf(" Threads: %d\n", cfg.Threads) fmt.Printf(" Target RPS: %d\n", cfg.RPS) fmt.Printf(" Duration: %ds\n", cfg.DurationSec) fmt.Printf(" Ramp-up: %ds\n", cfg.RampUpSec) fmt.Println() // Warnings hasWarning := false if cfg.RampUpSec >= cfg.DurationSec { fmt.Println("WARNING: Ramp-up >= duration. The test will never reach full RPS.") fmt.Printf(" Reduce LOAD_TEST_RAMP_UP_SECS (currently %ds) below duration (%ds).\n", cfg.RampUpSec, cfg.DurationSec) hasWarning = true } // Estimate: need threads >= RPS * avg_latency_secs. Assume ~500ms avg latency. estimatedLatency := 0.5 minThreads := int(float64(cfg.RPS)*estimatedLatency) + 1 if cfg.Threads < minThreads { fmt.Printf("WARNING: Threads (%d) may be too low for target RPS (%d).\n", cfg.Threads, cfg.RPS) fmt.Printf(" Recommended: at least %d threads (based on ~%.0fms estimated latency).\n", minThreads, estimatedLatency*1000) hasWarning = true } if hasWarning { fmt.Println() } fmt.Println("========================================") fmt.Println() client := sender.NewHTTPClient(cfg.Threads) st := &stats.Stats{} duration := time.Duration(cfg.DurationSec) * time.Second rampUp := time.Duration(cfg.RampUpSec) * time.Second // Ticker for live stats statsTicker := time.NewTicker(500 * time.Millisecond) defer statsTicker.Stop() testStart := time.Now() done := make(chan struct{}) // Live stats printer go func() { for { select { case <-statsTicker.C: st.PrintLive(time.Since(testStart)) case <-done: return } } }() // Rate limiter with ramp-up var wg sync.WaitGroup sem := make(chan struct{}, cfg.Threads) targetRPS := cfg.RPS ticker := time.NewTicker(time.Second / time.Duration(targetRPS)) defer ticker.Stop() fmt.Println("Starting load test...") fmt.Println() for { elapsed := time.Since(testStart) if elapsed >= duration { break } // Ramp-up: scale RPS linearly currentRPS := targetRPS if elapsed < rampUp && rampUp > 0 { rampFraction := float64(elapsed) / float64(rampUp) currentRPS = int(float64(targetRPS) * rampFraction) if currentRPS < 1 { currentRPS = 1 } ticker.Reset(time.Second / time.Duration(currentRPS)) } else if elapsed >= rampUp { ticker.Reset(time.Second / time.Duration(targetRPS)) } <-ticker.C sem <- struct{}{} wg.Add(1) go func() { defer wg.Done() defer func() { <-sem }() sender.SendSMS(client, cfg, st) }() } // Wait for in-flight requests fmt.Println("\n\nWaiting for in-flight requests to complete...") wg.Wait() close(done) st.PrintFinalReport(time.Since(testStart)) }