// Package testreport provides structured test reporting and output package testreport import ( "encoding/json" "fmt" "os" "strings" "testing" "time" ) // TestResult represents a single test result type TestResult struct { Name string `json:"name"` Package string `json:"package"` Status string `json:"status"` // pass, fail, skip Duration time.Duration `json:"duration"` Output string `json:"output,omitempty"` Error string `json:"error,omitempty"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` } // TestSuite represents a collection of test results type TestSuite struct { Name string `json:"name"` Package string `json:"package"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Tests []TestResult `json:"tests"` } // Summary provides aggregate statistics type Summary struct { Total int `json:"total"` Passed int `json:"passed"` Failed int `json:"failed"` Skipped int `json:"skipped"` Duration time.Duration `json:"duration"` } // Reporter handles test reporting type Reporter struct { suite TestSuite current *TestResult testMap map[string]*TestResult } // NewReporter creates a new test reporter func NewReporter(name, pkg string) *Reporter { return &Reporter{ suite: TestSuite{ Name: name, Package: pkg, StartTime: time.Now(), Tests: []TestResult{}, }, testMap: make(map[string]*TestResult), } } // StartTest records the start of a test func (r *Reporter) StartTest(name string) { result := &TestResult{ Name: name, Package: r.suite.Package, StartTime: time.Now(), Status: "running", } r.current = result r.testMap[name] = result } // EndTest records the end of a test func (r *Reporter) EndTest(name string, status string, err error) { if r.current == nil || r.current.Name != name { r.current = r.testMap[name] } if r.current == nil { return } r.current.EndTime = time.Now() r.current.Duration = r.current.EndTime.Sub(r.current.StartTime) r.current.Status = status if err != nil { r.current.Error = err.Error() } r.suite.Tests = append(r.suite.Tests, *r.current) r.current = nil } // RecordOutput captures test output func (r *Reporter) RecordOutput(output string) { if r.current != nil { r.current.Output += output + "\n" } } // Summary generates aggregate statistics func (r *Reporter) Summary() Summary { s := Summary{ Total: len(r.suite.Tests), } for _, t := range r.suite.Tests { switch t.Status { case "pass": s.Passed++ case "fail": s.Failed++ case "skip": s.Skipped++ } } return s } // ToJSON exports the test suite as JSON func (r *Reporter) ToJSON() ([]byte, error) { r.suite.EndTime = time.Now() return json.MarshalIndent(r.suite, "", " ") } // SaveToFile writes the test report to a file func (r *Reporter) SaveToFile(path string) error { data, err := r.ToJSON() if err != nil { return err } return os.WriteFile(path, data, 0644) } // ReportToEnv outputs report path to environment for CI func (r *Reporter) ReportToEnv() { if path := os.Getenv("TEST_REPORT_PATH"); path != "" { r.SaveToFile(path) fmt.Fprintf(os.Stderr, "Test report saved to: %s\n", path) } } // FlakyTestTracker tracks potentially flaky tests type FlakyTestTracker struct { runs map[string][]bool // test name -> []passed } // NewFlakyTestTracker creates a new flaky test tracker func NewFlakyTestTracker() *FlakyTestTracker { return &FlakyTestTracker{ runs: make(map[string][]bool), } } // RecordResult records a test result func (ft *FlakyTestTracker) RecordResult(name string, passed bool) { ft.runs[name] = append(ft.runs[name], passed) } // IsFlaky returns true if a test has inconsistent results func (ft *FlakyTestTracker) IsFlaky(name string) bool { runs := ft.runs[name] if len(runs) < 3 { return false } // Check for mixed results passed := 0 failed := 0 for _, r := range runs { if r { passed++ } else { failed++ } } // Flaky if both passed and failed exist return passed > 0 && failed > 0 } // GetFlakyTests returns all tests that appear flaky func (ft *FlakyTestTracker) GetFlakyTests() []string { var flaky []string for name := range ft.runs { if ft.IsFlaky(name) { flaky = append(flaky, name) } } return flaky } // Report generates a flaky test report func (ft *FlakyTestTracker) Report() string { flaky := ft.GetFlakyTests() if len(flaky) == 0 { return "No flaky tests detected" } var report strings.Builder report.WriteString("Potentially Flaky Tests:\n") for _, name := range flaky { runs := ft.runs[name] passed := 0 for _, r := range runs { if r { passed++ } } fmt.Fprintf(&report, " - %s: %d/%d passed (%.1f%%)\n", name, passed, len(runs), float64(passed)*100/float64(len(runs))) } return report.String() } // TestTimer provides timing utilities for tests type TestTimer struct { start time.Time duration time.Duration } // NewTestTimer creates a new test timer func NewTestTimer() *TestTimer { return &TestTimer{start: time.Now()} } // Elapsed returns elapsed time func (tt *TestTimer) Elapsed() time.Duration { return time.Since(tt.start) } // CheckBudget checks if test is within time budget func (tt *TestTimer) CheckBudget(budget time.Duration, t *testing.T) bool { elapsed := tt.Elapsed() if elapsed > budget { t.Logf("WARNING: Test exceeded time budget: %v > %v", elapsed, budget) return false } return true } // PerformanceRegression tracks performance metrics type PerformanceRegression struct { metrics map[string][]float64 // metric name -> values } // NewPerformanceRegression creates a new tracker func NewPerformanceRegression() *PerformanceRegression { return &PerformanceRegression{ metrics: make(map[string][]float64), } } // Record records a metric value func (pr *PerformanceRegression) Record(name string, value float64) { pr.metrics[name] = append(pr.metrics[name], value) } // CheckRegression checks if current value regresses from baseline func (pr *PerformanceRegression) CheckRegression(name string, current float64, threshold float64) bool { values := pr.metrics[name] if len(values) < 3 { return false // Not enough data } // Calculate average of previous runs var sum float64 for _, v := range values { sum += v } avg := sum / float64(len(values)) // Regression if current is worse than threshold * average return current > avg*threshold }