From 7a6962a0eeeb17322a70bdf423b6f1b6a31addc2 Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Tue, 8 Oct 2024 22:11:06 +0200 Subject: [PATCH] add cancelable context variable syntax sugar to `let` package --- let/As.go | 26 ------ let/As_test.go | 50 ----------- let/With.go | 26 ------ let/With_test.go | 43 ---------- let/{std.go => let.go} | 74 ++++++++++++++++ let/let_test.go | 188 +++++++++++++++++++++++++++++++++++++++++ let/person.go | 32 ------- let/person_test.go | 28 ------ let/std_test.go | 84 ------------------ 9 files changed, 262 insertions(+), 289 deletions(-) delete mode 100644 let/As.go delete mode 100644 let/As_test.go delete mode 100644 let/With.go delete mode 100644 let/With_test.go rename let/{std.go => let.go} (50%) create mode 100644 let/let_test.go delete mode 100644 let/person.go delete mode 100644 let/person_test.go delete mode 100644 let/std_test.go diff --git a/let/As.go b/let/As.go deleted file mode 100644 index 28c3192..0000000 --- a/let/As.go +++ /dev/null @@ -1,26 +0,0 @@ -package let - -import ( - "fmt" - "reflect" - - "go.llib.dev/testcase" -) - -func As[To, From any](Var testcase.Var[From]) testcase.Var[To] { - asID++ - fromType := reflect.TypeOf((*From)(nil)).Elem() - toType := reflect.TypeOf((*To)(nil)).Elem() - if !fromType.ConvertibleTo(toType) { - panic(fmt.Sprintf("you can't have %s as %s", fromType.String(), toType.String())) - } - return testcase.Var[To]{ - ID: fmt.Sprintf("%s AS %T #%d", Var.ID, *new(To), asID), - Init: func(t *testcase.T) To { - var rFrom = reflect.ValueOf(Var.Get(t)) - return rFrom.Convert(toType).Interface().(To) - }, - } -} - -var asID int // adds extra safety that there won't be a name collision between two variables diff --git a/let/As_test.go b/let/As_test.go deleted file mode 100644 index c122801..0000000 --- a/let/As_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package let_test - -import ( - "testing" - "time" - - "go.llib.dev/testcase" - "go.llib.dev/testcase/assert" - "go.llib.dev/testcase/let" - "go.llib.dev/testcase/sandbox" -) - -func TestAs(t *testing.T) { - t.Run("primitive type", func(t *testing.T) { - type MyString string - - s := testcase.NewSpec(t) - v1 := let.String(s) - v2 := let.As[MyString](v1) - - s.Test("", func(t *testcase.T) { - t.Must.Equal(MyString(v1.Get(t)), v2.Get(t)) - }) - }) - - t.Run("interface type", func(t *testing.T) { - type TimeAfterer interface { - After(u time.Time) bool - } - - s := testcase.NewSpec(t) - v1 := let.Time(s) - v2 := let.As[TimeAfterer](v1) - - s.Test("", func(t *testcase.T) { - t.Must.Equal(TimeAfterer(v1.Get(t)), v2.Get(t)) - }) - }) - - t.Run("panics on incorrect conversation", func(t *testing.T) { - ro := sandbox.Run(func() { - s := testcase.NewSpec(t) - v1 := let.Time(s) - _ = let.As[string](v1) - }) - assert.False(t, ro.OK) - assert.False(t, ro.Goexit) - assert.NotNil(t, ro.PanicValue) - }) -} diff --git a/let/With.go b/let/With.go deleted file mode 100644 index 8340e3f..0000000 --- a/let/With.go +++ /dev/null @@ -1,26 +0,0 @@ -package let - -import ( - "testing" - - "go.llib.dev/testcase" -) - -func With[V any, FN withFN[V]](s *testcase.Spec, fn FN) testcase.Var[V] { - var init testcase.VarInit[V] - switch fnv := any(fn).(type) { - case func() V: - init = func(t *testcase.T) V { return fnv() } - case func(testing.TB) V: - init = func(t *testcase.T) V { return fnv(t) } - case func(*testcase.T) V: - init = fnv - } - return testcase.Let(s, init) -} - -type withFN[V any] interface { - func() V | - func(testing.TB) V | - func(*testcase.T) V -} diff --git a/let/With_test.go b/let/With_test.go deleted file mode 100644 index 760a183..0000000 --- a/let/With_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package let_test - -import ( - "testing" - - "go.llib.dev/testcase" - "go.llib.dev/testcase/let" - "go.llib.dev/testcase/random" -) - -func TestWith(t *testing.T) { - rnd := random.New(random.CryptoSeed{}) - t.Run("func() V", func(t *testing.T) { - s := testcase.NewSpec(t) - n := rnd.Int() - v := let.With[int](s, func() int { - return n - }) - s.Test("", func(t *testcase.T) { - t.Must.Equal(n, v.Get(t)) - }) - }) - t.Run("func(testing.TB) V", func(t *testing.T) { - s := testcase.NewSpec(t) - n := rnd.String() - v := let.With[string](s, func(testing.TB) string { - return n - }) - s.Test("", func(t *testcase.T) { - t.Must.Equal(n, v.Get(t)) - }) - }) - t.Run("func(*testcase.T) V", func(t *testing.T) { - s := testcase.NewSpec(t) - n := let.UUID(s) - v := let.With[string](s, func(t *testcase.T) string { - return n.Get(t) - }) - s.Test("", func(t *testcase.T) { - t.Must.Equal(n.Get(t), v.Get(t)) - }) - }) -} diff --git a/let/std.go b/let/let.go similarity index 50% rename from let/std.go rename to let/let.go index c92e54c..3459d17 100644 --- a/let/std.go +++ b/let/let.go @@ -2,11 +2,53 @@ package let import ( "context" + "fmt" + "reflect" + "testing" "time" "go.llib.dev/testcase" + "go.llib.dev/testcase/internal" + "go.llib.dev/testcase/random" ) +func With[V any, FN withFN[V]](s *testcase.Spec, fn FN) testcase.Var[V] { + var init testcase.VarInit[V] + switch fnv := any(fn).(type) { + case func() V: + init = func(t *testcase.T) V { return fnv() } + case func(testing.TB) V: + init = func(t *testcase.T) V { return fnv(t) } + case func(*testcase.T) V: + init = fnv + } + return testcase.Let(s, init) +} + +type withFN[V any] interface { + func() V | + func(testing.TB) V | + func(*testcase.T) V +} + +func As[To, From any](Var testcase.Var[From]) testcase.Var[To] { + asID++ + fromType := reflect.TypeOf((*From)(nil)).Elem() + toType := reflect.TypeOf((*To)(nil)).Elem() + if !fromType.ConvertibleTo(toType) { + panic(fmt.Sprintf("you can't have %s as %s", fromType.String(), toType.String())) + } + return testcase.Var[To]{ + ID: fmt.Sprintf("%s AS %T #%d", Var.ID, *new(To), asID), + Init: func(t *testcase.T) To { + var rFrom = reflect.ValueOf(Var.Get(t)) + return rFrom.Convert(toType).Interface().(To) + }, + } +} + +var asID int // adds extra safety that there won't be a name collision between two variables + func Context(s *testcase.Spec) testcase.Var[context.Context] { return testcase.Let(s, func(t *testcase.T) context.Context { ctx, cancel := context.WithCancel(context.Background()) @@ -15,6 +57,14 @@ func Context(s *testcase.Spec) testcase.Var[context.Context] { }) } +func ContextWithCancel(s *testcase.Spec) (testcase.Var[context.Context], testcase.Var[func()]) { + return testcase.Let2(s, func(t *testcase.T) (context.Context, func()) { + ctx, cancel := context.WithCancel(context.Background()) + t.Defer(cancel) + return ctx, cancel + }) +} + func Error(s *testcase.Spec) testcase.Var[error] { return testcase.Let(s, func(t *testcase.T) error { return t.Random.Error() @@ -86,3 +136,27 @@ func DurationBetween(s *testcase.Spec, min, max time.Duration) testcase.Var[time return t.Random.DurationBetween(min, max) }) } + +func Contact(s *testcase.Spec, opts ...internal.ContactOption) testcase.Var[random.Contact] { + return testcase.Let[random.Contact](s, func(t *testcase.T) random.Contact { + return t.Random.Contact(opts...) + }) +} + +func FirstName(s *testcase.Spec, opts ...internal.ContactOption) testcase.Var[string] { + return testcase.Let(s, func(t *testcase.T) string { + return t.Random.Contact(opts...).FirstName + }) +} + +func LastName(s *testcase.Spec) testcase.Var[string] { + return testcase.Let(s, func(t *testcase.T) string { + return t.Random.Contact().LastName + }) +} + +func Email(s *testcase.Spec) testcase.Var[string] { + return testcase.Let(s, func(t *testcase.T) string { + return t.Random.Contact().Email + }) +} diff --git a/let/let_test.go b/let/let_test.go new file mode 100644 index 0000000..c63ead8 --- /dev/null +++ b/let/let_test.go @@ -0,0 +1,188 @@ +package let_test + +import ( + "context" + "testing" + "time" + + "go.llib.dev/testcase" + "go.llib.dev/testcase/assert" + "go.llib.dev/testcase/let" + "go.llib.dev/testcase/random" + "go.llib.dev/testcase/random/sextype" + "go.llib.dev/testcase/sandbox" +) + +func TestWith(t *testing.T) { + rnd := random.New(random.CryptoSeed{}) + t.Run("func() V", func(t *testing.T) { + s := testcase.NewSpec(t) + n := rnd.Int() + v := let.With[int](s, func() int { + return n + }) + s.Test("", func(t *testcase.T) { + t.Must.Equal(n, v.Get(t)) + }) + }) + t.Run("func(testing.TB) V", func(t *testing.T) { + s := testcase.NewSpec(t) + n := rnd.String() + v := let.With[string](s, func(testing.TB) string { + return n + }) + s.Test("", func(t *testcase.T) { + t.Must.Equal(n, v.Get(t)) + }) + }) + t.Run("func(*testcase.T) V", func(t *testing.T) { + s := testcase.NewSpec(t) + n := let.UUID(s) + v := let.With[string](s, func(t *testcase.T) string { + return n.Get(t) + }) + s.Test("", func(t *testcase.T) { + t.Must.Equal(n.Get(t), v.Get(t)) + }) + }) +} + +func TestAs(t *testing.T) { + t.Run("primitive type", func(t *testing.T) { + type MyString string + + s := testcase.NewSpec(t) + v1 := let.String(s) + v2 := let.As[MyString](v1) + + s.Test("", func(t *testcase.T) { + t.Must.Equal(MyString(v1.Get(t)), v2.Get(t)) + }) + }) + + t.Run("interface type", func(t *testing.T) { + type TimeAfterer interface { + After(u time.Time) bool + } + + s := testcase.NewSpec(t) + v1 := let.Time(s) + v2 := let.As[TimeAfterer](v1) + + s.Test("", func(t *testcase.T) { + t.Must.Equal(TimeAfterer(v1.Get(t)), v2.Get(t)) + }) + }) + + t.Run("panics on incorrect conversation", func(t *testing.T) { + ro := sandbox.Run(func() { + s := testcase.NewSpec(t) + v1 := let.Time(s) + _ = let.As[string](v1) + }) + assert.False(t, ro.OK) + assert.False(t, ro.Goexit) + assert.NotNil(t, ro.PanicValue) + }) +} + +func Test_smoke(t *testing.T) { + s := testcase.NewSpec(t) + + Context := let.Context(s) + Error := let.Error(s) + String := let.String(s) + StringNC := let.StringNC(s, 42, random.CharsetASCII()) + Bool := let.Bool(s) + Int := let.Int(s) + IntN := let.IntN(s, 42) + IntB := let.IntB(s, 7, 42) + Time := let.Time(s) + TimeB := let.TimeB(s, time.Now().AddDate(-1, 0, 0), time.Now()) + UUID := let.UUID(s) + Element := let.ElementFrom[string](s, "foo", "bar", "baz") + DurationBetween := let.DurationBetween(s, time.Second, time.Minute) + + charsterIs := func(t *testcase.T, cs, str string) { + for _, v := range str { + t.Must.Contain(cs, string(v)) + } + } + + s.Test("", func(t *testcase.T) { + t.Must.NotNil(Context.Get(t)) + t.Must.NoError(Context.Get(t).Err()) + t.Must.NotWithin(time.Millisecond, func(ctx context.Context) { + select { + case <-Context.Get(t).Done(): + // expect to block + case <-ctx.Done(): + // will be done after the assertion + } + }) + t.Must.Error(Error.Get(t)) + t.Must.NotEmpty(String.Get(t)) + t.Must.NotEmpty(StringNC.Get(t)) + t.Must.True(len(StringNC.Get(t)) == 42) + charsterIs(t, random.CharsetASCII(), StringNC.Get(t)) + t.Must.NotEmpty(Int.Get(t)) + t.Eventually(func(t *testcase.T) { + t.Must.NotEmpty(IntN.Get(testcase.ToT(&t.TB))) + }) + t.Must.NotEmpty(IntB.Get(t)) + t.Must.NotEmpty(DurationBetween.Get(t)) + t.Must.True(time.Second <= DurationBetween.Get(t)) + t.Must.True(DurationBetween.Get(t) <= time.Minute) + t.Must.NotEmpty(Time.Get(t)) + t.Must.NotEmpty(TimeB.Get(t)) + t.Must.True(TimeB.Get(t).After(time.Now().AddDate(-1, 0, -1))) + t.Must.NotEmpty(UUID.Get(t)) + t.Must.NotEmpty(Element.Get(t)) + t.Eventually(func(it *testcase.T) { + it.Must.True(Bool.Get(testcase.ToT(&t.TB))) + }) + }) +} + +func TestContext_cancellationDuringCleanup(t *testing.T) { + s := testcase.NewSpec(t) + s.Sequential() + ctxVar := let.Context(s) + var ctx context.Context + s.Test("", func(t *testcase.T) { + ctx = ctxVar.Get(t) + t.Must.NoError(ctx.Err()) + }) + s.Finish() + assert.NotNil(t, ctx) + assert.ErrorIs(t, context.Canceled, ctx.Err()) +} + +func TestContextWithCancel(t *testing.T) { + s := testcase.NewSpec(t) + ctxVar, cancelVar := let.ContextWithCancel(s) + s.Test("", func(t *testcase.T) { + assert.NoError(t, ctxVar.Get(t).Err()) + cancelVar.Get(t)() + assert.ErrorIs(t, ctxVar.Get(t).Err(), context.Canceled) + }) +} + +func TestPerson_smoke(t *testing.T) { + s := testcase.NewSpec(t) + + fn := let.FirstName(s) + ln := let.LastName(s) + mfn := let.FirstName(s, sextype.Male) + em := let.Email(s) + + s.Test("", func(t *testcase.T) { + t.Must.NotEmpty(fn.Get(t)) + t.Must.NotEmpty(ln.Get(t)) + t.Must.NotEmpty(mfn.Get(t)) + t.Must.NotEmpty(em.Get(t)) + t.Eventually(func(it *testcase.T) { + it.Must.Equal(t.Random.Contact(sextype.Male).FirstName, mfn.Get(t)) + }) + }) +} diff --git a/let/person.go b/let/person.go deleted file mode 100644 index 036c5f9..0000000 --- a/let/person.go +++ /dev/null @@ -1,32 +0,0 @@ -// Package let contains Common Testcase Variable Let declarations for testing purpose. -package let - -import ( - "go.llib.dev/testcase" - "go.llib.dev/testcase/internal" - "go.llib.dev/testcase/random" -) - -func Contact(s *testcase.Spec, opts ...internal.ContactOption) testcase.Var[random.Contact] { - return testcase.Let[random.Contact](s, func(t *testcase.T) random.Contact { - return t.Random.Contact(opts...) - }) -} - -func FirstName(s *testcase.Spec, opts ...internal.ContactOption) testcase.Var[string] { - return testcase.Let(s, func(t *testcase.T) string { - return t.Random.Contact(opts...).FirstName - }) -} - -func LastName(s *testcase.Spec) testcase.Var[string] { - return testcase.Let(s, func(t *testcase.T) string { - return t.Random.Contact().LastName - }) -} - -func Email(s *testcase.Spec) testcase.Var[string] { - return testcase.Let(s, func(t *testcase.T) string { - return t.Random.Contact().Email - }) -} diff --git a/let/person_test.go b/let/person_test.go deleted file mode 100644 index 14e41cf..0000000 --- a/let/person_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package let_test - -import ( - "testing" - - "go.llib.dev/testcase" - "go.llib.dev/testcase/let" - "go.llib.dev/testcase/random/sextype" -) - -func TestPerson_smoke(t *testing.T) { - s := testcase.NewSpec(t) - - fn := let.FirstName(s) - ln := let.LastName(s) - mfn := let.FirstName(s, sextype.Male) - em := let.Email(s) - - s.Test("", func(t *testcase.T) { - t.Must.NotEmpty(fn.Get(t)) - t.Must.NotEmpty(ln.Get(t)) - t.Must.NotEmpty(mfn.Get(t)) - t.Must.NotEmpty(em.Get(t)) - t.Eventually(func(it *testcase.T) { - it.Must.Equal(t.Random.Contact(sextype.Male).FirstName, mfn.Get(t)) - }) - }) -} diff --git a/let/std_test.go b/let/std_test.go deleted file mode 100644 index e0a7715..0000000 --- a/let/std_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package let_test - -import ( - "context" - "testing" - "time" - - "go.llib.dev/testcase" - "go.llib.dev/testcase/assert" - "go.llib.dev/testcase/let" - "go.llib.dev/testcase/random" -) - -func TestSTD_smoke(t *testing.T) { - s := testcase.NewSpec(t) - - Context := let.Context(s) - Error := let.Error(s) - String := let.String(s) - StringNC := let.StringNC(s, 42, random.CharsetASCII()) - Bool := let.Bool(s) - Int := let.Int(s) - IntN := let.IntN(s, 42) - IntB := let.IntB(s, 7, 42) - Time := let.Time(s) - TimeB := let.TimeB(s, time.Now().AddDate(-1, 0, 0), time.Now()) - UUID := let.UUID(s) - Element := let.ElementFrom[string](s, "foo", "bar", "baz") - DurationBetween := let.DurationBetween(s, time.Second, time.Minute) - - charsterIs := func(t *testcase.T, cs, str string) { - for _, v := range str { - t.Must.Contain(cs, string(v)) - } - } - - s.Test("", func(t *testcase.T) { - t.Must.NotNil(Context.Get(t)) - t.Must.NoError(Context.Get(t).Err()) - t.Must.NotWithin(time.Millisecond, func(ctx context.Context) { - select { - case <-Context.Get(t).Done(): - // expect to block - case <-ctx.Done(): - // will be done after the assertion - } - }) - t.Must.Error(Error.Get(t)) - t.Must.NotEmpty(String.Get(t)) - t.Must.NotEmpty(StringNC.Get(t)) - t.Must.True(len(StringNC.Get(t)) == 42) - charsterIs(t, random.CharsetASCII(), StringNC.Get(t)) - t.Must.NotEmpty(Int.Get(t)) - t.Eventually(func(t *testcase.T) { - t.Must.NotEmpty(IntN.Get(testcase.ToT(&t.TB))) - }) - t.Must.NotEmpty(IntB.Get(t)) - t.Must.NotEmpty(DurationBetween.Get(t)) - t.Must.True(time.Second <= DurationBetween.Get(t)) - t.Must.True(DurationBetween.Get(t) <= time.Minute) - t.Must.NotEmpty(Time.Get(t)) - t.Must.NotEmpty(TimeB.Get(t)) - t.Must.True(TimeB.Get(t).After(time.Now().AddDate(-1, 0, -1))) - t.Must.NotEmpty(UUID.Get(t)) - t.Must.NotEmpty(Element.Get(t)) - t.Eventually(func(it *testcase.T) { - it.Must.True(Bool.Get(testcase.ToT(&t.TB))) - }) - }) -} - -func TestContext_cancellationDuringCleanup(t *testing.T) { - s := testcase.NewSpec(t) - s.Sequential() - ctxVar := let.Context(s) - var ctx context.Context - s.Test("", func(t *testcase.T) { - ctx = ctxVar.Get(t) - t.Must.NoError(ctx.Err()) - }) - s.Finish() - assert.NotNil(t, ctx) - assert.ErrorIs(t, context.Canceled, ctx.Err()) -}