From cb0743dbe2e02ef21e39fd0dce0ebc8b6f308e4d Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Sun, 26 Oct 2025 15:49:56 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(cli):=20f=C3=BCge=20-csv=20Shortcut=20?= =?UTF-8?q?f=C3=BCr=20CSV-Stdout=20hinzu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/httprunner/main.go | 66 ++++++++++++++++++++++--------------- cmd/httprunner/main_test.go | 28 ++++++++++++++++ 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/cmd/httprunner/main.go b/cmd/httprunner/main.go index f889c41..40914c8 100644 --- a/cmd/httprunner/main.go +++ b/cmd/httprunner/main.go @@ -19,9 +19,9 @@ import ( var version = "dev" func main() { - // Command line flags - showVersion := flag.Bool("version", false, "Print version and exit") - flag.BoolVar(showVersion, "V", false, "Print version and exit") + // Command line flags + showVersion := flag.Bool("version", false, "Print version and exit") + flag.BoolVar(showVersion, "V", false, "Print version and exit") concurrency := flag.Int("u", 1, "Number of parallel virtual parallel users") iterations := flag.Int("i", 1, "Number of iterations") runtime := flag.Int("r", 0, "Runtime duration in seconds (0 means use iterations)") @@ -29,11 +29,12 @@ func main() { offset := flag.Int("offset", 0, "Max random startup delay per VU in milliseconds") requestFile := flag.String("f", "", ".http file containing http requests") envFile := flag.String("e", "", ".env file containing environment variables") - reportFormat := flag.String("report", "console", "Report format: console, html, csv, json") - reportOutput := flag.String("output", "results", "Output directory for results and reports") - reportDetail := flag.String("detail", "summary", "Report detail level: summary, goroutine, iteration, full") - verbose := flag.Bool("v", false, "Verbose mode: print request result JSON for each request") - rawFile := flag.String("raw", "", "Path to raw results .jsonl file to generate report without executing") + reportFormat := flag.String("report", "console", "Report format: console, html, csv, json") + reportOutput := flag.String("output", "results", "Output directory for results and reports") + reportDetail := flag.String("detail", "summary", "Report detail level: summary, goroutine, iteration, full") + verbose := flag.Bool("v", false, "Verbose mode: print request result JSON for each request") + rawFile := flag.String("raw", "", "Path to raw results .jsonl file to generate report without executing") + csvShortcut := flag.Bool("csv", false, "Shorthand to output CSV from -raw to stdout (forces -report=csv, -detail=summary)") flag.Parse() @@ -42,12 +43,25 @@ func main() { return } - // Offline reporting mode: when -raw is provided, skip execution - offlineMode := *rawFile != "" - if !offlineMode && *requestFile == "" { - fmt.Println("Error: -f flag is required (or provide -raw to read a raw results file)") - os.Exit(1) - } + // Offline reporting mode: when -raw is provided, skip execution + offlineMode := *rawFile != "" + if !offlineMode && *requestFile == "" { + fmt.Println("Error: -f flag is required (or provide -raw to read a raw results file)") + os.Exit(1) + } + + // If -csv shorthand is provided, force CSV formatting and summary detail; + // also prefer printing to stdout for easy redirection. + forceStdout := false + if *csvShortcut { + *reportFormat = string(types.FormatCSV) + *reportDetail = string(types.DetailSummary) + forceStdout = true + if !offlineMode { + fmt.Println("Error: -csv requires -raw to be provided") + os.Exit(1) + } + } // Validate runtime vs iterations parameters (only for execution mode) if !offlineMode { @@ -81,10 +95,10 @@ func main() { os.Exit(1) } - // If offline mode, build report from raw file and exit - if offlineMode { - // Build a FileReporter on the provided raw file - fr := reporting.NewFileReporter(*rawFile) + // If offline mode, build report from raw file and exit + if offlineMode { + // Build a FileReporter on the provided raw file + fr := reporting.NewFileReporter(*rawFile) var reportContent string var err error @@ -115,14 +129,14 @@ func main() { os.Exit(1) } - // Output handling mirrors execution mode - format := types.ReportFormat(strings.ToLower(*reportFormat)) - if format == types.FormatConsole { - fmt.Print(reportContent) - } else { - // Ensure output directory exists - if err := os.MkdirAll(*reportOutput, 0755); err != nil { - fmt.Printf("Error ensuring output directory: %v\n", err) + // Output handling mirrors execution mode + format := types.ReportFormat(strings.ToLower(*reportFormat)) + if format == types.FormatConsole || forceStdout { + fmt.Print(reportContent) + } else { + // Ensure output directory exists + if err := os.MkdirAll(*reportOutput, 0755); err != nil { + fmt.Printf("Error ensuring output directory: %v\n", err) os.Exit(1) } filename := filepath.Join(*reportOutput, "report") diff --git a/cmd/httprunner/main_test.go b/cmd/httprunner/main_test.go index 5f0f460..0aa593c 100644 --- a/cmd/httprunner/main_test.go +++ b/cmd/httprunner/main_test.go @@ -150,3 +150,31 @@ func TestRunnerExitsOnInvalidDetailLevel(t *testing.T) { t.Fatalf("expected invalid detail message; got: %s", string(out)) } } + +func TestOfflineCSVShortcutPrintsToStdout(t *testing.T) { + dir := t.TempDir() + // Write minimal JSONL with a single RequestResult + jsonl := `{"Name":"Req1","Verb":"GET","URL":"http://example.com","StatusCode":200,"ResponseTime":100000000,"Success":true,"Error":"","Timestamp":"2024-01-01T00:00:00Z","VirtualUserID":1,"IterationID":1,"Checks":[]}` + p := filepath.Join(dir, "results.jsonl") + if err := os.WriteFile(p, []byte(jsonl+"\n"), 0644); err != nil { + t.Fatalf("write jsonl: %v", err) + } + + // Isolate flags + oldFS := flag.CommandLine + flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) + flag.CommandLine.SetOutput(new(strings.Builder)) + defer func() { flag.CommandLine = oldFS }() + + oldArgs := os.Args + os.Args = []string{"httprunner", "-raw", p, "-csv"} + defer func() { os.Args = oldArgs }() + + out := captureStdout(func() { main() }) + if !strings.Contains(out, "Index,Name,Method,URL,Success,StatusCode,ResponseTime,Error,CheckFailures,Timestamp") { + t.Fatalf("expected CSV header in output; got:\n%s", out) + } + if !strings.Contains(out, "Req1") { + t.Fatalf("expected CSV row with request name; got:\n%s", out) + } +} From c658240c1f00516eca5e3441f60556d9236d69bf Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Sun, 26 Oct 2025 17:24:51 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(cli):=20fehlerbehandlung=20f=C3=BCr=20C?= =?UTF-8?q?SV-Shortcut=20korrigiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/httprunner/main.go | 80 ++++++++++++++++++------------------- cmd/httprunner/main_test.go | 50 +++++++++++------------ 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/cmd/httprunner/main.go b/cmd/httprunner/main.go index 40914c8..7834f41 100644 --- a/cmd/httprunner/main.go +++ b/cmd/httprunner/main.go @@ -19,9 +19,9 @@ import ( var version = "dev" func main() { - // Command line flags - showVersion := flag.Bool("version", false, "Print version and exit") - flag.BoolVar(showVersion, "V", false, "Print version and exit") + // Command line flags + showVersion := flag.Bool("version", false, "Print version and exit") + flag.BoolVar(showVersion, "V", false, "Print version and exit") concurrency := flag.Int("u", 1, "Number of parallel virtual parallel users") iterations := flag.Int("i", 1, "Number of iterations") runtime := flag.Int("r", 0, "Runtime duration in seconds (0 means use iterations)") @@ -29,12 +29,12 @@ func main() { offset := flag.Int("offset", 0, "Max random startup delay per VU in milliseconds") requestFile := flag.String("f", "", ".http file containing http requests") envFile := flag.String("e", "", ".env file containing environment variables") - reportFormat := flag.String("report", "console", "Report format: console, html, csv, json") - reportOutput := flag.String("output", "results", "Output directory for results and reports") - reportDetail := flag.String("detail", "summary", "Report detail level: summary, goroutine, iteration, full") - verbose := flag.Bool("v", false, "Verbose mode: print request result JSON for each request") - rawFile := flag.String("raw", "", "Path to raw results .jsonl file to generate report without executing") - csvShortcut := flag.Bool("csv", false, "Shorthand to output CSV from -raw to stdout (forces -report=csv, -detail=summary)") + reportFormat := flag.String("report", "console", "Report format: console, html, csv, json") + reportOutput := flag.String("output", "results", "Output directory for results and reports") + reportDetail := flag.String("detail", "summary", "Report detail level: summary, goroutine, iteration, full") + verbose := flag.Bool("v", false, "Verbose mode: print request result JSON for each request") + rawFile := flag.String("raw", "", "Path to raw results .jsonl file to generate report without executing") + csvShortcut := flag.Bool("csv", false, "Shorthand to output CSV from -raw to stdout (forces -report=csv, -detail=summary)") flag.Parse() @@ -43,25 +43,25 @@ func main() { return } - // Offline reporting mode: when -raw is provided, skip execution - offlineMode := *rawFile != "" - if !offlineMode && *requestFile == "" { - fmt.Println("Error: -f flag is required (or provide -raw to read a raw results file)") - os.Exit(1) - } - - // If -csv shorthand is provided, force CSV formatting and summary detail; - // also prefer printing to stdout for easy redirection. - forceStdout := false - if *csvShortcut { - *reportFormat = string(types.FormatCSV) - *reportDetail = string(types.DetailSummary) - forceStdout = true - if !offlineMode { - fmt.Println("Error: -csv requires -raw to be provided") - os.Exit(1) - } - } + // Offline reporting mode: when -raw is provided, skip execution + offlineMode := *rawFile != "" + if !offlineMode && *requestFile == "" { + fmt.Println("Error: -f flag is required (or provide -raw to read a raw results file)") + os.Exit(1) + } + + // If -csv shorthand is provided, force CSV formatting and summary detail; + // also prefer printing to stdout for easy redirection. + forceStdout := false + if *csvShortcut { + *reportFormat = string(types.FormatCSV) + *reportDetail = string(types.DetailSummary) + forceStdout = true + if !offlineMode { + fmt.Println("Error: -csv requires -raw to be provided") + os.Exit(1) + } + } // Validate runtime vs iterations parameters (only for execution mode) if !offlineMode { @@ -95,10 +95,10 @@ func main() { os.Exit(1) } - // If offline mode, build report from raw file and exit - if offlineMode { - // Build a FileReporter on the provided raw file - fr := reporting.NewFileReporter(*rawFile) + // If offline mode, build report from raw file and exit + if offlineMode { + // Build a FileReporter on the provided raw file + fr := reporting.NewFileReporter(*rawFile) var reportContent string var err error @@ -129,14 +129,14 @@ func main() { os.Exit(1) } - // Output handling mirrors execution mode - format := types.ReportFormat(strings.ToLower(*reportFormat)) - if format == types.FormatConsole || forceStdout { - fmt.Print(reportContent) - } else { - // Ensure output directory exists - if err := os.MkdirAll(*reportOutput, 0755); err != nil { - fmt.Printf("Error ensuring output directory: %v\n", err) + // Output handling mirrors execution mode + format := types.ReportFormat(strings.ToLower(*reportFormat)) + if format == types.FormatConsole || forceStdout { + fmt.Print(reportContent) + } else { + // Ensure output directory exists + if err := os.MkdirAll(*reportOutput, 0755); err != nil { + fmt.Printf("Error ensuring output directory: %v\n", err) os.Exit(1) } filename := filepath.Join(*reportOutput, "report") diff --git a/cmd/httprunner/main_test.go b/cmd/httprunner/main_test.go index 0aa593c..8b814d8 100644 --- a/cmd/httprunner/main_test.go +++ b/cmd/httprunner/main_test.go @@ -152,29 +152,29 @@ func TestRunnerExitsOnInvalidDetailLevel(t *testing.T) { } func TestOfflineCSVShortcutPrintsToStdout(t *testing.T) { - dir := t.TempDir() - // Write minimal JSONL with a single RequestResult - jsonl := `{"Name":"Req1","Verb":"GET","URL":"http://example.com","StatusCode":200,"ResponseTime":100000000,"Success":true,"Error":"","Timestamp":"2024-01-01T00:00:00Z","VirtualUserID":1,"IterationID":1,"Checks":[]}` - p := filepath.Join(dir, "results.jsonl") - if err := os.WriteFile(p, []byte(jsonl+"\n"), 0644); err != nil { - t.Fatalf("write jsonl: %v", err) - } - - // Isolate flags - oldFS := flag.CommandLine - flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) - flag.CommandLine.SetOutput(new(strings.Builder)) - defer func() { flag.CommandLine = oldFS }() - - oldArgs := os.Args - os.Args = []string{"httprunner", "-raw", p, "-csv"} - defer func() { os.Args = oldArgs }() - - out := captureStdout(func() { main() }) - if !strings.Contains(out, "Index,Name,Method,URL,Success,StatusCode,ResponseTime,Error,CheckFailures,Timestamp") { - t.Fatalf("expected CSV header in output; got:\n%s", out) - } - if !strings.Contains(out, "Req1") { - t.Fatalf("expected CSV row with request name; got:\n%s", out) - } + dir := t.TempDir() + // Write minimal JSONL with a single RequestResult + jsonl := `{"Name":"Req1","Verb":"GET","URL":"http://example.com","StatusCode":200,"ResponseTime":100000000,"Success":true,"Error":"","Timestamp":"2024-01-01T00:00:00Z","VirtualUserID":1,"IterationID":1,"Checks":[]}` + p := filepath.Join(dir, "results.jsonl") + if err := os.WriteFile(p, []byte(jsonl+"\n"), 0644); err != nil { + t.Fatalf("write jsonl: %v", err) + } + + // Isolate flags + oldFS := flag.CommandLine + flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) + flag.CommandLine.SetOutput(new(strings.Builder)) + defer func() { flag.CommandLine = oldFS }() + + oldArgs := os.Args + os.Args = []string{"httprunner", "-raw", p, "-csv"} + defer func() { os.Args = oldArgs }() + + out := captureStdout(func() { main() }) + if !strings.Contains(out, "Index,Name,Method,URL,Success,StatusCode,ResponseTime,Error,CheckFailures,Timestamp") { + t.Fatalf("expected CSV header in output; got:\n%s", out) + } + if !strings.Contains(out, "Req1") { + t.Fatalf("expected CSV row with request name; got:\n%s", out) + } }