package main import ( "fmt" "os" "path/filepath" "runtime" "strings" "time" ) // Targets defines the pass/fail thresholds. var Targets = struct { BaselineP50 time.Duration BaselineP99 time.Duration BaselineMaxLat time.Duration BaselineErrRate float64 SustainedP99 time.Duration SustainedErrRate float64 DegradationPct float64 }{ BaselineP50: 50 * time.Millisecond, BaselineP99: 200 * time.Millisecond, BaselineMaxLat: 1 * time.Second, BaselineErrRate: 0.0, SustainedP99: 200 * time.Millisecond, SustainedErrRate: 0.1, DegradationPct: 100.0, // <2x = <100% degradation } // TestResults holds all scenario results. type TestResults struct { BaselineMetrics *Metrics SustainedMetrics *Metrics ConcurrentMetrics *Metrics APIURL string Duration time.Duration TargetRPS int ReaderCount int MeterDisabled bool } // GenerateReport creates a markdown report from test results. func GenerateReport(results TestResults, outputDir string) (string, error) { now := time.Now() filename := fmt.Sprintf("%s-load-test.md", now.Format("2006-01-02")) outputPath := filepath.Join(outputDir, filename) var sb strings.Builder // Header sb.WriteString("# UAT Report: Load Testing Results\n\n") sb.WriteString(fmt.Sprintf("**Date:** %s\n", now.Format("2006-01-02"))) sb.WriteString("**Phase/Feature:** P4.1 Load Testing\n") sb.WriteString("**Tester:** load-test CLI\n") // Calculate overall status overallPass := true if results.BaselineMetrics != nil { if results.BaselineMetrics.WriteP99() > Targets.BaselineP99 || results.BaselineMetrics.WriteErrorRate() > Targets.BaselineErrRate { overallPass = false } } if results.SustainedMetrics != nil { if results.SustainedMetrics.WriteP99() > Targets.SustainedP99 || results.SustainedMetrics.WriteErrorRate() > Targets.SustainedErrRate { overallPass = false } } if results.ConcurrentMetrics != nil { if results.ConcurrentMetrics.DegradationPct() > Targets.DegradationPct { overallPass = false } } if overallPass { sb.WriteString("**Status:** PASS\n\n") } else { sb.WriteString("**Status:** FAIL\n\n") } // Environment sb.WriteString("## Environment\n\n") sb.WriteString(fmt.Sprintf("- Go version: %s\n", runtime.Version())) sb.WriteString(fmt.Sprintf("- OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) sb.WriteString(fmt.Sprintf("- StemeDB API: %s\n", results.APIURL)) if results.MeterDisabled { sb.WriteString("- Meter: Disabled (`STEMEDB_METER_ENABLED=false`)\n") } else { sb.WriteString("- Meter: Enabled (may affect sustained test)\n") } sb.WriteString("\n") // Test Results sb.WriteString("## Test Results\n\n") // Scenario A: Baseline if results.BaselineMetrics != nil { sb.WriteString(writeBaselineSection(results.BaselineMetrics)) } // Scenario B: Sustained if results.SustainedMetrics != nil { sb.WriteString(writeSustainedSection(results.SustainedMetrics, results.Duration, results.TargetRPS)) } // Scenario C: Concurrent if results.ConcurrentMetrics != nil { sb.WriteString(writeConcurrentSection(results.ConcurrentMetrics)) } // Recommendations sb.WriteString(writeRecommendations(results)) // Write file if err := os.MkdirAll(outputDir, 0755); err != nil { return "", fmt.Errorf("failed to create output directory: %w", err) } if err := os.WriteFile(outputPath, []byte(sb.String()), 0644); err != nil { return "", fmt.Errorf("failed to write report: %w", err) } return outputPath, nil } func writeBaselineSection(m *Metrics) string { var sb strings.Builder sb.WriteString("### Scenario A: Baseline Latency (10K Assertions)\n\n") sb.WriteString("| Metric | Target | Actual | Status |\n") sb.WriteString("|--------|--------|--------|--------|\n") // Total count sb.WriteString(fmt.Sprintf("| Total | 10,000 | %d | %s |\n", m.WriteCount(), passFailEmoji(m.WriteCount() >= 10000))) // P50 latency p50Pass := m.WriteP50() <= Targets.BaselineP50 sb.WriteString(fmt.Sprintf("| p50 latency | <%s | %s | %s |\n", Targets.BaselineP50, m.WriteP50(), passFailEmoji(p50Pass))) // P99 latency p99Pass := m.WriteP99() <= Targets.BaselineP99 sb.WriteString(fmt.Sprintf("| p99 latency | <%s | %s | %s |\n", Targets.BaselineP99, m.WriteP99(), passFailEmoji(p99Pass))) // Max latency maxPass := m.WriteMax() <= Targets.BaselineMaxLat sb.WriteString(fmt.Sprintf("| Max latency | <%s | %s | %s |\n", Targets.BaselineMaxLat, m.WriteMax(), passFailEmoji(maxPass))) // Error rate errPass := m.WriteErrorRate() <= Targets.BaselineErrRate sb.WriteString(fmt.Sprintf("| Error rate | 0%% | %.2f%% | %s |\n", m.WriteErrorRate(), passFailEmoji(errPass))) sb.WriteString("\n") return sb.String() } func writeSustainedSection(m *Metrics, duration time.Duration, targetRPS int) string { var sb strings.Builder sb.WriteString("### Scenario B: Sustained Writes\n\n") sb.WriteString("| Metric | Target | Actual | Status |\n") sb.WriteString("|--------|--------|--------|--------|\n") // Duration sb.WriteString(fmt.Sprintf("| Duration | %s | %s | PASS |\n", duration.Round(time.Second), m.Duration().Round(time.Second))) // Throughput actualRPS := m.WriteThroughput() rpsPass := actualRPS >= float64(targetRPS)*0.9 // Allow 10% tolerance sb.WriteString(fmt.Sprintf("| Avg throughput | %d/sec | %.0f/sec | %s |\n", targetRPS, actualRPS, passFailEmoji(rpsPass))) // P99 latency p99Pass := m.WriteP99() <= Targets.SustainedP99 sb.WriteString(fmt.Sprintf("| p99 latency | <%s | %s | %s |\n", Targets.SustainedP99, m.WriteP99(), passFailEmoji(p99Pass))) // Error rate errPass := m.WriteErrorRate() <= Targets.SustainedErrRate sb.WriteString(fmt.Sprintf("| Error rate | <%.1f%% | %.3f%% | %s |\n", Targets.SustainedErrRate, m.WriteErrorRate(), passFailEmoji(errPass))) // Total writes sb.WriteString(fmt.Sprintf("| Total writes | - | %d | - |\n", m.WriteCount())) sb.WriteString("\n") return sb.String() } func writeConcurrentSection(m *Metrics) string { var sb strings.Builder sb.WriteString("### Scenario C: Concurrent Readers\n\n") sb.WriteString("| Metric | Target | Actual | Status |\n") sb.WriteString("|--------|--------|--------|--------|\n") // Reader count sb.WriteString(fmt.Sprintf("| Reader count | 100 | %d | PASS |\n", m.ReaderCount())) // Baseline P99 sb.WriteString(fmt.Sprintf("| Baseline p99 | - | %s | - |\n", m.BaselineP99())) // Under-load P99 degradationPass := m.DegradationPct() <= Targets.DegradationPct sb.WriteString(fmt.Sprintf("| Under-load p99 | <2x baseline | %s | %s |\n", m.LoadedP99(), passFailEmoji(degradationPass))) // Degradation percentage sb.WriteString(fmt.Sprintf("| Degradation | <100%% | %.1f%% | %s |\n", m.DegradationPct(), passFailEmoji(degradationPass))) sb.WriteString("\n") return sb.String() } func writeRecommendations(results TestResults) string { var sb strings.Builder var recommendations []string sb.WriteString("## Recommendations\n\n") // Check baseline if results.BaselineMetrics != nil { if results.BaselineMetrics.WriteP99() > Targets.BaselineP99 { recommendations = append(recommendations, "Baseline p99 latency exceeds target. Consider profiling write path for bottlenecks.") } if results.BaselineMetrics.WriteErrorRate() > 0 { recommendations = append(recommendations, "Baseline test had errors. Investigate server logs for root cause.") } } // Check sustained if results.SustainedMetrics != nil { if results.SustainedMetrics.WriteP99() > Targets.SustainedP99 { recommendations = append(recommendations, "Sustained write p99 latency exceeds target. Consider scaling or optimizing WAL.") } if results.SustainedMetrics.WriteErrorRate() > Targets.SustainedErrRate { recommendations = append(recommendations, "Sustained write error rate too high. Check for resource exhaustion or rate limiting.") } if !results.MeterDisabled { recommendations = append(recommendations, "Meter was enabled during sustained test. Re-run with STEMEDB_METER_ENABLED=false for accurate results.") } } // Check concurrent if results.ConcurrentMetrics != nil { if results.ConcurrentMetrics.DegradationPct() > Targets.DegradationPct { recommendations = append(recommendations, "Read latency degradation under load exceeds 2x. Consider read replica or caching layer.") } } if len(recommendations) == 0 { sb.WriteString("All tests passed. System is ready for production load.\n\n") } else { for _, rec := range recommendations { sb.WriteString(fmt.Sprintf("- %s\n", rec)) } sb.WriteString("\n") } return sb.String() } func passFailEmoji(pass bool) string { if pass { return "PASS" } return "FAIL" }