package oltp
import (
"path/filepath"
"testing"
"time"
"craftdesign.group/quarry/internal/fault"
)
func excDB(t *testing.T) *DB {
t.Helper()
d, err := Open(filepath.Join(t.TempDir(), "t.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { d.Close() })
return d
}
// The whole point of persisting: the panel can answer for more than the
// process that is running now.
func TestExceptionSurvivesAndCounts(t *testing.T) {
d := excDB(t)
first := time.Now().Add(-time.Hour)
e := Exception{Sig: "abc12345", Message: "boom", Surface: "web", Path: "/x",
Count: 1, First: first, Last: first}
if err := d.RecordException(e); err != nil {
t.Fatal(err)
}
e.Last = time.Now()
if err := d.RecordException(e); err != nil {
t.Fatal(err)
}
got, err := d.Exceptions(10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("stored %d rows, want 1 — a repeat is the same signature", len(got))
}
if got[0].Count != 2 {
t.Errorf("count %d, want 2", got[0].Count)
}
// The FIRST sighting keeps its date; only the clock on "last" moves.
if !got[0].First.Truncate(time.Second).Equal(first.UTC().Truncate(time.Second)) {
t.Errorf("first moved to %v, want %v", got[0].First, first.UTC())
}
sigs, occ, err := d.ExceptionTotals()
if err != nil {
t.Fatal(err)
}
if sigs != 1 || occ != 2 {
t.Errorf("totals = %d signatures / %d occurrences, want 1/2", sigs, occ)
}
}
// An error that comes back is not resolved, whatever anybody clicked.
func TestRecurrenceReopensAResolvedException(t *testing.T) {
d := excDB(t)
now := time.Now()
e := Exception{Sig: "s", Message: "flaky", Surface: "ingest", Path: "/y",
Count: 1, First: now, Last: now}
if err := d.RecordException(e); err != nil {
t.Fatal(err)
}
if err := d.ResolveException("s"); err != nil {
t.Fatal(err)
}
got, _ := d.Exceptions(10)
if !got[0].Resolved {
t.Fatal("resolve did not take")
}
e.Last = now.Add(time.Minute)
if err := d.RecordException(e); err != nil {
t.Fatal(err)
}
got, _ = d.Exceptions(10)
if got[0].Resolved {
t.Error("a signature that recurred is still marked resolved")
}
}
// The context a reader acts on has to survive a restart, and it has to
// describe the LATEST occurrence: a queue depth from an hour ago is not
// the number anyone is about to act on. The trace is the exception —
// it names the code path, which does not move.
func TestExceptionKeepsLatestContextAndFirstTrace(t *testing.T) {
d := excDB(t)
first := Exception{
Sig: "abc12345", Message: "ingest lane is full", Surface: "ingest",
Path: "ingest sqlite-docsrc", Count: 1, Trace: "the first stack",
First: time.Now().Add(-time.Hour), Last: time.Now().Add(-time.Hour),
Why: "check-ins wait to be indexed", Fix: "raise the limit",
Build: "1.0.0", Req: "9f",
Fields: []fault.Field{{Key: "queued", Value: "4"}},
}
if err := d.RecordException(first); err != nil {
t.Fatal(err)
}
later := first
later.Count, later.Trace, later.Build, later.Req = 1, "a later stack", "1.0.1", "aa"
later.Fields = []fault.Field{{Key: "queued", Value: "32"}}
later.Last = time.Now()
if err := d.RecordException(later); err != nil {
t.Fatal(err)
}
got, err := d.Exceptions(10)
if err != nil || len(got) != 1 {
t.Fatalf("rows %d err %v — one fault must stay one row", len(got), err)
}
e := got[0]
if e.Count != 2 {
t.Errorf("count %d, want both occurrences", e.Count)
}
if e.Trace != "the first stack" {
t.Errorf("trace %q: the first stack names the code path and should be kept", e.Trace)
}
if len(e.Fields) != 1 || e.Fields[0].Value != "32" {
t.Errorf("fields %v, want the latest occurrence's numbers", e.Fields)
}
if e.Build != "1.0.1" || e.Req != "aa" {
t.Errorf("build %q req %q, want the latest", e.Build, e.Req)
}
if e.Why == "" || e.Fix == "" {
t.Error("what it costs and what to do did not survive the restart")
}
}