// Package check provides lightweight test helpers to reduce boilerplate in // table-driven tests. // // Error-family functions (NoError, Error, ErrorContains) accept either a plain // error or a result.Expect[T] value — the package extracts the error from // whichever type it receives. // // Every helper calls t.Helper() so failures are reported at the call site. package check import ( "reflect" "strings" "testing" "gitea.djmil.dev/go/template/pkg/result" ) // extractErr pulls an error out of v, which must be error or result.Expect[T]. // Fatally fails the test for any other type. func extractErr(t *testing.T, v any) error { t.Helper() if v == nil { return nil } switch x := v.(type) { case error: return x case interface{ Err() error }: return x.Err() default: t.Fatalf("check: unsupported type %T (want error or result.Expect[T])", v) return nil } } // NoError fails the test if v contains a non-nil error. func NoError(t *testing.T, v any) { t.Helper() if err := extractErr(t, v); err != nil { t.Fatalf("unexpected error: %v", err) } } // Error fails the test if v contains no error. func Error(t *testing.T, v any) { t.Helper() if extractErr(t, v) == nil { t.Fatal("expected error, got nil") } } // ErrorContains fails the test if v contains no error or its message does not // contain substr. func ErrorContains(t *testing.T, v any, substr string) { t.Helper() err := extractErr(t, v) if err == nil { t.Fatal("expected error, got nil") return } if !strings.Contains(err.Error(), substr) { t.Errorf("error %q does not contain %q", err.Error(), substr) } } // Equal fails the test if got != want. func Equal[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v, want %v", got, want) } } // NotEqual fails the test if got == want. func NotEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got == want { t.Errorf("expected values to differ, got %v", got) } } // DeepEqual fails the test if got and want are not deeply equal. // Use instead of Equal for maps, slices, and structs with slice fields. func DeepEqual(t *testing.T, got, want any) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } // ElementsMatch fails the test if got and want do not contain the same // elements regardless of order, including duplicates. T must be comparable — // for slices or maps as elements, use DeepEqual after sorting manually. func ElementsMatch[T comparable](t *testing.T, got, want []T) { t.Helper() if len(got) != len(want) { t.Errorf("length mismatch: got %d elements, want %d", len(got), len(want)) return } freq := make(map[T]int, len(want)) for _, v := range want { freq[v]++ } for _, v := range got { if freq[v]--; freq[v] < 0 { t.Errorf("unexpected element: %v", v) return } } } // Ok fails the test if r holds an error, then returns the value. func Ok[T any](t *testing.T, r result.Expect[T]) T { t.Helper() if r.Err() != nil { t.Fatalf("unexpected error: %v", r.Err()) } return r.Value() } // OkNotNil fails the test if r holds an error or its value is the zero value. // T must be a pointer or comparable type where zero means absent. func OkNotNil[T comparable](t *testing.T, r result.Expect[T]) T { t.Helper() v := Ok(t, r) var zero T if v == zero { t.Fatal("expected non-nil value, got zero") } return v }