package errlog
import (
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/fault"
"craftdesign.group/quarry/internal/work"
)
// The journal reads the whole chain, not just the outermost fault.
//
// This is the ingest-lane case exactly. work.busy() attaches lane,
// running, limit, waiting, queue and shed — "the numbers that make the
// refusal actionable", by its own comment — and states what to do about
// it. The poll loop wraps that in "a store could not be ingested" with
// the repository's name. The panel showed the name, none of the
// numbers, and no remedy: ten thousand occurrences of a capacity signal
// that never said what the capacity was.
func TestTheJournalKeepsAWrappedFaultsContext(t *testing.T) {
inner := fault.Wrap(work.ErrBusy, "ingest lane is full").
Cost("work is being refused rather than queued").
Do("raise this lane's limit or queue, or add a node").
With("lane", "ingest", "running", 3, "limit", 3, "shed", 9871)
outer := fault.Wrap(inner, "a store could not be ingested").
Cost("this store's new check-ins are not searchable or listed").
With("repo", "tcl")
r := New(10)
r.Report("ingest", "ingest tcl", outer)
got := r.Recent(1)
if len(got) != 1 {
t.Fatal("FAIL: nothing was journalled")
}
e := got[0]
keys := map[string]string{}
for _, f := range e.Fields {
keys[f.Key] = f.Value
}
for _, want := range []string{"repo", "lane", "running", "limit", "shed"} {
if _, ok := keys[want]; !ok {
t.Errorf("FAIL: the entry lost the %q field: %v", want, keys)
}
}
if keys["shed"] != "9871" {
t.Errorf("FAIL: shed = %q, want the lane's own count", keys["shed"])
}
// The outer fault's sentence is the row's title; the remedy is
// inherited, because the layer that knows what to do is the lane.
if e.Message != "a store could not be ingested" {
t.Errorf("FAIL: message = %q", e.Message)
}
if !strings.Contains(e.Fix, "raise this lane's limit") {
t.Errorf("FAIL: the row carries no remedy: %q", e.Fix)
}
// The cost is the outermost one: it is the layer nearest the reader.
if !strings.Contains(e.Why, "not searchable") {
t.Errorf("FAIL: why = %q, want the wrapper's own cost", e.Why)
}
}
// A collision keeps the wrapper's value: it is closer to the site the
// reader is looking at.
func TestTheOuterFaultWinsAKeyCollision(t *testing.T) {
inner := fault.New("inner").With("repo", "from-the-inside")
outer := fault.Wrap(inner, "outer").With("repo", "from-the-outside")
got := fault.FieldsOf(outer)
n := 0
for _, f := range got {
if f.Key == "repo" {
n++
if f.Value != "from-the-outside" {
t.Errorf("FAIL: repo = %q, want the wrapper's", f.Value)
}
}
}
if n != 1 {
t.Errorf("FAIL: %d repo fields, want one", n)
}
}
// A plain error carries nothing, and asking must not panic.
func TestAPlainErrorHasNoContext(t *testing.T) {
err := errors.New("just an error")
if got := fault.FieldsOf(err); len(got) != 0 {
t.Errorf("FAIL: %v", got)
}
if fault.WhyOf(err) != "" || fault.FixOf(err) != "" {
t.Error("FAIL: a plain error invented a cost or a remedy")
}
if fault.FieldsOf(nil) != nil {
t.Error("FAIL: nil gave fields")
}
}