diff --git a/internal/adapters/scriptorium/runner.go b/internal/adapters/scriptorium/runner.go index 719c933..46d54a7 100644 --- a/internal/adapters/scriptorium/runner.go +++ b/internal/adapters/scriptorium/runner.go @@ -105,29 +105,20 @@ func (r Runner) Render(ctx context.Context, req RenderRequest) (*RenderResult, e if req.DataPackagePath == "" { return nil, fmt.Errorf("data package path is required") } - binary := r.Binary - if binary == "" { - binary = "scriptorium" - } - commands := r.Commands - if commands == nil { - commands = ExecRunner{} - } - args := r.renderArgs(req) - commandResult, err := commands.Run(ctx, binary, args, r.Timeout) + execution, err := r.execute(ctx, r.renderArgs(req)) if err != nil { return nil, fmt.Errorf("run scriptorium render: %w", err) } result := &RenderResult{ - Command: append([]string{binary}, args...), - Stdout: string(commandResult.Stdout), - Stderr: string(commandResult.Stderr), - StdoutTruncated: commandResult.StdoutTruncated, - StderrTruncated: commandResult.StderrTruncated, - ExitCode: commandResult.ExitCode, + Command: execution.argv(), + Stdout: string(execution.result.Stdout), + Stderr: string(execution.result.Stderr), + StdoutTruncated: execution.result.StdoutTruncated, + StderrTruncated: execution.result.StderrTruncated, + ExitCode: execution.result.ExitCode, } - if commandResult.ExitCode != 0 { - return result, fmt.Errorf("scriptorium render exited with code %d: %s", commandResult.ExitCode, result.Stderr) + if execution.result.ExitCode != 0 { + return result, fmt.Errorf("scriptorium render exited with code %d: %s", execution.result.ExitCode, result.Stderr) } return result, nil } @@ -142,6 +133,32 @@ func (r Runner) Run(ctx context.Context, req RunRequest) (*RunResult, error) { if req.OutputPath == "" { return nil, fmt.Errorf("output path is required") } + execution, err := r.execute(ctx, r.runArgs(req)) + if err != nil { + return nil, fmt.Errorf("run scriptorium: %w", err) + } + result := &RunResult{ + Command: execution.argv(), + Stdout: string(execution.result.Stdout), + Stderr: string(execution.result.Stderr), + StdoutTruncated: execution.result.StdoutTruncated, + StderrTruncated: execution.result.StderrTruncated, + ExitCode: execution.result.ExitCode, + OutputPath: req.OutputPath, + } + if execution.result.ExitCode != 0 { + return result, fmt.Errorf("scriptorium run exited with code %d: %s", execution.result.ExitCode, result.Stderr) + } + return result, nil +} + +type execution struct { + binary string + args []string + result CommandResult +} + +func (r Runner) execute(ctx context.Context, args []string) (execution, error) { binary := r.Binary if binary == "" { binary = "scriptorium" @@ -150,24 +167,15 @@ func (r Runner) Run(ctx context.Context, req RunRequest) (*RunResult, error) { if commands == nil { commands = ExecRunner{} } - args := r.runArgs(req) - commandResult, err := commands.Run(ctx, binary, args, r.Timeout) + result, err := commands.Run(ctx, binary, args, r.Timeout) if err != nil { - return nil, fmt.Errorf("run scriptorium: %w", err) + return execution{}, err } - result := &RunResult{ - Command: append([]string{binary}, args...), - Stdout: string(commandResult.Stdout), - Stderr: string(commandResult.Stderr), - StdoutTruncated: commandResult.StdoutTruncated, - StderrTruncated: commandResult.StderrTruncated, - ExitCode: commandResult.ExitCode, - OutputPath: req.OutputPath, - } - if commandResult.ExitCode != 0 { - return result, fmt.Errorf("scriptorium run exited with code %d: %s", commandResult.ExitCode, result.Stderr) - } - return result, nil + return execution{binary: binary, args: args, result: result}, nil +} + +func (e execution) argv() []string { + return append([]string{e.binary}, e.args...) } func (r Runner) renderArgs(req RenderRequest) []string { diff --git a/internal/cli/root.go b/internal/cli/root.go index 441eb97..a274062 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" + "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) @@ -184,12 +185,12 @@ func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) { if len(args) == 0 { return app.GenerateRequest{}, fmt.Errorf("generate requires a report name") } - report, ok := reportKind(args[0]) + reportKind, ok := reportKind(args[0]) if !ok { return app.GenerateRequest{}, fmt.Errorf("unknown generate report %q", args[0]) } - opts, err := parseGenerateFlags(report, args[1:]) + opts, err := parseGenerateFlags(reportKind, args[1:]) if err != nil { return app.GenerateRequest{}, err } @@ -208,12 +209,12 @@ func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) { req := app.GenerateRequest{ Config: cfg, - Report: report, + Report: reportKind, OutputPath: opts.Output, Now: r.Clock.Now(), } - switch report { + switch reportKind { case app.ReportDaily: if opts.Date == "" { req.Date = timeutil.LocalDate(r.Clock.Now(), location) @@ -230,17 +231,12 @@ func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) { if opts.End == "" { return app.GenerateRequest{}, fmt.Errorf("generate storm requires --end") } - req.StormStart, err = timeutil.ParseStormTime(opts.Start, location) + period, err := report.ParseStormPeriod(opts.Start, opts.End, location) if err != nil { return app.GenerateRequest{}, err } - req.StormEnd, err = timeutil.ParseStormTime(opts.End, location) - if err != nil { - return app.GenerateRequest{}, err - } - if !req.StormEnd.After(req.StormStart) { - return app.GenerateRequest{}, fmt.Errorf("generate storm requires --end after --start") - } + req.StormStart = period.Start + req.StormEnd = period.End } return req, nil diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 6bfb48c..1425ad7 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -682,7 +682,15 @@ func TestResolveGenerateAppliesSharedFlags(t *testing.T) { func TestResolveGenerateStormRequiresStartAndEnd(t *testing.T) { runner := Runner{Clock: fixedClock()} - _, err := runner.resolveGenerate([]string{"storm", "--start", "2026-05-29T18:00"}) + _, err := runner.resolveGenerate([]string{"storm", "--end", "2026-05-29T18:00"}) + if err == nil { + t.Fatal("resolveGenerate() error = nil, want missing start error") + } + if !strings.Contains(err.Error(), "requires --start") { + t.Fatalf("error = %q, want missing start", err.Error()) + } + + _, err = runner.resolveGenerate([]string{"storm", "--start", "2026-05-29T18:00"}) if err == nil { t.Fatal("resolveGenerate() error = nil, want missing end error") } @@ -691,6 +699,26 @@ func TestResolveGenerateStormRequiresStartAndEnd(t *testing.T) { } } +func TestResolveGenerateStormParsesLocalTimestamps(t *testing.T) { + runner := Runner{Clock: fixedClock()} + + req, err := runner.resolveGenerate([]string{ + "storm", + "--tz", "America/Chicago", + "--start", "2026-05-29T18:00", + "--end", "2026-05-30T06:00", + }) + if err != nil { + t.Fatalf("resolveGenerate() error = %v", err) + } + if got := req.StormStart.Format(time.RFC3339); got != "2026-05-29T18:00:00-05:00" { + t.Fatalf("StormStart = %q, want local Chicago time", got) + } + if got := req.StormEnd.Format(time.RFC3339); got != "2026-05-30T06:00:00-05:00" { + t.Fatalf("StormEnd = %q, want local Chicago time", got) + } +} + func TestResolveGenerateStormParsesRFC3339(t *testing.T) { runner := Runner{Clock: fixedClock()} @@ -707,6 +735,22 @@ func TestResolveGenerateStormParsesRFC3339(t *testing.T) { } } +func TestResolveGenerateStormRejectsInvalidBounds(t *testing.T) { + runner := Runner{Clock: fixedClock()} + + _, err := runner.resolveGenerate([]string{ + "storm", + "--start", "2026-05-30T06:00", + "--end", "2026-05-29T18:00", + }) + if err == nil { + t.Fatal("resolveGenerate() error = nil, want invalid bounds error") + } + if !strings.Contains(err.Error(), "end time after start time") { + t.Fatalf("error = %q, want invalid bounds context", err.Error()) + } +} + func TestResolveRunCommands(t *testing.T) { tests := []struct { name string