diff --git a/.golangci.yml b/.golangci.yml index 307062e..5a86215 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,7 +31,14 @@ linters: files: - $all allow: + - golang.org/x/exp/constraints$ - github.com/cinode/go-common/ + - errors$ + - fmt$ + - reflect$ + - regexp$ + - strings$ + - testing$ dupl: threshold: 100 goconst: diff --git a/go.mod b/go.mod index fe1d92c..691c54a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/cinode/go-common go 1.25.3 + +require golang.org/x/exp v0.0.0-20251017212417-90e834f514db diff --git a/go.sum b/go.sum index e69de29..25ba8fa 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw= +golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= diff --git a/picotestify/assert/assert.go b/picotestify/assert/assert.go new file mode 100644 index 0000000..ff68dee --- /dev/null +++ b/picotestify/assert/assert.go @@ -0,0 +1,329 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package assert + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strings" + + "golang.org/x/exp/constraints" +) + +type TestingT interface { + Helper() + Error(msgAndArgs ...any) +} + +func fail(t TestingT, msgAndArgs []any, fmtString string, fmtArgs ...any) { + t.Error( + append( + []any{fmt.Sprintf(fmtString, fmtArgs...)}, + msgAndArgs..., + )..., + ) +} + +func Equal[T any](t TestingT, expected, actual T, msgAndArgs ...any) bool { + t.Helper() + + if reflect.DeepEqual(expected, actual) { + return true + } + + fail(t, msgAndArgs, "Values not equal, expected: %+v, actual: %+v", expected, actual) + + return false +} + +func NotEqual[T any](t TestingT, expected, actual T, msgAndArgs ...any) bool { + t.Helper() + + if !reflect.DeepEqual(expected, actual) { + return true + } + + fail(t, msgAndArgs, "Values are equal: %v", actual) + + return false +} + +func True(t TestingT, condition bool, msgAndArgs ...any) bool { + t.Helper() + + if condition { + return true + } + + fail(t, msgAndArgs, "Condition is not true") + + return false +} + +func False(t TestingT, condition bool, msgAndArgs ...any) bool { + t.Helper() + + if !condition { + return true + } + + fail(t, msgAndArgs, "Condition is not false") + + return false +} + +func Nil(t TestingT, object any, msgAndArgs ...any) bool { + t.Helper() + + if isNil(object) { + return true + } + + fail(t, msgAndArgs, "Object is not nil") + + return false +} + +func NotNil(t TestingT, object any, msgAndArgs ...any) bool { + t.Helper() + + if !isNil(object) { + return true + } + + fail(t, msgAndArgs, "Object is nil") + + return false +} + +func isNil(object any) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + switch value.Kind() { + case + reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Pointer, + reflect.Slice, + reflect.UnsafePointer: + if value.IsNil() { + return true + } + } + + return false +} + +func NoError(t TestingT, err error, msgAndArgs ...any) bool { + t.Helper() + + if err == nil { + return true + } + + fail(t, msgAndArgs, "Received unexpected error: %v", err) + + return false +} + +func ErrorIs(t TestingT, err, target error, msgAndArgs ...any) bool { + t.Helper() + + if errors.Is(err, target) { + return true + } + + fail(t, msgAndArgs, "Error is not %T: %v", target, err) + + return false +} + +func ErrorContains(t TestingT, err error, contains string, msgAndArgs ...any) bool { + t.Helper() + + if err != nil && strings.Contains(err.Error(), contains) { + return true + } + + fail(t, msgAndArgs, "Error %q does not contain %q", err.Error(), contains) + + return false +} + +func Empty(t TestingT, object any, msgAndArgs ...any) bool { + t.Helper() + + if isEmpty(object) { + return true + } + + fail(t, msgAndArgs, "Object is not empty") + + return false +} + +func NotEmpty(t TestingT, object any, msgAndArgs ...any) bool { + t.Helper() + + if !isEmpty(object) { + return true + } + + fail(t, msgAndArgs, "Object is empty") + + return false +} + +func isEmpty(object any) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + switch value.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return value.Len() == 0 + } + + return false +} + +func Greater[T constraints.Ordered](t TestingT, a, b T, msgAndArgs ...any) bool { + t.Helper() + + if a > b { + return true + } + + fail(t, msgAndArgs, "Expected %v to be greater than %v", a, b) + + return false +} + +func GreaterOrEqual[T constraints.Ordered](t TestingT, a, b T, msgAndArgs ...any) bool { + t.Helper() + + if a >= b { + return true + } + + fail(t, msgAndArgs, "Expected %v to be greater or equal than %v", a, b) + + return false +} + +func isZero(value any) bool { + return value == nil || reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface()) +} + +func Zero(t TestingT, value any, msgAndArgs ...any) bool { + t.Helper() + + if isZero(value) { + return true + } + + fail(t, msgAndArgs, "Expected %v to be zero", value) + + return false +} + +func NotZero(t TestingT, value any, msgAndArgs ...any) bool { + t.Helper() + + if !isZero(value) { + return true + } + + fail(t, msgAndArgs, "Expected %v to not be zero", value) + + return false +} + +func didPanic(f func()) (didPanic bool) { + didPanic = true + + defer func() { _ = recover() }() + + f() + didPanic = false + + return +} + +func Panics(t TestingT, f func(), msgAndArgs ...any) bool { + t.Helper() + + if didPanic(f) { + return true + } + + fail(t, msgAndArgs, "Expected function to panic") + + return false +} + +func NotPanics(t TestingT, f func(), msgAndArgs ...any) bool { + t.Helper() + + if !didPanic(f) { + return true + } + + fail(t, msgAndArgs, "Expected function not to panic") + + return false +} + +func Regexp(t TestingT, pattern, text string, msgAndArgs ...any) bool { + t.Helper() + + if matches, err := regexp.MatchString(pattern, text); err != nil { + t.Helper() + fail(t, msgAndArgs, "Invalid regexp pattern: %v", err) + t.Error(msgAndArgs...) + + return false + } else if matches { + return true + } + + fail(t, msgAndArgs, "Expected %v to match %v", text, pattern) + + return false +} + +func Len(t TestingT, obj any, length int, msgAndArgs ...any) bool { + t.Helper() + + r := reflect.ValueOf(obj) + + if r.Len() == length { + return true + } + + fail(t, msgAndArgs, "Expected length of %d, found: %d", length, r.Len()) + + return false +} diff --git a/picotestify/assert/assert_test.go b/picotestify/assert/assert_test.go new file mode 100644 index 0000000..8ed3b58 --- /dev/null +++ b/picotestify/assert/assert_test.go @@ -0,0 +1,169 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package assert_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/cinode/go-common/picotestify/assert" +) + +type testingMock struct { + helper bool + error bool +} + +func (t *testingMock) Helper() { + t.helper = true +} + +func (t *testingMock) Error(msgAndArgs ...any) { + t.error = true +} + +func TestAssert(t *testing.T) { + var testError = errors.New("test-error") + + for _, tt := range []struct { + pass func(t assert.TestingT) + fail func(t assert.TestingT) + name string + }{ + { + name: "Equal", + pass: func(t assert.TestingT) { assert.Equal(t, 1, 1) }, + fail: func(t assert.TestingT) { assert.Equal(t, 1, 2) }, + }, + { + name: "NotEqual", + pass: func(t assert.TestingT) { assert.NotEqual(t, 1, 2) }, + fail: func(t assert.TestingT) { assert.NotEqual(t, 1, 1) }, + }, + { + name: "True", + pass: func(t assert.TestingT) { assert.True(t, true) }, + fail: func(t assert.TestingT) { assert.True(t, false) }, + }, + { + name: "False", + pass: func(t assert.TestingT) { assert.False(t, false) }, + fail: func(t assert.TestingT) { assert.False(t, true) }, + }, + { + name: "Nil", + pass: func(t assert.TestingT) { assert.Nil(t, nil) }, + fail: func(t assert.TestingT) { assert.Nil(t, 1) }, + }, + { + name: "Nil - function", + pass: func(t assert.TestingT) { assert.Nil(t, (func())(nil)) }, + fail: func(t assert.TestingT) { assert.Nil(t, func() {}) }, + }, + { + name: "NotNil", + pass: func(t assert.TestingT) { assert.NotNil(t, 1) }, + fail: func(t assert.TestingT) { assert.NotNil(t, nil) }, + }, + { + name: "NoError", + pass: func(t assert.TestingT) { assert.NoError(t, nil) }, + fail: func(t assert.TestingT) { assert.NoError(t, errors.New("error")) }, + }, + { + name: "ErrorIs", + pass: func(t assert.TestingT) { assert.ErrorIs(t, fmt.Errorf("error: %w", testError), testError) }, + fail: func(t assert.TestingT) { assert.ErrorIs(t, fmt.Errorf("error: %w", testError), errors.New("error2")) }, + }, + { + name: "ErrorContains", + pass: func(t assert.TestingT) { assert.ErrorContains(t, errors.New("test error: check"), "error") }, + fail: func(t assert.TestingT) { assert.ErrorContains(t, errors.New("test error: check"), "error2") }, + }, + { + name: "Empty", + pass: func(t assert.TestingT) { assert.Empty(t, []int{}) }, + fail: func(t assert.TestingT) { assert.Empty(t, []int{1}) }, + }, + { + name: "NotEmpty", + pass: func(t assert.TestingT) { assert.NotEmpty(t, 1) }, + fail: func(t assert.TestingT) { assert.NotEmpty(t, nil) }, + }, + { + name: "Zero", + pass: func(t assert.TestingT) { assert.Zero(t, 0) }, + fail: func(t assert.TestingT) { assert.Zero(t, 1) }, + }, + { + name: "NotZero", + pass: func(t assert.TestingT) { assert.NotZero(t, 1) }, + fail: func(t assert.TestingT) { assert.NotZero(t, 0) }, + }, + { + name: "Greater", + pass: func(t assert.TestingT) { assert.Greater(t, 2, 1) }, + fail: func(t assert.TestingT) { assert.Greater(t, 1, 2) }, + }, + { + name: "GreaterOrEqual", + pass: func(t assert.TestingT) { assert.GreaterOrEqual(t, 2, 2) }, + fail: func(t assert.TestingT) { assert.GreaterOrEqual(t, 1, 2) }, + }, + { + name: "Panics", + pass: func(t assert.TestingT) { assert.Panics(t, func() { panic("test") }) }, + fail: func(t assert.TestingT) { assert.Panics(t, func() {}) }, + }, + { + name: "NotPanics", + pass: func(t assert.TestingT) { assert.NotPanics(t, func() {}) }, + fail: func(t assert.TestingT) { assert.NotPanics(t, func() { panic("test") }) }, + }, + { + name: "Regexp", + pass: func(t assert.TestingT) { assert.Regexp(t, `^t.s*t+$`, "test") }, + fail: func(t assert.TestingT) { assert.Regexp(t, `^t.s*t+$`, "a test") }, + }, + { + name: "Regexp - bad pattern", + pass: func(t assert.TestingT) { assert.Regexp(t, `test`, "test") }, + fail: func(t assert.TestingT) { assert.Regexp(t, `*test`, "test") }, + }, + { + name: "Len", + pass: func(t assert.TestingT) { assert.Len(t, []int{1, 2, 3}, 3) }, + fail: func(t assert.TestingT) { assert.Len(t, []int{1, 2, 3}, 2) }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Run("pass", func(t *testing.T) { + hlp := &testingMock{} + tt.pass(hlp) + assert.True(t, hlp.helper) + assert.False(t, hlp.error) + }) + t.Run("fail", func(t *testing.T) { + hlp := &testingMock{} + tt.fail(hlp) + assert.True(t, hlp.helper) + assert.True(t, hlp.error) + }) + }) + } +} diff --git a/picotestify/require/require.go b/picotestify/require/require.go new file mode 100644 index 0000000..d7b8538 --- /dev/null +++ b/picotestify/require/require.go @@ -0,0 +1,160 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package require + +import ( + "github.com/cinode/go-common/picotestify/assert" + "golang.org/x/exp/constraints" +) + +type TestingT interface { + assert.TestingT + FailNow() +} + +func Equal[T any](t TestingT, expected, actual T, msgAndArgs ...any) { + t.Helper() + if !assert.Equal(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + +func NotEqual[T any](t TestingT, expected, actual T, msgAndArgs ...any) { + t.Helper() + if !assert.NotEqual(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + +func Greater[T constraints.Ordered](t TestingT, a, b T, msgAndArgs ...any) { + t.Helper() + if !assert.Greater(t, a, b, msgAndArgs...) { + t.FailNow() + } +} + +func GreaterOrEqual[T constraints.Ordered](t TestingT, a, b T, msgAndArgs ...any) { + t.Helper() + if !assert.GreaterOrEqual(t, a, b, msgAndArgs...) { + t.FailNow() + } +} + +func True(t TestingT, condition bool, msgAndArgs ...any) { + t.Helper() + if !assert.True(t, condition, msgAndArgs...) { + t.FailNow() + } +} + +func False(t TestingT, condition bool, msgAndArgs ...any) { + t.Helper() + if !assert.False(t, condition, msgAndArgs...) { + t.FailNow() + } +} + +func Nil(t TestingT, object any, msgAndArgs ...any) { + t.Helper() + if !assert.Nil(t, object, msgAndArgs...) { + t.FailNow() + } +} + +func NotNil(t TestingT, object any, msgAndArgs ...any) { + t.Helper() + if !assert.NotNil(t, object, msgAndArgs...) { + t.FailNow() + } +} + +func NoError(t TestingT, err error, msgAndArgs ...any) { + t.Helper() + if !assert.NoError(t, err, msgAndArgs...) { + t.FailNow() + } +} + +func ErrorIs(t TestingT, err, target error, msgAndArgs ...any) { + t.Helper() + if !assert.ErrorIs(t, err, target, msgAndArgs...) { + t.FailNow() + } +} + +func ErrorContains(t TestingT, err error, contains string, msgAndArgs ...any) { + t.Helper() + if !assert.ErrorContains(t, err, contains, msgAndArgs...) { + t.FailNow() + } +} + +func Empty(t TestingT, object any, msgAndArgs ...any) { + t.Helper() + if !assert.Empty(t, object, msgAndArgs...) { + t.FailNow() + } +} + +func NotEmpty(t TestingT, object any, msgAndArgs ...any) { + t.Helper() + if !assert.NotEmpty(t, object, msgAndArgs...) { + t.FailNow() + } +} + +func Zero(t TestingT, value any, msgAndArgs ...any) { + t.Helper() + if !assert.Zero(t, value, msgAndArgs...) { + t.FailNow() + } +} + +func NotZero(t TestingT, value any, msgAndArgs ...any) { + t.Helper() + if !assert.NotZero(t, value, msgAndArgs...) { + t.FailNow() + } +} + +func Panics(t TestingT, f func(), msgAndArgs ...any) { + t.Helper() + if !assert.Panics(t, f, msgAndArgs...) { + t.FailNow() + } +} + +func NotPanics(t TestingT, f func(), msgAndArgs ...any) { + t.Helper() + if !assert.NotPanics(t, f, msgAndArgs...) { + t.FailNow() + } +} + +func Regexp(t TestingT, pattern, text string, msgAndArgs ...any) { + t.Helper() + if !assert.Regexp(t, pattern, text, msgAndArgs...) { + t.FailNow() + } +} + +func Len(t TestingT, obj any, length int, msgAndArgs ...any) { + t.Helper() + if !assert.Len(t, obj, length, msgAndArgs...) { + t.FailNow() + } +} diff --git a/picotestify/require/require_test.go b/picotestify/require/require_test.go new file mode 100644 index 0000000..c316414 --- /dev/null +++ b/picotestify/require/require_test.go @@ -0,0 +1,176 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package require_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/cinode/go-common/picotestify/require" +) + +type testingMock struct { + helperCalled bool + errorCalled bool + failNowCalled bool +} + +func (t *testingMock) Helper() { + t.helperCalled = true +} + +func (t *testingMock) Error(msgAndArgs ...any) { + t.errorCalled = true +} + +func (t *testingMock) FailNow() { + t.failNowCalled = true +} + +func TestRequire(t *testing.T) { + var testError = errors.New("test-error") + + for _, tt := range []struct { + pass func(t require.TestingT) + fail func(t require.TestingT) + name string + }{ + { + name: "Equal", + pass: func(t require.TestingT) { require.Equal(t, 1, 1) }, + fail: func(t require.TestingT) { require.Equal(t, 1, 2) }, + }, + { + name: "NotEqual", + pass: func(t require.TestingT) { require.NotEqual(t, 1, 2) }, + fail: func(t require.TestingT) { require.NotEqual(t, 1, 1) }, + }, + { + name: "True", + pass: func(t require.TestingT) { require.True(t, true) }, + fail: func(t require.TestingT) { require.True(t, false) }, + }, + { + name: "False", + pass: func(t require.TestingT) { require.False(t, false) }, + fail: func(t require.TestingT) { require.False(t, true) }, + }, + { + name: "Nil", + pass: func(t require.TestingT) { require.Nil(t, nil) }, + fail: func(t require.TestingT) { require.Nil(t, 1) }, + }, + { + name: "Nil - function", + pass: func(t require.TestingT) { require.Nil(t, (func())(nil)) }, + fail: func(t require.TestingT) { require.Nil(t, func() {}) }, + }, + { + name: "NotNil", + pass: func(t require.TestingT) { require.NotNil(t, 1) }, + fail: func(t require.TestingT) { require.NotNil(t, nil) }, + }, + { + name: "NoError", + pass: func(t require.TestingT) { require.NoError(t, nil) }, + fail: func(t require.TestingT) { require.NoError(t, errors.New("error")) }, + }, + { + name: "ErrorIs", + pass: func(t require.TestingT) { require.ErrorIs(t, fmt.Errorf("error: %w", testError), testError) }, + fail: func(t require.TestingT) { require.ErrorIs(t, fmt.Errorf("error: %w", testError), errors.New("error2")) }, + }, + { + name: "ErrorContains", + pass: func(t require.TestingT) { require.ErrorContains(t, errors.New("test error: check"), "error") }, + fail: func(t require.TestingT) { require.ErrorContains(t, errors.New("test error: check"), "error2") }, + }, + { + name: "Empty", + pass: func(t require.TestingT) { require.Empty(t, []int{}) }, + fail: func(t require.TestingT) { require.Empty(t, []int{1}) }, + }, + { + name: "NotEmpty", + pass: func(t require.TestingT) { require.NotEmpty(t, 1) }, + fail: func(t require.TestingT) { require.NotEmpty(t, nil) }, + }, + { + name: "Zero", + pass: func(t require.TestingT) { require.Zero(t, 0) }, + fail: func(t require.TestingT) { require.Zero(t, 1) }, + }, + { + name: "NotZero", + pass: func(t require.TestingT) { require.NotZero(t, 1) }, + fail: func(t require.TestingT) { require.NotZero(t, 0) }, + }, + { + name: "Greater", + pass: func(t require.TestingT) { require.Greater(t, 2, 1) }, + fail: func(t require.TestingT) { require.Greater(t, 1, 2) }, + }, + { + name: "GreaterOrEqual", + pass: func(t require.TestingT) { require.GreaterOrEqual(t, 2, 2) }, + fail: func(t require.TestingT) { require.GreaterOrEqual(t, 1, 2) }, + }, + { + name: "Panics", + pass: func(t require.TestingT) { require.Panics(t, func() { panic("test") }) }, + fail: func(t require.TestingT) { require.Panics(t, func() {}) }, + }, + { + name: "NotPanics", + pass: func(t require.TestingT) { require.NotPanics(t, func() {}) }, + fail: func(t require.TestingT) { require.NotPanics(t, func() { panic("test") }) }, + }, + { + name: "Regexp", + pass: func(t require.TestingT) { require.Regexp(t, `^t.s*t+$`, "test") }, + fail: func(t require.TestingT) { require.Regexp(t, `^t.s*t+$`, "a test") }, + }, + { + name: "Regexp - bad pattern", + pass: func(t require.TestingT) { require.Regexp(t, `test`, "test") }, + fail: func(t require.TestingT) { require.Regexp(t, `*test`, "test") }, + }, + { + name: "Len", + pass: func(t require.TestingT) { require.Len(t, []int{1, 2, 3}, 3) }, + fail: func(t require.TestingT) { require.Len(t, []int{1, 2, 3}, 2) }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Run("pass", func(t *testing.T) { + hlp := &testingMock{} + tt.pass(hlp) + require.True(t, hlp.helperCalled) + require.False(t, hlp.errorCalled) + require.False(t, hlp.failNowCalled) + }) + t.Run("fail", func(t *testing.T) { + hlp := &testingMock{} + tt.fail(hlp) + require.True(t, hlp.helperCalled) + require.True(t, hlp.errorCalled) + require.True(t, hlp.failNowCalled) + }) + }) + } +} diff --git a/picotestify/suite/suite.go b/picotestify/suite/suite.go new file mode 100644 index 0000000..e98c102 --- /dev/null +++ b/picotestify/suite/suite.go @@ -0,0 +1,87 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package suite + +import ( + "reflect" + "regexp" + "testing" +) + +type Suite struct { + t *testing.T +} + +type suiteInterface interface { + T() *testing.T + SetT(t *testing.T) +} + +func (s *Suite) T() *testing.T { return s.t } +func (s *Suite) SetT(t *testing.T) { s.t = t } + +var testMethodRe = regexp.MustCompile("^Test") + +func listTests(suite suiteInterface) func(yield func(string, func()) bool) { + return func(yield func(string, func()) bool) { + st := reflect.TypeOf(suite) + + for i := 0; i < st.NumMethod(); i++ { + method := st.Method(i) + if !testMethodRe.MatchString(method.Name) { + continue + } + + if !yield( + method.Name, + func() { method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) }, + ) { + return + } + } + } +} + +func Run(parentT *testing.T, suite suiteInterface) { + suite.SetT(parentT) + + if setupSuite, isSetupSuite := suite.(interface{ SetupSuite() }); isSetupSuite { + setupSuite.SetupSuite() + } + + if tearDownSuite, isTearDownSuite := suite.(interface{ TearDownSuite() }); isTearDownSuite { + defer tearDownSuite.TearDownSuite() + } + + for name, testFunc := range listTests(suite) { + parentT.Run(name, func(childT *testing.T) { + suite.SetT(childT) + + if setupTest, isSetupTest := suite.(interface{ SetupTest() }); isSetupTest { + setupTest.SetupTest() + } + + if tearDownTest, isTearDownTest := suite.(interface{ TearDownTest() }); isTearDownTest { + defer tearDownTest.TearDownTest() + } + + testFunc() + + suite.SetT(parentT) + }) + } +} diff --git a/picotestify/suite/suite_internal_test.go b/picotestify/suite/suite_internal_test.go new file mode 100644 index 0000000..868e6bf --- /dev/null +++ b/picotestify/suite/suite_internal_test.go @@ -0,0 +1,58 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package suite + +import ( + "reflect" + "testing" +) + +type sampleSuite struct { + Suite +} + +func (s *sampleSuite) Test1() {} +func (s *sampleSuite) NotATest2() {} +func (s *sampleSuite) Test3() {} + +func TestListTests(t *testing.T) { + t.Run("list all tests", func(t *testing.T) { + tests := []string{} + for name, testFunc := range listTests(&sampleSuite{}) { + if testFunc == nil { + t.Errorf("test function is nil for test %s", name) + continue + } + tests = append(tests, name) + } + + if !reflect.DeepEqual(tests, []string{"Test1", "Test3"}) { + t.Errorf("expected tests to be [Test1, Test3], got %v", tests) + } + }) + + t.Run("get one test only", func(t *testing.T) { + for name := range listTests(&sampleSuite{}) { + if name != "Test1" { + t.Errorf("expected Test1, got %s", name) + continue + } + + break + } + }) +} diff --git a/picotestify/suite/suite_test.go b/picotestify/suite/suite_test.go new file mode 100644 index 0000000..eaa647e --- /dev/null +++ b/picotestify/suite/suite_test.go @@ -0,0 +1,80 @@ +/* +Copyright © 2025 Bartłomiej Święcki (byo) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package suite_test + +import ( + "testing" + + "github.com/cinode/go-common/picotestify/suite" +) + +type sampleSuite struct { + suite.Suite + + test1T *testing.T + test2T *testing.T + test3T *testing.T + + setupSuiteCalls int + tearDownSuiteCalls int + + setupTestCalls int + tearDownTestCalls int +} + +func (s *sampleSuite) SetupSuite() { s.setupSuiteCalls++ } +func (s *sampleSuite) TearDownSuite() { s.tearDownSuiteCalls++ } +func (s *sampleSuite) SetupTest() { s.setupTestCalls++ } +func (s *sampleSuite) TearDownTest() { s.tearDownTestCalls++ } + +func (s *sampleSuite) Test1() { s.test1T = s.T() } +func (s *sampleSuite) NotATest2() { s.test2T = s.T() } +func (s *sampleSuite) Test3() { s.test3T = s.T() } + +func TestSuite(t *testing.T) { + s := sampleSuite{} + + suite.Run(t, &s) + + if s.test1T == nil { + t.Errorf("Test1 was not called") + } + + if s.test2T != nil { + t.Errorf("NotATest2 was called") + } + + if s.test3T == nil { + t.Errorf("Test3 was not called") + } + + if s.setupSuiteCalls != 1 { + t.Errorf("SetupSuite was called %d times, expected 1", s.setupSuiteCalls) + } + + if s.tearDownSuiteCalls != 1 { + t.Errorf("TearDownSuite was called %d times, expected 1", s.tearDownSuiteCalls) + } + + if s.setupTestCalls != 2 { + t.Errorf("SetupTest was called %d times, expected 2", s.setupTestCalls) + } + + if s.tearDownTestCalls != 2 { + t.Errorf("TearDownTest was called %d times, expected 2", s.tearDownTestCalls) + } +}