From 5340f73d7f706ba30bbc88bba7d8caba05a2388c Mon Sep 17 00:00:00 2001 From: Said Boudjelda Date: Sat, 17 Oct 2026 19:17:52 +0200 Subject: [PATCH] fix(cli): report exports read each phase's name, metrics and SLA verdict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend writes a report phase as {phaseName, metrics, slaVerdict}, but the CLI read name, status, recordsSent, throughputRecordsPerSec, p99LatencyMs and errorRate, none of which exist. Every phase of a LOAD report, produce and consume alike, came out of `report export --format md` and `--format html` as "main ✓ 0 0.0 rec/s", a phase whose SLA failed included. The client test encoded the Go struct, so it agreed with itself. ReportPhase now has the backend's shape. Both renderers take the numbers from the phase's metrics and mark the column, renamed from Status to SLA, ✓ or ✖ from its slaVerdict (— when there is none). The phase name is the user's scenario text, so the HTML export escapes it and the Markdown one escapes |. report show no longer sums phases to fill in a summary of 0: they are built from the same rows as the summary, so the sum is 0 too. `report show -o json` now prints phaseName, metrics and slaVerdict in place of the old keys, which were always empty. TestReport serves the backend's JSON as written. --- cli/client/client_test.go | 28 +++++++++----- cli/client/types.go | 11 +++--- cli/cmd/report.go | 66 +++++++++++---------------------- cli/cmd/report_html.go | 19 ++++++---- cli/cmd/report_html_test.go | 13 ++++--- cli/cmd/report_markdown.go | 19 ++++++---- cli/cmd/report_markdown_test.go | 22 ++++++++--- 7 files changed, 91 insertions(+), 87 deletions(-) diff --git a/cli/client/client_test.go b/cli/client/client_test.go index 4bd94ca3..663a2ae7 100644 --- a/cli/client/client_test.go +++ b/cli/client/client_test.go @@ -586,16 +586,17 @@ func TestBackends(t *testing.T) { } } +// The report is served as the backend writes it, not as the Go struct +// encodes it, so a phase key the CLI names wrongly fails here. func TestReport(t *testing.T) { - c, _ := testServer(t, jsonHandler(t, "GET", "/api/tests/", Report{ - Summary: &ReportSummary{ - TotalRecords: 100000, AvgLatencyMs: 5.2, P99LatencyMs: 12.3, - AvgThroughputRecPerSec: 50000, ErrorRate: 0.001, - }, - OverallSlaVerdict: &SlaVerdict{ - Passed: true, - }, - })) + c, _ := testServer(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"summary":{"totalRecords":100000,"avgThroughputRecPerSec":50000,` + + `"avgLatencyMs":5.2,"p99LatencyMs":12.3,"errorRate":0.001},` + + `"overallSlaVerdict":{"passed":true},"phases":[` + + `{"phaseName":"produce","metrics":{"totalRecords":60000,"avgThroughputRecPerSec":30000,"p99LatencyMs":9.5},"slaVerdict":{"passed":true}},` + + `{"phaseName":"consume","metrics":{"totalRecords":40000,"avgThroughputRecPerSec":20000},"slaVerdict":{"passed":false}}]}`)) + }) resp, err := c.Report(context.Background(), "run-1") if err != nil { t.Fatal(err) @@ -606,6 +607,15 @@ func TestReport(t *testing.T) { if !resp.OverallSlaVerdict.Passed { t.Error("SLA should have passed") } + if len(resp.Phases) != 2 { + t.Fatalf("Phases = %+v", resp.Phases) + } + if p := resp.Phases[0]; p.Metrics == nil || p.Metrics.AvgThroughputRecPerSec != 30000 || p.Metrics.P99LatencyMs != 9.5 { + t.Errorf("produce = %+v", p.Metrics) + } + if p := resp.Phases[1]; p.Name != "consume" || p.SlaVerdict == nil || p.SlaVerdict.Passed { + t.Errorf("consume = %+v", p) + } } func TestReportSummary(t *testing.T) { diff --git a/cli/client/types.go b/cli/client/types.go index 9c243187..081ed28f 100644 --- a/cli/client/types.go +++ b/cli/client/types.go @@ -286,13 +286,12 @@ type Report struct { Phases []ReportPhase `json:"phases,omitempty"` } +// ReportPhase is the backend's PhaseReport: a phase's name, its own summary +// and its own SLA verdict. type ReportPhase struct { - Name string `json:"name"` - Status string `json:"status"` - RecordsSent float64 `json:"recordsSent"` - ThroughputRecPerSec float64 `json:"throughputRecordsPerSec"` - P99LatencyMs float64 `json:"p99LatencyMs"` - ErrorRate float64 `json:"errorRate"` + Name string `json:"phaseName"` + Metrics *ReportSummary `json:"metrics,omitempty"` + SlaVerdict *SlaVerdict `json:"slaVerdict,omitempty"` } type SlaVerdict struct { diff --git a/cli/cmd/report.go b/cli/cmd/report.go index 59e41e8d..d2e494f5 100644 --- a/cli/cmd/report.go +++ b/cli/cmd/report.go @@ -35,61 +35,39 @@ var reportShowCmd = &cobra.Command{ summary := result.Summary if summary == nil || summary.AvgThroughputRecPerSec == 0 { - if len(result.Phases) > 0 { + run, fetchErr := apiClient.GetTest(context.Background(), args[0]) + if fetchErr == nil && len(run.Results) > 0 && !isStaleResult(run.Results) { s := &client.ReportSummary{} count := 0 - for _, p := range result.Phases { - s.TotalRecords += p.RecordsSent - if p.ThroughputRecPerSec > s.PeakThroughputRecPerSec { - s.PeakThroughputRecPerSec = p.ThroughputRecPerSec + for _, r := range run.Results { + s.TotalRecords += r.RecordsSent + if r.ThroughputRecordsPerSec > s.PeakThroughputRecPerSec { + s.PeakThroughputRecPerSec = r.ThroughputRecordsPerSec } - if p.ThroughputRecPerSec > 0 { - s.AvgThroughputRecPerSec += p.ThroughputRecPerSec + if r.ThroughputRecordsPerSec > 0 { + s.AvgThroughputRecPerSec += r.ThroughputRecordsPerSec count++ } - if p.P99LatencyMs > s.P99LatencyMs { - s.P99LatencyMs = p.P99LatencyMs + if r.P99LatencyMs > s.P99LatencyMs { + s.P99LatencyMs = r.P99LatencyMs + } + if r.AvgLatencyMs > s.AvgLatencyMs { + s.AvgLatencyMs = r.AvgLatencyMs + } + if r.P50LatencyMs > s.P50LatencyMs { + s.P50LatencyMs = r.P50LatencyMs + } + if r.P95LatencyMs > s.P95LatencyMs { + s.P95LatencyMs = r.P95LatencyMs + } + if r.MaxLatencyMs > s.MaxLatencyMs { + s.MaxLatencyMs = r.MaxLatencyMs } } if count > 0 { s.AvgThroughputRecPerSec /= float64(count) } summary = s - } else { - run, fetchErr := apiClient.GetTest(context.Background(), args[0]) - if fetchErr == nil && len(run.Results) > 0 && !isStaleResult(run.Results) { - s := &client.ReportSummary{} - count := 0 - for _, r := range run.Results { - s.TotalRecords += r.RecordsSent - if r.ThroughputRecordsPerSec > s.PeakThroughputRecPerSec { - s.PeakThroughputRecPerSec = r.ThroughputRecordsPerSec - } - if r.ThroughputRecordsPerSec > 0 { - s.AvgThroughputRecPerSec += r.ThroughputRecordsPerSec - count++ - } - if r.P99LatencyMs > s.P99LatencyMs { - s.P99LatencyMs = r.P99LatencyMs - } - if r.AvgLatencyMs > s.AvgLatencyMs { - s.AvgLatencyMs = r.AvgLatencyMs - } - if r.P50LatencyMs > s.P50LatencyMs { - s.P50LatencyMs = r.P50LatencyMs - } - if r.P95LatencyMs > s.P95LatencyMs { - s.P95LatencyMs = r.P95LatencyMs - } - if r.MaxLatencyMs > s.MaxLatencyMs { - s.MaxLatencyMs = r.MaxLatencyMs - } - } - if count > 0 { - s.AvgThroughputRecPerSec /= float64(count) - } - summary = s - } } } diff --git a/cli/cmd/report_html.go b/cli/cmd/report_html.go index 98f3ef0f..a28dcd69 100644 --- a/cli/cmd/report_html.go +++ b/cli/cmd/report_html.go @@ -213,17 +213,20 @@ func renderHTMLReport(id string, r *client.Report) string { b.WriteString(`

Phase Breakdown

- + `) for _, p := range r.Phases { - phaseName := p.Name - if phaseName == "" { - phaseName = "main" + m := client.ReportSummary{} + if p.Metrics != nil { + m = *p.Metrics } - statusBadge := `` + p.Status + `` - if strings.EqualFold(p.Status, "FAILED") || strings.EqualFold(p.Status, "ERROR") { - statusBadge = `` + p.Status + `` + slaBadge := "—" + if v := p.SlaVerdict; v != nil { + slaBadge = `✓ PASSED` + if !v.Passed { + slaBadge = `✖ FAILED` + } } b.WriteString(fmt.Sprintf(` @@ -233,7 +236,7 @@ func renderHTMLReport(id string, r *client.Report) string { -`, phaseName, statusBadge, fmtNum(p.RecordsSent), fmtFloat(p.ThroughputRecPerSec, 1), fmtFloat(p.P99LatencyMs, 2), p.ErrorRate*100)) +`, html.EscapeString(p.Name), slaBadge, fmtNum(m.TotalRecords), fmtFloat(m.AvgThroughputRecPerSec, 1), fmtFloat(m.P99LatencyMs, 2), m.ErrorRate*100)) } b.WriteString(`
PhaseStatusRecordsThroughputP99 (ms)Errors
PhaseSLARecordsThroughputP99 (ms)Errors
%s%s %.4f%%
diff --git a/cli/cmd/report_html_test.go b/cli/cmd/report_html_test.go index 6992ffe8..c9efa0fa 100644 --- a/cli/cmd/report_html_test.go +++ b/cli/cmd/report_html_test.go @@ -118,8 +118,8 @@ func TestRenderHTMLReport_WithPhases(t *testing.T) { report := &client.Report{ Summary: &client.ReportSummary{TotalRecords: 1}, Phases: []client.ReportPhase{ - {Name: "warmup", Status: "DONE", RecordsSent: 500}, - {Name: "burst", Status: "FAILED", RecordsSent: 200}, + {Name: "warmup", Metrics: &client.ReportSummary{TotalRecords: 500}, SlaVerdict: &client.SlaVerdict{Passed: true}}, + {Name: "