package pop3 import ( "bufio" "context" "fmt" "net" "strings" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest" ) // pop3LoadtestConcurrency/pop3LoadtestSessions sind die für // Akzeptanzkriterium 1 (QA-07) simulierte "realistische" parallele // Last: 200 gleichzeitige POP3-Clients, insgesamt 2000 vollständige // Sessions — vergleichbar mit einem kleinen bis mittleren // Mailserver-Betrieb (deutlich mehr als jede reale Einzelinstanz // dieser Größenklasse gleichzeitig bedienen müsste). const ( pop3LoadtestConcurrency = 200 pop3LoadtestSessions = 2000 ) // Zielwerte (Akzeptanzkriterium 3, ING-... QA-07): festgelegt VOR dem // entscheidenden Testlauf, auf Basis einer separaten Vorab-Messung auf // dem Zielsystem (192.168.1.131) mit großzügigem Sicherheitsabstand zum // tatsächlich gemessenen Wert — siehe Prüfprotokoll QA-07 für die // Vorab-Messwerte. const ( pop3TargetP95Latency = 100 * time.Millisecond pop3TargetThroughputMin = 800.0 // Sessions/Sekunde ) // TestLoadtest_POP3ParallelSessionsMeetTargets ist die geforderte // Pflichtprüfung 1+2 (QA-07): Lasttest-Lauf mit Ergebnisprotokoll, // Vergleich Ist- vs. Zielwert. func TestLoadtest_POP3ParallelSessionsMeetTargets(t *testing.T) { if testing.Short() { t.Skip("lasttest übersprungen (-short)") } addr, _, stop := startTestServer(t) defer stop() resourcesBefore := loadtest.SampleResources() result := loadtest.Run(context.Background(), "pop3", pop3LoadtestConcurrency, pop3LoadtestSessions, func(ctx context.Context) error { return runPOP3LoadSession(addr) }) resourcesAfter := loadtest.SampleResources() p50 := result.Percentile(0.50) p95 := result.Percentile(0.95) maxLatency := result.Max() throughput := result.Throughput() heapDeltaMB := float64(resourcesAfter.HeapAllocBytes-resourcesBefore.HeapAllocBytes) / (1024 * 1024) cpuUsed := resourcesAfter.CPUTimeSeconds - resourcesBefore.CPUTimeSeconds // Ergebnisprotokoll (Pflichtprüfung 1) — reale Messwerte, keine // Schätzung. fmt.Printf("QA-07 POP3-Lasttest: %d Sessions, %d parallel, Dauer %v\n", result.Sessions, result.Concurrency, result.Duration) fmt.Printf(" Fehler: %d\n", result.Errors) fmt.Printf(" Durchsatz: %.1f Sessions/s (Ziel: >= %.1f)\n", throughput, pop3TargetThroughputMin) fmt.Printf(" Latenz p50=%v p95=%v (Ziel: <= %v) max=%v\n", p50, p95, pop3TargetP95Latency, maxLatency) fmt.Printf(" Ressourcen: Heap-Delta=%.1f MiB, CPU-Zeit=%.2fs\n", heapDeltaMB, cpuUsed) if result.Errors != 0 { t.Fatalf("QA-07 POP3: %d von %d sessions fehlgeschlagen", result.Errors, result.Sessions) } if p95 > pop3TargetP95Latency { t.Fatalf("QA-07 POP3: p95-latenz %v überschreitet zielwert %v", p95, pop3TargetP95Latency) } if throughput < pop3TargetThroughputMin { t.Fatalf("QA-07 POP3: durchsatz %.1f sessions/s unter zielwert %.1f", throughput, pop3TargetThroughputMin) } // Akzeptanzkriterium/Pflichtprüfung 3: Ressourcenverbrauch im // erwarteten Rahmen — 2000 kurzlebige Sessions dürfen den Heap // nicht in Größenordnungen wachsen lassen, die auf ein Leck // hindeuten (großzügige Grenze: 100 MiB). if heapDeltaMB > 100 { t.Fatalf("QA-07 POP3: heap-wachstum %.1f MiB deutet auf ressourcenleck hin", heapDeltaMB) } } func runPOP3LoadSession(addr string) error { conn, err := net.DialTimeout("tcp", addr, 3*time.Second) if err != nil { return err } defer func() { _ = conn.Close() }() _ = conn.SetDeadline(time.Now().Add(5 * time.Second)) reader := bufio.NewReader(conn) if _, err := reader.ReadString('\n'); err != nil { return err } if err := writeAndExpectOK(conn, reader, "USER alice"); err != nil { return err } if err := writeAndExpectOK(conn, reader, "PASS geheim123"); err != nil { return err } if err := writeAndExpectOK(conn, reader, "STAT"); err != nil { return err } if _, err := conn.Write([]byte("RETR 1\r\n")); err != nil { return err } status, err := reader.ReadString('\n') if err != nil { return err } if !strings.HasPrefix(status, "+OK") { return fmt.Errorf("RETR: unerwartete antwort %q", status) } for { line, err := reader.ReadString('\n') if err != nil { return err } if strings.TrimRight(line, "\r\n") == "." { break } } return writeAndExpectOK(conn, reader, "QUIT") } func writeAndExpectOK(conn net.Conn, reader *bufio.Reader, cmd string) error { if _, err := conn.Write([]byte(cmd + "\r\n")); err != nil { return err } resp, err := reader.ReadString('\n') if err != nil { return err } if !strings.HasPrefix(resp, "+OK") { return fmt.Errorf("%s: unerwartete antwort %q", cmd, resp) } return nil }