## Phase 8: Enterprise Extractor Improvements ✅ - 14 security extractors (TLS, JWT, SQL injection, XSS, etc.) - 10 framework-specific extractors (Spring, Django, Rails, etc.) - Config file security detection (YAML, TOML) ## Phase 9: Autonomous Extractor Generation ✅ - Shadow mode executor with TP/FP tracking - Graduation pipeline with confidence thresholds - Auto-rollback on regression detection - Cross-project pattern syncing ## UAT Suite Complete (14 scripts, 90 tests) - test-core-detection.sh (6 tests) - test-declarative-extractors.sh (5 tests) - test-domain-frameworks.sh (5 tests) - test-domain-unreal.sh (3 tests) - test-llm-extraction.sh (6 tests) - test-eval-harness.sh (5 tests) - test-cross-language.sh (3 tests) - test-precommit-performance.sh (4 tests) - test-output-formats.sh (8 tests) - test-drift-detection.sh (6 tests) - test-exit-codes.sh (12 tests) + 3 more scripts ## Other Changes - Updated roadmap to mark Phase 8-9 complete - Added .gitignore entries for build artifacts - Updated pre-commit: 800 line limit, exclude tests/data/cmd Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
300 lines
8.6 KiB
Go
300 lines
8.6 KiB
Go
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"
|
|
}
|