From 60ede9b424e30f21a35920321e616b61eb633066 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Wed, 2 Oct 2024 22:40:23 -0300 Subject: [PATCH 1/3] fn: fix UnwrapOrFail success case Return the stored value, not zero. --- fn/result.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fn/result.go b/fn/result.go index 93d2dd7d66b..3483b0b0315 100644 --- a/fn/result.go +++ b/fn/result.go @@ -118,9 +118,8 @@ func (r Result[T]) UnwrapOrFail(t *testing.T) T { t.Fatalf("Result[%T] contained error: %v", r.left, r.right) } - var zero T - return zero + return r.left } // FlatMap applies a function that returns a Result to the success value if it From df4a1ddd415ecb595c1e196d8e2127a3b989ba0d Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Wed, 2 Oct 2024 22:55:52 -0300 Subject: [PATCH 2/3] fn: update "testify" to v1.9.0 This is needed to have require.EventuallyWithT function. --- fn/go.mod | 2 +- fn/go.sum | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/fn/go.mod b/fn/go.mod index 1780b996832..8dafcff4aaa 100644 --- a/fn/go.mod +++ b/fn/go.mod @@ -3,7 +3,7 @@ module github.com/lightningnetwork/lnd/fn go 1.19 require ( - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b golang.org/x/sync v0.7.0 ) diff --git a/fn/go.sum b/fn/go.sum index 86f138bc53a..fa0f334ca0c 100644 --- a/fn/go.sum +++ b/fn/go.sum @@ -1,21 +1,14 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 246ddc86a2ebcace05c34c425aede032bd121e1f Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Wed, 18 Sep 2024 22:27:56 -0300 Subject: [PATCH 3/3] fn: generalize type of t in UnwrapOrFail It is needed to pass other types satisfying needed interfaces to methods Option.UnwrapOrFail and Result.UnwrapOrFail. An example of such a type is *lntest.HarnessTest. It embeds *testing.T and has all the methods but can't be passed directly as *testing.T, but can be passed now as an instance of Testing interface. Also *testing.B and assert.CollectT (from "require" package). Methods Helper() and FailNow() are optional and they are called if exist. Also added the test for UnwrapOrFail methods of Option and Result. --- fn/option.go | 14 +++++++++----- fn/option_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ fn/result.go | 14 +++++++++----- fn/result_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ fn/testing.go | 32 ++++++++++++++++++++++++++++++++ fn/testing_test.go | 25 +++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 fn/option_test.go create mode 100644 fn/result_test.go create mode 100644 fn/testing.go create mode 100644 fn/testing_test.go diff --git a/fn/option.go b/fn/option.go index 797f3a0ff0d..a737eaa6c3d 100644 --- a/fn/option.go +++ b/fn/option.go @@ -1,7 +1,5 @@ package fn -import "testing" - // Option[A] represents a value which may or may not be there. This is very // often preferable to nil-able pointers. type Option[A any] struct { @@ -58,14 +56,20 @@ func (o Option[A]) UnwrapOrFunc(f func() A) A { // UnwrapOrFail is used to extract a value from an option within a test // context. If the option is None, then the test fails. -func (o Option[A]) UnwrapOrFail(t *testing.T) A { - t.Helper() +func (o Option[A]) UnwrapOrFail(t TestingT) A { + if helper, ok := t.(TestingHelper); ok { + helper.Helper() + } if o.isSome { return o.some } - t.Fatalf("Option[%T] was None()", o.some) + t.Errorf("Option[%T] was None()", o.some) + + if failer, ok := t.(TestingFailer); ok { + failer.FailNow() + } var zero A return zero diff --git a/fn/option_test.go b/fn/option_test.go new file mode 100644 index 00000000000..87433aaa03f --- /dev/null +++ b/fn/option_test.go @@ -0,0 +1,40 @@ +package fn + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOption(t *testing.T) { + t.Run("UnwrapOrFail", func(t *testing.T) { + // Test Some case with real t. + opt := Some(123) + n := opt.UnwrapOrFail(t) + require.Equal(t, 123, n) + + // Test Some case with mock t. + mockT := &testingMock{} + n = opt.UnwrapOrFail(mockT) + require.Equal(t, 123, n) + require.True(t, mockT.helperCalled) + require.False(t, mockT.errorfCalled) + require.False(t, mockT.failNowCalled) + + // Make sure it works with assert.CollectT. + require.EventuallyWithT(t, func(t *assert.CollectT) { + n = opt.UnwrapOrFail(t) + require.Equal(t, 123, n) + }, time.Second, time.Millisecond) + + // Test None case with mock t. + opt = None[int]() + mockT = &testingMock{} + _ = opt.UnwrapOrFail(mockT) + require.True(t, mockT.helperCalled) + require.True(t, mockT.errorfCalled) + require.True(t, mockT.failNowCalled) + }) +} diff --git a/fn/result.go b/fn/result.go index 3483b0b0315..af8c3fbfa6c 100644 --- a/fn/result.go +++ b/fn/result.go @@ -2,7 +2,6 @@ package fn import ( "fmt" - "testing" ) // Result represents a value that can either be a success (T) or an error. @@ -111,13 +110,18 @@ func (r Result[T]) UnwrapOrElse(f func() T) T { } // UnwrapOrFail returns the success value or fails the test if it's an error. -func (r Result[T]) UnwrapOrFail(t *testing.T) T { - t.Helper() +func (r Result[T]) UnwrapOrFail(t TestingT) T { + if helper, ok := t.(TestingHelper); ok { + helper.Helper() + } if r.IsErr() { - t.Fatalf("Result[%T] contained error: %v", r.left, r.right) - } + t.Errorf("Result[%T] contained error: %v", r.left, r.right) + if failer, ok := t.(TestingFailer); ok { + failer.FailNow() + } + } return r.left } diff --git a/fn/result_test.go b/fn/result_test.go new file mode 100644 index 00000000000..480c634754d --- /dev/null +++ b/fn/result_test.go @@ -0,0 +1,41 @@ +package fn + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResult(t *testing.T) { + t.Run("UnwrapOrFail", func(t *testing.T) { + // Test Ok case with real t. + opt := Ok(123) + n := opt.UnwrapOrFail(t) + require.Equal(t, 123, n) + + // Test Ok case with mock t. + mockT := &testingMock{} + n = opt.UnwrapOrFail(mockT) + require.Equal(t, 123, n) + require.True(t, mockT.helperCalled) + require.False(t, mockT.errorfCalled) + require.False(t, mockT.failNowCalled) + + // Make sure it works with assert.CollectT. + require.EventuallyWithT(t, func(t *assert.CollectT) { + n = opt.UnwrapOrFail(t) + require.Equal(t, 123, n) + }, time.Second, time.Millisecond) + + // Test None case with mock t. + opt = Err[int](errors.New("test error")) + mockT = &testingMock{} + _ = opt.UnwrapOrFail(mockT) + require.True(t, mockT.helperCalled) + require.True(t, mockT.errorfCalled) + require.True(t, mockT.failNowCalled) + }) +} diff --git a/fn/testing.go b/fn/testing.go new file mode 100644 index 00000000000..22cf05269a6 --- /dev/null +++ b/fn/testing.go @@ -0,0 +1,32 @@ +package fn + +// TestingT abstracts *testing.T, *testing.B, assert.TestingT etc types +// that are passed to testing functions. It has only the methods needed +// by Option.UnwrapOrFail and Result.UnwrapOrFail. +type TestingT interface { + // Errorf formats its arguments according to the format, analogous to + // Printf, and records the text in the error log, then marks the + // function as having failed. + Errorf(format string, args ...any) +} + +// TestingHelper abstracts *testing.T, *testing.B etc types that are passed to +// testing functions. It has only Helper method. +type TestingHelper interface { + // Helper marks the calling function as a test helper function. When + // printing file and line information, that function will be skipped. + // Helper may be called simultaneously from multiple goroutines. + Helper() +} + +// TestingFailer abstracts *testing.T, *testing.B etc types that are passed to +// testing functions. It has only FailNow method. +type TestingFailer interface { + // FailNow marks the function as having failed and stops its execution + // by calling runtime.Goexit (which then runs all deferred calls in the + // current goroutine). Execution will continue at the next test or + // benchmark. FailNow must be called from the goroutine running the test + // or benchmark function, not from other goroutines created during the + // test. Calling FailNow does not stop those other goroutines. + FailNow() +} diff --git a/fn/testing_test.go b/fn/testing_test.go new file mode 100644 index 00000000000..0941a2b0571 --- /dev/null +++ b/fn/testing_test.go @@ -0,0 +1,25 @@ +package fn + +// testingMock is a mock used in tests of UnwrapOrFail methods of Option and +// Result. It implements TestingT, TestingHelper, and TestingFailer and just +// records if a method was called. +type testingMock struct { + errorfCalled bool + helperCalled bool + failNowCalled bool +} + +// Errorf records the fact that the method was called. +func (t *testingMock) Errorf(format string, args ...any) { + t.errorfCalled = true +} + +// Helper records the fact that the method was called. +func (t *testingMock) Helper() { + t.helperCalled = true +} + +// FailNow records the fact that the method was called. +func (t *testingMock) FailNow() { + t.failNowCalled = true +}