package imap import ( "bufio" "context" "fmt" "net" "strings" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest" ) // imapLoadtestConcurrency/imapLoadtestSessions — siehe pop3-Pendant für // die Begründung der Größenordnung (Akzeptanzkriterium 1, QA-07). const ( imapLoadtestConcurrency = 200 imapLoadtestSessions = 2000 ) // Zielwerte (Akzeptanzkriterium 3, QA-07) — festgelegt VOR dem // entscheidenden Testlauf, siehe Prüfprotokoll QA-07 für die // Vorab-Messwerte auf 192.168.1.131. const ( imapTargetP95Latency = 100 * time.Millisecond imapTargetThroughputMin = 800.0 // Sessions/Sekunde ) // TestLoadtest_IMAPParallelSessionsMeetTargets ist die geforderte // Pflichtprüfung 1+2 (QA-07). func TestLoadtest_IMAPParallelSessionsMeetTargets(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(), "imap", imapLoadtestConcurrency, imapLoadtestSessions, func(ctx context.Context) error { return runIMAPLoadSession(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 fmt.Printf("QA-07 IMAP-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, imapTargetThroughputMin) fmt.Printf(" Latenz p50=%v p95=%v (Ziel: <= %v) max=%v\n", p50, p95, imapTargetP95Latency, maxLatency) fmt.Printf(" Ressourcen: Heap-Delta=%.1f MiB, CPU-Zeit=%.2fs\n", heapDeltaMB, cpuUsed) if result.Errors != 0 { t.Fatalf("QA-07 IMAP: %d von %d sessions fehlgeschlagen", result.Errors, result.Sessions) } if p95 > imapTargetP95Latency { t.Fatalf("QA-07 IMAP: p95-latenz %v überschreitet zielwert %v", p95, imapTargetP95Latency) } if throughput < imapTargetThroughputMin { t.Fatalf("QA-07 IMAP: durchsatz %.1f sessions/s unter zielwert %.1f", throughput, imapTargetThroughputMin) } if heapDeltaMB > 100 { t.Fatalf("QA-07 IMAP: heap-wachstum %.1f MiB deutet auf ressourcenleck hin", heapDeltaMB) } } // runIMAPLoadSession führt eine vollständige, realistische IMAP-Session // über eine ROHE TCP-Verbindung aus (keine testing.T-Hilfsfunktionen, // die bei Fehlern t.Fatalf/goexit auslösen — in einer nebenläufigen // Lasttest-Worker-Goroutine unzulässig und würde den Fehler stillschweigend // verschlucken statt ihn zu zählen). func runIMAPLoadSession(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 { // Begrüßung return err } if _, err := sendTaggedRaw(conn, reader, "A1", "LOGIN alice geheim123"); err != nil { return fmt.Errorf("LOGIN: %w", err) } if _, err := sendTaggedRaw(conn, reader, "A2", "SELECT INBOX"); err != nil { return fmt.Errorf("SELECT: %w", err) } if _, err := sendTaggedRaw(conn, reader, "A3", "FETCH 1 (FLAGS)"); err != nil { return fmt.Errorf("FETCH: %w", err) } _, err = sendTaggedRaw(conn, reader, "A4", "LOGOUT") return err } // sendTaggedRaw sendet ein getaggtes Kommando und liest bis zur // getaggten Abschlusszeile; liefert einen Fehler, wenn diese kein "OK" // enthält. func sendTaggedRaw(conn net.Conn, reader *bufio.Reader, tag, command string) (lines []string, err error) { if _, err := conn.Write([]byte(tag + " " + command + "\r\n")); err != nil { return nil, err } for { line, err := reader.ReadString('\n') if err != nil { return nil, err } line = strings.TrimRight(line, "\r\n") lines = append(lines, line) if strings.HasPrefix(line, tag+" ") { if !strings.Contains(line, "OK") { return lines, fmt.Errorf("unerwartete abschlussantwort: %q", line) } return lines, nil } } }