Skip to content
quarry Artifact [1abc968536]
Login

Artifact [1abc968536]

Artifact 1abc968536ca1de8c16dd55a02dc91796d63a73bf556d62ec0babc90efe02b48:


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")
	}
}