Skip to content
quarry Artifact [84abb74d09]
Login

Artifact [84abb74d09]

Artifact 84abb74d09e68543ae95dd690fac2c8e59cd93c3e3962fe2a40efc9baf4b6995:


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