package fault
import (
"errors"
"strings"
"testing"
)
// A fault is an ordinary error first: it wraps, it compares, and it
// says everything it knows on one line for a log file.
func TestFaultIsAnOrdinaryError(t *testing.T) {
cause := errors.New("dial tcp: connection refused")
f := Wrap(cause, "notify is unreachable").
Cost("no mentions or alerts are delivered").
With("endpoint", "127.0.0.1:3000", "attempt", 3)
if !errors.Is(f, cause) {
t.Error("the cause is not reachable through the fault")
}
got := f.Error()
for _, want := range []string{"notify is unreachable", "endpoint=127.0.0.1:3000",
"attempt=3", cause.Error()} {
if !strings.Contains(got, want) {
t.Errorf("Error() lost %q: %s", want, got)
}
}
if Of(f) != f {
t.Error("Of did not find the fault it was given")
}
// Wrapped further up, the structure is still reachable — which is
// the whole point of putting it in the error rather than beside it.
if Of(errors.New("plain")) != nil {
t.Error("Of invented a fault")
}
buried := Wrap(f, "settings page failed")
if Of(buried).Msg != "settings page failed" {
t.Error("Of skipped the outermost fault")
}
}
// Nil is the happy path: `return fault.Wrap(err, …)` must not turn a
// success into a failure, and the builders must not panic on it.
func TestWrapNilStaysNil(t *testing.T) {
if f := Wrap(nil, "nothing went wrong"); f != nil {
t.Fatalf("wrapping nil produced %v", f)
}
var f *Fault
if f.Cost("x").Do("y").With("k", "v") != nil || f.Error() != "" || f.Keys() != nil {
t.Error("a nil fault did not stay quiet")
}
}
// An odd trailing key keeps its place with an empty value: losing an
// operator's context to a typo is the wrong trade.
func TestOddFieldKeepsItsKey(t *testing.T) {
f := New("x").With("lane", "ingest", "queued")
if len(f.Fields) != 2 || f.Fields[1].Key != "queued" || f.Fields[1].Value != "" {
t.Errorf("fields %+v", f.Fields)
}
if strings.Join(f.Keys(), " ") != "lane queued" {
t.Errorf("keys %v", f.Keys())
}
}
// Grouping is the reason the journal showed one outage as six rows.
// Everything that varies per occurrence — a name, a URL, a hash, a
// number — is not what identifies the fault.
func TestNormalizeGroupsOneFaultIntoOneRow(t *testing.T) {
same := [][]string{{
`notify: ensure workflow "quarry-mention": Put "http://127.0.0.1:3000/v1/workflows/quarry-mention": dial tcp: refused`,
`notify: ensure workflow "quarry-landing": Put "http://127.0.0.1:3000/v1/workflows/quarry-landing": dial tcp: refused`,
}, {
`unhandledrejection: FetchFailed POST /r/quarry/chat`,
`unhandledrejection: FetchFailed POST /r/synthetic/chat`,
}, {
`zit tag add ci-fail 828de7a3a188cf8ee093a5731db3dd18: exit status 1`,
`zit tag add ci-fail 5b1c0f9d77e2aa4419bb0c8e3f21d704: exit status 1`,
}}
for _, pair := range same {
if a, b := Normalize(pair[0]), Normalize(pair[1]); a != b {
t.Errorf("two occurrences of one fault stayed apart:\n %s\n %s", a, b)
}
}
// Over-grouping is the opposite failure and just as bad: two real
// faults must not collapse into one row.
apart := [][]string{
{"notify is unreachable", "dagnats is unreachable"},
{"ingest lane is full", "render lane is full"},
}
for _, pair := range apart {
if a, b := Normalize(pair[0]), Normalize(pair[1]); a == b {
t.Errorf("two different faults collapsed into %q", a)
}
}
}