diff --git a/alias_typed_test.go b/alias_typed_test.go new file mode 100644 index 0000000..c7663a5 --- /dev/null +++ b/alias_typed_test.go @@ -0,0 +1,37 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "testing" +) + +func Test_string_NotContains_success(t *testing.T) { + gunit.String(t, "Hello").NotContains("bye") +} + +func Test_string_NotContains_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.String(aSpy, "Hello").NotContains("ell") + aSpy.HadErrorContaining(t, "not to contain") +} + +func Test_string_NotEmpty_success(t *testing.T) { + gunit.String(t, "Hello").NotEmpty() +} + +func Test_string_NotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.String(aSpy, "").NotEmpty() + aSpy.HadErrorContaining(t, "was not") +} + +func Test_number_NotEqual_success(t *testing.T) { + gunit.Number(t, 1).NotEqual(2) +} + +func Test_number_NotEqual_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Number(aSpy, 1).NotEqual(1) + aSpy.HadErrorContaining(t, "not equal") +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..93ed3ad --- /dev/null +++ b/error.go @@ -0,0 +1,95 @@ +package gunit + +import ( + "errors" + "regexp" + "strings" +) + +func NilError(t T, err error) { + t.Helper() + if err != nil { + t.Errorf("got <%v>, want nil error", err) + } +} + +func Error(t T, err error) { + t.Helper() + if err == nil { + t.Errorf("got nil, want error") + } +} + +func EqualError(t T, err error, expected string) { + t.Helper() + if err == nil { + t.Errorf("got nil error, want error message <%s>", expected) + return + } + if err.Error() != expected { + t.Errorf("got error message <%s>, want <%s>", err.Error(), expected) + } +} + +func ErrorContains(t T, err error, expected string) { + t.Helper() + if err == nil { + t.Errorf("got nil error, want error containing <%s>", expected) + return + } + if !strings.Contains(err.Error(), expected) { + t.Errorf("got error message <%s>, want containing <%s>", err.Error(), expected) + } +} + +func ErrorMatchesRegexp(t T, err error, pattern string) { + t.Helper() + if err == nil { + t.Errorf("got nil error, want error matching regexp <%s>", pattern) + return + } + re, compileErr := regexp.Compile(pattern) + if compileErr != nil { + t.Errorf("invalid regexp <%s>: %v", pattern, compileErr) + return + } + if !re.MatchString(err.Error()) { + t.Errorf("got error message <%s>, want regexp <%s>", err.Error(), pattern) + } +} + +func ErrorIs(t T, err error, target error) { + t.Helper() + if !errors.Is(err, target) { + t.Errorf("got <%v>, want error matching <%v>", err, target) + } +} + +func NotErrorIs(t T, err error, target error) { + t.Helper() + if errors.Is(err, target) { + t.Errorf("got <%v>, want error not matching <%v>", err, target) + } +} + +func ErrorAs[E any](t T, err error, target *E) { + t.Helper() + if !errors.As(err, target) { + t.Errorf("got <%v>, want error assignable to <%T>", err, target) + } +} + +func NotErrorAs[E any](t T, err error, target *E) { + t.Helper() + if errors.As(err, target) { + t.Errorf("got <%v>, want error not assignable to <%T>", err, target) + } +} + +func ErrorType[E error](t T, err error) { + t.Helper() + var target E + if !errors.As(err, &target) { + t.Errorf("got <%v>, want error assignable to <%T>", err, target) + } +} diff --git a/error_typed_test.go b/error_typed_test.go new file mode 100644 index 0000000..cbd75f9 --- /dev/null +++ b/error_typed_test.go @@ -0,0 +1,147 @@ +package gunit_test + +import ( + "errors" + "fmt" + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "testing" +) + +func Test_error_NilError_success(t *testing.T) { + gunit.NilError(t, nil) +} + +func Test_error_NilError_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.NilError(aSpy, errors.New("sentinel")) + aSpy.HadErrorContaining(t, "want nil error") +} + +func Test_error_Error_success(t *testing.T) { + gunit.Error(t, errors.New("sentinel")) +} + +func Test_error_Error_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Error(aSpy, nil) + aSpy.HadErrorContaining(t, "want error") +} + +func Test_error_EqualError_success(t *testing.T) { + gunit.EqualError(t, errors.New("sentinel"), "sentinel") +} + +func Test_error_EqualError_failure_for_nil(t *testing.T) { + aSpy := eye.Spy() + gunit.EqualError(aSpy, nil, "sentinel") + aSpy.HadErrorContaining(t, "got nil error") +} + +func Test_error_EqualError_failure_for_mismatch(t *testing.T) { + aSpy := eye.Spy() + gunit.EqualError(aSpy, errors.New("wrapped sentinel"), "sentinel") + aSpy.HadErrorContaining(t, "want ") +} + +func Test_error_ErrorContains_success(t *testing.T) { + gunit.ErrorContains(t, errors.New("wrapped sentinel"), "wrapped") +} + +func Test_error_ErrorContains_failure_for_nil(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorContains(aSpy, nil, "wrapped") + aSpy.HadErrorContaining(t, "got nil error") +} + +func Test_error_ErrorContains_failure_for_mismatch(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorContains(aSpy, errors.New("wrapped sentinel"), "missing") + aSpy.HadErrorContaining(t, "want containing") +} + +func Test_error_ErrorMatchesRegexp_success(t *testing.T) { + gunit.ErrorMatchesRegexp(t, errors.New("wrapped sentinel"), "sentinel") +} + +func Test_error_ErrorMatchesRegexp_failure_for_nil(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorMatchesRegexp(aSpy, nil, "wrapped") + aSpy.HadErrorContaining(t, "got nil error") +} + +func Test_error_ErrorMatchesRegexp_failure_for_invalid_regexp(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorMatchesRegexp(aSpy, errors.New("wrapped sentinel"), "[") + aSpy.HadErrorContaining(t, "invalid regexp") +} + +func Test_error_ErrorMatchesRegexp_failure_for_mismatch(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorMatchesRegexp(aSpy, errors.New("wrapped sentinel"), "missing") + aSpy.HadErrorContaining(t, "want regexp") +} + +func Test_error_ErrorIs_success(t *testing.T) { + sentinel := errors.New("sentinel") + gunit.ErrorIs(t, fmt.Errorf("wrapped: %w", sentinel), sentinel) +} + +func Test_error_ErrorIs_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorIs(aSpy, errors.New("wrapped"), errors.New("sentinel")) + aSpy.HadErrorContaining(t, "want error matching") +} + +func Test_error_NotErrorIs_success(t *testing.T) { + gunit.NotErrorIs(t, errors.New("wrapped"), errors.New("sentinel")) +} + +func Test_error_NotErrorIs_failure(t *testing.T) { + sentinel := errors.New("sentinel") + aSpy := eye.Spy() + gunit.NotErrorIs(aSpy, sentinel, sentinel) + aSpy.HadErrorContaining(t, "want error not matching") +} + +func Test_error_ErrorAs_success(t *testing.T) { + var target *typedTestError + gunit.ErrorAs(t, &typedTestError{msg: "wrapped sentinel"}, &target) +} + +func Test_error_ErrorAs_failure(t *testing.T) { + var target *typedTestError + aSpy := eye.Spy() + gunit.ErrorAs(aSpy, errors.New("sentinel"), &target) + aSpy.HadErrorContaining(t, "want error assignable") +} + +func Test_error_NotErrorAs_success(t *testing.T) { + var target *otherTypedTestError + gunit.NotErrorAs(t, &typedTestError{msg: "wrapped sentinel"}, &target) +} + +func Test_error_NotErrorAs_failure(t *testing.T) { + var target *typedTestError + aSpy := eye.Spy() + gunit.NotErrorAs(aSpy, &typedTestError{msg: "wrapped sentinel"}, &target) + aSpy.HadErrorContaining(t, "want error not assignable") +} + +func Test_error_ErrorType_success(t *testing.T) { + gunit.ErrorType[*typedTestError](t, &typedTestError{msg: "wrapped sentinel"}) +} + +func Test_error_ErrorType_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.ErrorType[*typedTestError](aSpy, errors.New("sentinel")) + aSpy.HadErrorContaining(t, "want error assignable") +} + +type typedTestError struct{ msg string } + +func (e *typedTestError) Error() string { return e.msg } + +type otherTypedTestError struct{ msg string } + +func (e *otherTypedTestError) Error() string { return e.msg } diff --git a/filesystem.go b/filesystem.go new file mode 100644 index 0000000..45802f9 --- /dev/null +++ b/filesystem.go @@ -0,0 +1,60 @@ +package gunit + +import ( + "errors" + "os" +) + +func FileExists(t T, path string) { + t.Helper() + info, err := os.Stat(path) + if err != nil { + t.Errorf("got path <%s> unavailable: %v, wanted existing file", path, err) + return + } + if info.IsDir() { + t.Errorf("got directory <%s>, wanted existing file", path) + } +} + +func NoFileExists(t T, path string) { + t.Helper() + info, err := os.Stat(path) + if errors.Is(err, os.ErrNotExist) { + return + } + if err != nil { + t.Errorf("got path <%s> unavailable: %v, wanted no existing file", path, err) + return + } + if !info.IsDir() { + t.Errorf("got existing file <%s>, wanted no file", path) + } +} + +func DirExists(t T, path string) { + t.Helper() + info, err := os.Stat(path) + if err != nil { + t.Errorf("got path <%s> unavailable: %v, wanted existing directory", path, err) + return + } + if !info.IsDir() { + t.Errorf("got file <%s>, wanted existing directory", path) + } +} + +func NoDirExists(t T, path string) { + t.Helper() + info, err := os.Stat(path) + if errors.Is(err, os.ErrNotExist) { + return + } + if err != nil { + t.Errorf("got path <%s> unavailable: %v, wanted no existing directory", path, err) + return + } + if info.IsDir() { + t.Errorf("got existing directory <%s>, wanted no directory", path) + } +} diff --git a/filesystem_typed_test.go b/filesystem_typed_test.go new file mode 100644 index 0000000..9a7d2c7 --- /dev/null +++ b/filesystem_typed_test.go @@ -0,0 +1,77 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "path/filepath" + "testing" +) + +func Test_filesystem_FileExists_success(t *testing.T) { + _, file := makeTempDirAndFile(t) + gunit.FileExists(t, file) +} + +func Test_filesystem_FileExists_failure_for_missing(t *testing.T) { + dir := t.TempDir() + aSpy := eye.Spy() + gunit.FileExists(aSpy, filepath.Join(dir, "missing.txt")) + aSpy.HadErrorContaining(t, "wanted existing file") +} + +func Test_filesystem_FileExists_failure_for_directory(t *testing.T) { + dir := t.TempDir() + aSpy := eye.Spy() + gunit.FileExists(aSpy, dir) + aSpy.HadErrorContaining(t, "wanted existing file") +} + +func Test_filesystem_NoFileExists_success_for_missing(t *testing.T) { + dir := t.TempDir() + gunit.NoFileExists(t, filepath.Join(dir, "missing.txt")) +} + +func Test_filesystem_NoFileExists_success_for_directory(t *testing.T) { + gunit.NoFileExists(t, t.TempDir()) +} + +func Test_filesystem_NoFileExists_failure(t *testing.T) { + _, file := makeTempDirAndFile(t) + aSpy := eye.Spy() + gunit.NoFileExists(aSpy, file) + aSpy.HadErrorContaining(t, "wanted no file") +} + +func Test_filesystem_DirExists_success(t *testing.T) { + gunit.DirExists(t, t.TempDir()) +} + +func Test_filesystem_DirExists_failure_for_missing(t *testing.T) { + dir := t.TempDir() + aSpy := eye.Spy() + gunit.DirExists(aSpy, filepath.Join(dir, "missing")) + aSpy.HadErrorContaining(t, "wanted existing directory") +} + +func Test_filesystem_DirExists_failure_for_file(t *testing.T) { + _, file := makeTempDirAndFile(t) + aSpy := eye.Spy() + gunit.DirExists(aSpy, file) + aSpy.HadErrorContaining(t, "wanted existing directory") +} + +func Test_filesystem_NoDirExists_success_for_missing(t *testing.T) { + dir := t.TempDir() + gunit.NoDirExists(t, filepath.Join(dir, "missing")) +} + +func Test_filesystem_NoDirExists_success_for_file(t *testing.T) { + _, file := makeTempDirAndFile(t) + gunit.NoDirExists(t, file) +} + +func Test_filesystem_NoDirExists_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.NoDirExists(aSpy, t.TempDir()) + aSpy.HadErrorContaining(t, "wanted no directory") +} diff --git a/float.go b/float.go new file mode 100644 index 0000000..f2b3b6d --- /dev/null +++ b/float.go @@ -0,0 +1,40 @@ +package gunit + +import "math" + +type Floaty interface{ ~float32 | ~float64 } + +func Float[F Floaty](t T, actual F) *Flt[F] { return &Flt[F]{T: t, actual: actual} } + +type Flt[F Floaty] struct { + T + actual F +} + +func (f *Flt[F]) CloseTo(expected, delta F) { + f.Helper() + if math.Abs(float64(f.actual-expected)) > float64(delta) { + f.Errorf("got <%v>, wanted within <%v> of <%v>", f.actual, delta, expected) + } +} + +func (f *Flt[F]) IsNaN() { + f.Helper() + if !math.IsNaN(float64(f.actual)) { + f.Errorf("got <%v>, wanted NaN", f.actual) + } +} + +func (f *Flt[F]) IsInf() { + f.Helper() + if !math.IsInf(float64(f.actual), 0) { + f.Errorf("got <%v>, wanted infinity", f.actual) + } +} + +func (f *Flt[F]) IsInfSign(sign int) { + f.Helper() + if !math.IsInf(float64(f.actual), sign) { + f.Errorf("got <%v>, wanted infinity with sign <%d>", f.actual, sign) + } +} diff --git a/float_typed_test.go b/float_typed_test.go new file mode 100644 index 0000000..164f93a --- /dev/null +++ b/float_typed_test.go @@ -0,0 +1,48 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "math" + "testing" +) + +func Test_float_CloseTo_success(t *testing.T) { + gunit.Float(t, 1.1).CloseTo(1.0, 0.2) +} + +func Test_float_CloseTo_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Float(aSpy, 1.3).CloseTo(1.0, 0.2) + aSpy.HadErrorContaining(t, "wanted within") +} + +func Test_float_IsNaN_success(t *testing.T) { + gunit.Float(t, math.NaN()).IsNaN() +} + +func Test_float_IsNaN_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Float(aSpy, 1.0).IsNaN() + aSpy.HadErrorContaining(t, "wanted NaN") +} + +func Test_float_IsInf_success(t *testing.T) { + gunit.Float(t, math.Inf(1)).IsInf() +} + +func Test_float_IsInf_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Float(aSpy, 1.0).IsInf() + aSpy.HadErrorContaining(t, "wanted infinity") +} + +func Test_float_IsInfSign_success(t *testing.T) { + gunit.Float(t, math.Inf(-1)).IsInfSign(-1) +} + +func Test_float_IsInfSign_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Float(aSpy, math.Inf(1)).IsInfSign(-1) + aSpy.HadErrorContaining(t, "wanted infinity with sign") +} diff --git a/map.go b/map.go index 39b798a..017c935 100644 --- a/map.go +++ b/map.go @@ -90,3 +90,118 @@ func (m *Mappy[K, V]) WithValues(values ...V) { } // TODO: Implement Includes() or Contains() for subset of pairs? + +func (m *Mappy[K, V]) HasKey(key K) { + m.Helper() + if _, ok := m.actual[key]; !ok { + m.Errorf("got key absent <%v>, wanted present in map", key) + } +} + +func (m *Mappy[K, V]) NotHasKey(key K) { + m.Helper() + if _, ok := m.actual[key]; ok { + m.Errorf("got key present <%v>, wanted absent from map", key) + } +} + +func (m *Mappy[K, V]) KeysExactly(expected ...K) { + m.Helper() + actualKeys := make(map[K]bool, len(m.actual)) + for k := range m.actual { + actualKeys[k] = true + } + expectedKeys := make(map[K]bool, len(expected)) + for _, k := range expected { + expectedKeys[k] = true + } + var missing, extra []K + for k := range expectedKeys { + if !actualKeys[k] { + missing = append(missing, k) + } + } + for k := range actualKeys { + if !expectedKeys[k] { + extra = append(extra, k) + } + } + if len(missing) > 0 || len(extra) > 0 { + m.Errorf("got extra keys <%v> and missing keys <%v>, wanted exact key set", extra, missing) + } +} + +func (m *Mappy[K, V]) NotEmpty() { + m.Helper() + if len(m.actual) == 0 { + m.Errorf("got len=<0>, wanted non-empty map") + } +} + +func (m *Mappy[K, V]) IsNotEmpty() { m.NotEmpty() } + +func (m *Mappy[K, V]) NotContains(values ...V) { + m.Helper() + var present []V + seen := make(map[int]bool) + for _, actual := range m.actual { + for i, expected := range values { + if cmp.Equal(actual, expected) { + if !seen[i] { + present = append(present, expected) + } + seen[i] = true + } + } + } + if len(present) > 0 { + m.Errorf("got values <%v>, wanted absent from map", present) + } +} + +func (m *Mappy[K, V]) Len(expected int) { + m.Helper() + if len(m.actual) != expected { + m.Errorf("got len=<%v>, wanted <%v>", len(m.actual), expected) + } +} + +func (m *Mappy[K, V]) WithItem(k K, expected V) { + m.Helper() + actual, ok := m.actual[k] + if !ok { + m.Errorf("got key absent, wanted value for key <%v>", k) + return + } + if !cmp.Equal(actual, expected) { + m.Errorf("got value=<%v> for key=<%v>, wanted <%v>", actual, k, expected) + } +} + +func (m *Mappy[K, V]) WithItems(expected map[K]V) { + m.Helper() + var missing, mismatched []K + for key, expectedValue := range expected { + actualValue, ok := m.actual[key] + if !ok { + missing = append(missing, key) + continue + } + if !cmp.Equal(actualValue, expectedValue) { + mismatched = append(mismatched, key) + } + } + if len(missing) > 0 || len(mismatched) > 0 { + m.Errorf("got missing keys <%v> and mismatched keys <%v>, wanted entries <%v>", missing, mismatched, expected) + } +} + +func (m *Mappy[K, V]) WithoutItems(expected map[K]V) { + m.Helper() + for key, expectedValue := range expected { + if actualValue, ok := m.actual[key]; ok && cmp.Equal(actualValue, expectedValue) { + m.Errorf("got all entries <%v>, wanted at least one absent or different", expected) + return + } + } +} diff --git a/map_typed_test.go b/map_typed_test.go new file mode 100644 index 0000000..3d3128d --- /dev/null +++ b/map_typed_test.go @@ -0,0 +1,119 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "testing" +) + +func Test_map_HasKey_success(t *testing.T) { + gunit.Map(t, testMap()).HasKey("a") +} + +func Test_map_HasKey_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).HasKey("c") + aSpy.HadErrorContaining(t, "wanted present in map") +} + +func Test_map_NotHasKey_success(t *testing.T) { + gunit.Map(t, testMap()).NotHasKey("c") +} + +func Test_map_NotHasKey_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).NotHasKey("a") + aSpy.HadErrorContaining(t, "wanted absent from map") +} + +func Test_map_KeysExactly_success(t *testing.T) { + gunit.Map(t, testMap()).KeysExactly("a", "b") +} + +func Test_map_KeysExactly_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).KeysExactly("a", "c") + aSpy.HadErrorContaining(t, "wanted exact key set") +} + +func Test_map_NotEmpty_success(t *testing.T) { + gunit.Map(t, testMap()).NotEmpty() +} + +func Test_map_NotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, map[string]int{}).NotEmpty() + aSpy.HadErrorContaining(t, "wanted non-empty map") +} + +func Test_map_IsNotEmpty_success(t *testing.T) { + gunit.Map(t, testMap()).IsNotEmpty() +} + +func Test_map_IsNotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, map[string]int{}).IsNotEmpty() + aSpy.HadErrorContaining(t, "wanted non-empty map") +} + +func Test_map_NotContains_success(t *testing.T) { + gunit.Map(t, testMap()).NotContains(3) +} + +func Test_map_NotContains_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).NotContains(1) + aSpy.HadErrorContaining(t, "wanted absent from map") +} + +func Test_map_Len_success(t *testing.T) { + gunit.Map(t, testMap()).Len(2) +} + +func Test_map_Len_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).Len(3) + aSpy.HadErrorContaining(t, "wanted <3>") +} + +func Test_map_WithItem_success(t *testing.T) { + gunit.Map(t, testMap()).WithItem("a", 1) +} + +func Test_map_WithItem_failure_for_missing_key(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).WithItem("c", 3) + aSpy.HadErrorContaining(t, "got key absent") +} + +func Test_map_WithItem_failure_for_mismatched_value(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).WithItem("a", 3) + aSpy.HadErrorContaining(t, "wanted <3>") +} + +func Test_map_WithItems_success(t *testing.T) { + gunit.Map(t, testMap()).WithItems(map[string]int{"a": 1}) +} + +func Test_map_WithItems_failure_for_missing_key(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).WithItems(map[string]int{"c": 3}) + aSpy.HadErrorContaining(t, "missing keys") +} + +func Test_map_WithItems_failure_for_mismatched_value(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).WithItems(map[string]int{"a": 3}) + aSpy.HadErrorContaining(t, "mismatched keys") +} + +func Test_map_WithoutItems_success(t *testing.T) { + gunit.Map(t, testMap()).WithoutItems(map[string]int{"a": 2}) +} + +func Test_map_WithoutItems_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Map(aSpy, testMap()).WithoutItems(map[string]int{"a": 1}) + aSpy.HadErrorContaining(t, "wanted at least one absent") +} diff --git a/number.go b/number.go index 064f652..e0f72bb 100644 --- a/number.go +++ b/number.go @@ -31,6 +31,10 @@ func (n *Num[N]) NotEqualTo(expected N) { } } +func (n *Num[N]) NotEqual(expected N) { + n.NotEqualTo(expected) +} + func (n *Num[N]) LessThan(expected N) { n.Helper() if n.actual >= expected { diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..7eafb17 --- /dev/null +++ b/reader.go @@ -0,0 +1,91 @@ +package gunit + +import ( + "bytes" + "io" + "strings" +) + +func Reader(t T, actual io.Reader) *ReaderAssert { return &ReaderAssert{T: t, actual: actual} } + +type ReaderAssert struct { + T + actual io.Reader + body []byte + read bool +} + +func (r *ReaderAssert) EqualToString(expected string) { + r.Helper() + actual, ok := r.readAll() + if !ok { + return + } + if string(actual) != expected { + r.Errorf("got reader string <%s>, wanted <%s>", string(actual), expected) + } +} + +func (r *ReaderAssert) ContainsString(expected string) { + r.Helper() + actual, ok := r.readAll() + if !ok { + return + } + if !strings.Contains(string(actual), expected) { + r.Errorf("got reader string <%s>, wanted substring <%s>", string(actual), expected) + } +} + +func (r *ReaderAssert) EqualToBytes(expected []byte) { + r.Helper() + actual, ok := r.readAll() + if !ok { + return + } + if !bytes.Equal(actual, expected) { + r.Errorf("got reader bytes <%v>, wanted <%v>", actual, expected) + } +} + +func (r *ReaderAssert) IsEmpty() { + r.Helper() + actual, ok := r.readAll() + if !ok { + return + } + if len(actual) != 0 { + r.Errorf("got reader len()=%d, wanted empty reader", len(actual)) + } +} + +func (r *ReaderAssert) IsNotEmpty() { + r.Helper() + actual, ok := r.readAll() + if !ok { + return + } + if len(actual) == 0 { + r.Errorf("got empty reader, wanted non-empty reader") + } +} + +func (r *ReaderAssert) NotEmpty() { r.IsNotEmpty() } + +func (r *ReaderAssert) readAll() ([]byte, bool) { + if r.actual == nil { + r.Errorf("got nil reader, wanted reader body") + return nil, false + } + if r.read { + return r.body, true + } + body, err := io.ReadAll(r.actual) + if err != nil { + r.Errorf("got reader read error: %v", err) + return nil, false + } + r.body = body + r.read = true + return body, true +} diff --git a/reader_typed_test.go b/reader_typed_test.go new file mode 100644 index 0000000..18383cf --- /dev/null +++ b/reader_typed_test.go @@ -0,0 +1,75 @@ +package gunit_test + +import ( + "bytes" + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "strings" + "testing" +) + +func Test_reader_ContainsString_success(t *testing.T) { + gunit.Reader(t, strings.NewReader("hello world")).ContainsString("world") +} + +func Test_reader_ContainsString_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, strings.NewReader("hello")).ContainsString("bye") + aSpy.HadErrorContaining(t, "wanted substring") +} + +func Test_reader_EqualToString_success(t *testing.T) { + gunit.Reader(t, strings.NewReader("hello")).EqualToString("hello") +} + +func Test_reader_EqualToString_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, strings.NewReader("hello")).EqualToString("bye") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_reader_EqualToString_failure_for_nil(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, nil).EqualToString("hello") + aSpy.HadErrorContaining(t, "got nil reader") +} + +func Test_reader_EqualToBytes_success(t *testing.T) { + gunit.Reader(t, bytes.NewBufferString("bytes")).EqualToBytes([]byte("bytes")) +} + +func Test_reader_EqualToBytes_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, bytes.NewBufferString("bytes")).EqualToBytes([]byte("other")) + aSpy.HadErrorContaining(t, "wanted") +} + +func Test_reader_IsEmpty_success(t *testing.T) { + gunit.Reader(t, strings.NewReader("")).IsEmpty() +} + +func Test_reader_IsEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, strings.NewReader("x")).IsEmpty() + aSpy.HadErrorContaining(t, "wanted empty reader") +} + +func Test_reader_IsNotEmpty_success(t *testing.T) { + gunit.Reader(t, strings.NewReader("x")).IsNotEmpty() +} + +func Test_reader_IsNotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, strings.NewReader("")).IsNotEmpty() + aSpy.HadErrorContaining(t, "wanted non-empty reader") +} + +func Test_reader_NotEmpty_success(t *testing.T) { + gunit.Reader(t, strings.NewReader("x")).NotEmpty() +} + +func Test_reader_NotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Reader(aSpy, strings.NewReader("")).NotEmpty() + aSpy.HadErrorContaining(t, "wanted non-empty reader") +} diff --git a/slice.go b/slice.go new file mode 100644 index 0000000..4e98b5f --- /dev/null +++ b/slice.go @@ -0,0 +1,133 @@ +package gunit + +import "github.com/google/go-cmp/cmp" + +func Slice[I any](t T, actual []I) *Slc[I] { + return &Slc[I]{T: t, actual: actual} +} + +type Slc[I any] struct { + T + actual []I +} + +func (s *Slc[I]) Contains(expected ...I) { + s.Helper() + missing := missingItems(s.actual, expected) + if len(missing) > 0 { + s.Errorf("got missing items <%v>, wanted slice containing <%v>", missing, expected) + } +} + +func (s *Slc[I]) ContainsAny(expected ...I) { + s.Helper() + for _, actual := range s.actual { + for _, want := range expected { + if cmp.Equal(actual, want) { + return + } + } + } + s.Errorf("got no matching item, wanted any of <%v>", expected) +} + +func (s *Slc[I]) NotContains(expected ...I) { + s.Helper() + var present []I + for _, want := range expected { + for _, actual := range s.actual { + if cmp.Equal(actual, want) { + present = append(present, want) + break + } + } + } + if len(present) > 0 { + s.Errorf("got items <%v> present in slice, wanted absent", present) + } +} + +func (s *Slc[I]) EqualTo(expected ...I) { + s.Helper() + if diff := cmp.Diff(expected, s.actual); diff != "" { + s.Errorf("slice mismatch (-want +got):\n%s", diff) + } +} + +func (s *Slc[I]) Len(expected int) { + s.Helper() + if len(s.actual) != expected { + s.Errorf("got len()=%d, wanted %d", len(s.actual), expected) + } +} + +func (s *Slc[I]) Cap(expected int) { + s.Helper() + if cap(s.actual) != expected { + s.Errorf("got cap()=%d, wanted %d", cap(s.actual), expected) + } +} + +func (s *Slc[I]) IsEmpty() { + s.Helper() + if len(s.actual) != 0 { + s.Errorf("got len()=%d, wanted 0", len(s.actual)) + } +} + +func (s *Slc[I]) IsNotEmpty() { + s.Helper() + if len(s.actual) == 0 { + s.Errorf("got len()=0, wanted > 0") + } +} + +func (s *Slc[I]) NotEmpty() { s.IsNotEmpty() } + +func (s *Slc[I]) ContainsExactly(expected ...I) { + s.Helper() + if len(s.actual) != len(expected) { + s.Errorf("length mismatch got %d, want %d", len(s.actual), len(expected)) + return + } + s.Contains(expected...) +} + +func (s *Slc[I]) SubsetOf(expected ...I) { + s.Helper() + for _, actual := range s.actual { + if !containsEqual(expected, actual) { + s.Errorf("got item <%v> outside expected set <%v>", actual, expected) + return + } + } +} + +func (s *Slc[I]) NotSubsetOf(expected ...I) { + s.Helper() + for _, actual := range s.actual { + if !containsEqual(expected, actual) { + return + } + } + s.Errorf("got subset <%v>, wanted at least one item outside <%v>", s.actual, expected) +} + +func missingItems[I any](actual, expected []I) []I { + var missing []I + for _, want := range expected { + if !containsEqual(actual, want) { + missing = append(missing, want) + } + } + return missing +} + +func containsEqual[I any](items []I, target I) bool { + for _, item := range items { + if cmp.Equal(item, target) { + return true + } + } + return false +} diff --git a/slice_typed_test.go b/slice_typed_test.go new file mode 100644 index 0000000..35376a3 --- /dev/null +++ b/slice_typed_test.go @@ -0,0 +1,127 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "testing" +) + +func Test_slice_Contains_success(t *testing.T) { + gunit.Slice(t, []int{1, 2, 3}).Contains(2, 3) +} + +func Test_slice_Contains_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).Contains(2) + aSpy.HadErrorContaining(t, "missing items") +} + +func Test_slice_ContainsAny_success(t *testing.T) { + gunit.Slice(t, []int{1, 2, 3}).ContainsAny(4, 3) +} + +func Test_slice_ContainsAny_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).ContainsAny(2) + aSpy.HadErrorContaining(t, "no matching item") +} + +func Test_slice_NotContains_success(t *testing.T) { + gunit.Slice(t, []int{1, 2, 3}).NotContains(4) +} + +func Test_slice_NotContains_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).NotContains(1) + aSpy.HadErrorContaining(t, "present in slice") +} + +func Test_slice_EqualTo_success(t *testing.T) { + gunit.Slice(t, []int{1, 2, 3}).EqualTo(1, 2, 3) +} + +func Test_slice_EqualTo_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).EqualTo(2) + aSpy.HadErrorContaining(t, "slice mismatch") +} + +func Test_slice_Len_success(t *testing.T) { + gunit.Slice(t, make([]int, 2, 4)).Len(2) +} + +func Test_slice_Len_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).Len(2) + aSpy.HadErrorContaining(t, "got len()=1") +} + +func Test_slice_Cap_success(t *testing.T) { + gunit.Slice(t, make([]int, 2, 4)).Cap(4) +} + +func Test_slice_Cap_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, make([]int, 1, 2)).Cap(1) + aSpy.HadErrorContaining(t, "got cap()=2") +} + +func Test_slice_IsEmpty_success(t *testing.T) { + gunit.Slice(t, []int{}).IsEmpty() +} + +func Test_slice_IsEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).IsEmpty() + aSpy.HadErrorContaining(t, "wanted 0") +} + +func Test_slice_IsNotEmpty_success(t *testing.T) { + gunit.Slice(t, []int{1}).IsNotEmpty() +} + +func Test_slice_IsNotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{}).IsNotEmpty() + aSpy.HadErrorContaining(t, "wanted > 0") +} + +func Test_slice_NotEmpty_success(t *testing.T) { + gunit.Slice(t, []int{1}).NotEmpty() +} + +func Test_slice_NotEmpty_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{}).NotEmpty() + aSpy.HadErrorContaining(t, "wanted > 0") +} + +func Test_slice_ContainsExactly_success(t *testing.T) { + gunit.Slice(t, []int{1, 2}).ContainsExactly(2, 1) +} + +func Test_slice_ContainsExactly_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1}).ContainsExactly(1, 2) + aSpy.HadErrorContaining(t, "length mismatch") +} + +func Test_slice_SubsetOf_success(t *testing.T) { + gunit.Slice(t, []int{1, 2}).SubsetOf(1, 2, 3) +} + +func Test_slice_SubsetOf_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1, 4}).SubsetOf(1, 2, 3) + aSpy.HadErrorContaining(t, "outside expected set") +} + +func Test_slice_NotSubsetOf_success(t *testing.T) { + gunit.Slice(t, []int{1, 4}).NotSubsetOf(1, 2, 3) +} + +func Test_slice_NotSubsetOf_failure(t *testing.T) { + aSpy := eye.Spy() + gunit.Slice(aSpy, []int{1, 2}).NotSubsetOf(1, 2, 3) + aSpy.HadErrorContaining(t, "got subset") +} diff --git a/string.go b/string.go index 1ec1a04..6c1b0c3 100644 --- a/string.go +++ b/string.go @@ -29,6 +29,13 @@ func (s *Str[S]) Contains(needle S) { } } +func (s *Str[S]) NotContains(needle S) { + s.Helper() + if strings.Contains(string(s.actual), string(needle)) { + s.Errorf("want <%v> not to contain <%v>", s.actual, needle) + } +} + func (s *Str[S]) HasPrefix(prefix S) { s.Helper() if !strings.HasPrefix(string(s.actual), string(prefix)) { @@ -43,6 +50,10 @@ func (s *Str[S]) HasSuffix(suffix S) { } } +func (s *Str[S]) NotEmpty() { + s.IsNotEmpty() +} + func (s *Str[S]) IsEmpty() { s.Helper() if s.actual != "" { diff --git a/typed_assertion_helpers_test.go b/typed_assertion_helpers_test.go new file mode 100644 index 0000000..b4c3258 --- /dev/null +++ b/typed_assertion_helpers_test.go @@ -0,0 +1,29 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "net/url" + "os" + "path/filepath" + "testing" +) + +func makeTempDirAndFile(t *testing.T) (string, string) { + t.Helper() + dir := t.TempDir() + file := filepath.Join(dir, "file.txt") + if err := os.WriteFile(file, []byte("hello"), 0o600); err != nil { + t.Fatal(err) + } + return dir, file +} + +func testURL(t gunit.T) *gunit.URLAssert { + parsed, err := url.Parse("https://example.com/path?q=go") + if err != nil { + t.Fatalf("failed to parse test URL: %v", err) + } + return gunit.URL(t, parsed) +} + +func testMap() map[string]int { return map[string]int{"a": 1, "b": 2} } diff --git a/url.go b/url.go new file mode 100644 index 0000000..040dc22 --- /dev/null +++ b/url.go @@ -0,0 +1,100 @@ +package gunit + +import "net/url" + +func URL(t T, actual *url.URL) *URLAssert { return &URLAssert{T: t, actual: actual} } +func ParseURL(t T, actual string) *URLAssert { + parsed, err := url.Parse(actual) + if err != nil { + return &URLAssert{T: t, validation: err, raw: actual} + } + return URL(t, parsed) +} + +type URLAssert struct { + T + actual *url.URL + validation error + raw string +} + +func (u *URLAssert) Scheme(expected string) { + if !u.ready() { + return + } + if u.actual.Scheme != expected { + u.Errorf("got URL scheme <%s>, wanted <%s>", u.actual.Scheme, expected) + } +} + +func (u *URLAssert) Host(expected string) { + if !u.ready() { + return + } + if u.actual.Host != expected { + u.Errorf("got URL host <%s>, wanted <%s>", u.actual.Host, expected) + } +} + +func (u *URLAssert) Path(expected string) { + if !u.ready() { + return + } + if u.actual.Path != expected { + u.Errorf("got URL path <%s>, wanted <%s>", u.actual.Path, expected) + } +} + +func (u *URLAssert) RawQuery(expected string) { + if !u.ready() { + return + } + if u.actual.RawQuery != expected { + u.Errorf("got URL raw query <%s>, wanted <%s>", u.actual.RawQuery, expected) + } +} + +func (u *URLAssert) QueryParam(key, expected string) { + if !u.ready() { + return + } + values, ok := u.actual.Query()[key] + if !ok { + u.Errorf("missing URL query parameter <%s>, wanted <%s>", key, expected) + return + } + if values[0] != expected { + u.Errorf("got URL query parameter <%s> value <%s>, wanted <%s>", key, values[0], expected) + } +} + +func (u *URLAssert) NoQueryParam(key string) { + if !u.ready() { + return + } + if _, ok := u.actual.Query()[key]; ok { + u.Errorf("got URL query parameter <%s>, wanted it absent", key) + } +} + +func (u *URLAssert) String(expected string) { + if !u.ready() { + return + } + if u.actual.String() != expected { + u.Errorf("got URL string <%s>, wanted <%s>", u.actual.String(), expected) + } +} + +func (u *URLAssert) ready() bool { + u.Helper() + if u.validation != nil { + u.Errorf("invalid URL <%s>: %v", u.raw, u.validation) + return false + } + if u.actual == nil { + u.Errorf("got nil URL") + return false + } + return true +} diff --git a/url_typed_test.go b/url_typed_test.go new file mode 100644 index 0000000..c509504 --- /dev/null +++ b/url_typed_test.go @@ -0,0 +1,99 @@ +package gunit_test + +import ( + "github.com/gogunit/gunit" + "github.com/gogunit/gunit/eye" + "testing" +) + +func Test_url_Scheme_success(t *testing.T) { + testURL(t).Scheme("https") +} + +func Test_url_Scheme_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).Scheme("http") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_Scheme_failure_for_nil(t *testing.T) { + aSpy := eye.Spy() + gunit.URL(aSpy, nil).Scheme("https") + aSpy.HadErrorContaining(t, "got nil URL") +} + +func Test_url_Host_success(t *testing.T) { + testURL(t).Host("example.com") +} + +func Test_url_Host_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).Host("other.example") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_Path_success(t *testing.T) { + testURL(t).Path("/path") +} + +func Test_url_Path_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).Path("/other") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_RawQuery_success(t *testing.T) { + testURL(t).RawQuery("q=go") +} + +func Test_url_RawQuery_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).RawQuery("q=rust") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_QueryParam_success(t *testing.T) { + testURL(t).QueryParam("q", "go") +} + +func Test_url_QueryParam_failure_for_missing(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).QueryParam("missing", "go") + aSpy.HadErrorContaining(t, "missing URL query parameter") +} + +func Test_url_QueryParam_failure_for_mismatch(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).QueryParam("q", "rust") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_NoQueryParam_success(t *testing.T) { + testURL(t).NoQueryParam("missing") +} + +func Test_url_NoQueryParam_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).NoQueryParam("q") + aSpy.HadErrorContaining(t, "wanted it absent") +} + +func Test_url_String_success(t *testing.T) { + testURL(t).String("https://example.com/path?q=go") +} + +func Test_url_String_failure(t *testing.T) { + aSpy := eye.Spy() + testURL(aSpy).String("https://example.com/other") + aSpy.HadErrorContaining(t, "wanted ") +} + +func Test_url_ParseURL_String_success(t *testing.T) { + gunit.ParseURL(t, "https://example.com/path?q=go").String("https://example.com/path?q=go") +} + +func Test_url_ParseURL_String_failure_for_invalid_url(t *testing.T) { + aSpy := eye.Spy() + gunit.ParseURL(aSpy, "%zz").String("x") + aSpy.HadErrorContaining(t, "invalid URL") +}