diff --git a/cmd/report.go b/cmd/report.go index 73114e3..b268ea4 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/url" + "strings" "time" "github.com/jongio/grut/internal/crashlog" @@ -140,9 +141,13 @@ func writeCrashReportListJSON(w io.Writer, reports []*crashlog.CrashReport) erro } func runReportShow(id string) error { - report, err := crashlog.Read(id) + reports, err := crashlog.List() + if err != nil { + return fmt.Errorf("listing crash reports: %w", err) + } + report, err := findCrashReport(id, reports) if err != nil { - return fmt.Errorf("reading crash report %q: %w", id, err) + return err } data, err := json.MarshalIndent(report, "", " ") @@ -153,6 +158,33 @@ func runReportShow(id string) error { return nil } +func findCrashReport(id string, reports []*crashlog.CrashReport) (*crashlog.CrashReport, error) { + id = strings.TrimSpace(id) + if id == "" { + return nil, fmt.Errorf("crash report ID is required") + } + for _, report := range reports { + if report.ID == id { + return report, nil + } + } + + var matches []*crashlog.CrashReport + for _, report := range reports { + if strings.HasPrefix(report.ID, id) { + matches = append(matches, report) + } + } + switch len(matches) { + case 1: + return matches[0], nil + case 0: + return nil, fmt.Errorf("crash report with id %q not found", id) + default: + return nil, fmt.Errorf("crash report id %q matches %d reports; use a longer ID", id, len(matches)) + } +} + func runReportLatest(noBrowser bool) error { reports, err := crashlog.List() if err != nil { diff --git a/cmd/report_test.go b/cmd/report_test.go index 5e46b41..5b4b397 100644 --- a/cmd/report_test.go +++ b/cmd/report_test.go @@ -196,6 +196,51 @@ func TestRunReport_ShowFlag_NonexistentID(t *testing.T) { assert.Error(t, err) } +func TestFindCrashReport_ExactID(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "123400000"}, + } + + report, err := findCrashReport("123456789", reports) + + require.NoError(t, err) + assert.Equal(t, "123456789", report.ID) +} + +func TestFindCrashReport_UnambiguousPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "987654321"}, + } + + report, err := findCrashReport("1234", reports) + + require.NoError(t, err) + assert.Equal(t, "123456789", report.ID) +} + +func TestFindCrashReport_AmbiguousPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "123400000"}, + } + + _, err := findCrashReport("1234", reports) + + require.Error(t, err) + assert.Contains(t, err.Error(), "matches 2 reports") +} + +func TestFindCrashReport_MissingPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{{ID: "123456789"}} + + _, err := findCrashReport("999", reports) + + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + func TestRunReport_JSONWithoutListReturnsError(t *testing.T) { cmd := newReportCmd() cmd.SetArgs([]string{"--json"}) diff --git a/internal/crashlog/crashlog_test.go b/internal/crashlog/crashlog_test.go index c6046a6..8745ad4 100644 --- a/internal/crashlog/crashlog_test.go +++ b/internal/crashlog/crashlog_test.go @@ -136,7 +136,7 @@ func TestFormatGitHubIssueBody(t *testing.T) { } // --------------------------------------------------------------------------- -// Write / Read / List / Clear / PruneOld +// Write / List / Clear / PruneOld // --------------------------------------------------------------------------- func TestWriteAndRead(t *testing.T) { @@ -147,10 +147,11 @@ func TestWriteAndRead(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, path) - got, err := Read(r.ID) + reports, err := List() require.NoError(t, err) - assert.Equal(t, r.PanicValue, got.PanicValue) - assert.Equal(t, r.ID, got.ID) + require.Len(t, reports, 1) + assert.Equal(t, r.PanicValue, reports[0].PanicValue) + assert.Equal(t, r.ID, reports[0].ID) } func TestWriteAndList(t *testing.T) { @@ -602,27 +603,6 @@ func TestList_MalformedJSON(t *testing.T) { assert.Equal(t, "valid", reports[0].PanicValue) } -func TestRead_NotFound(t *testing.T) { - setTestDataHome(t) - - // Write a report so the directory exists. - r := NewReport("exists", []byte("stack"), "ctx") - _, err := Write(r) - require.NoError(t, err) - - _, err = Read("nonexistent-id") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -func TestRead_NoDirectory(t *testing.T) { - setTestDataHome(t) - - _, err := Read("any-id") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no crash reports found") -} - func TestClear_EmptyDirectory(t *testing.T) { setTestDataHome(t) diff --git a/internal/crashlog/reader.go b/internal/crashlog/reader.go index 0515dbb..89c3c43 100644 --- a/internal/crashlog/reader.go +++ b/internal/crashlog/reader.go @@ -54,43 +54,6 @@ func List() ([]*CrashReport, error) { return reports, nil } -// Read loads a single crash report by its ID. It returns an error if no -// report with the given ID exists. -func Read(id string) (*CrashReport, error) { - if len(id) > 64 { - return nil, fmt.Errorf("invalid crash report ID") - } - dir := crashDir() - entries, err := os.ReadDir(dir) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("no crash reports found") - } - return nil, fmt.Errorf("reading crash directory: %w", err) - } - - for _, e := range entries { - if e.IsDir() || !strings.HasPrefix(e.Name(), "crash-") || !strings.HasSuffix(e.Name(), ".json") { - continue - } - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - slog.Warn("skipping unreadable crash report", "file", e.Name(), "error", err) - continue - } - var r CrashReport - if err := json.Unmarshal(data, &r); err != nil { - slog.Warn("skipping malformed crash report", "file", e.Name(), "error", err) - continue - } - if r.ID == id { - return &r, nil - } - } - - return nil, fmt.Errorf("crash report with id %q not found", id) -} - // Clear deletes all crash report files from the crash directory and // returns the number of files removed. func Clear() (int, error) {