diff --git a/Spec_bc_test.go b/Spec_bc_test.go index 7f8f6bc..083ec17 100644 --- a/Spec_bc_test.go +++ b/Spec_bc_test.go @@ -16,7 +16,7 @@ func TestSpec_FriendlyVarNotDefined(t *testing.T) { v1 := Let[string](s, func(t *T) string { return `hello-world` }) v2 := Let[string](s, func(t *T) string { return `hello-world` }) - tct := NewT(stub, s) + tct := NewTWithSpec(stub, s) s.Test(`var1 var found`, func(t *T) { assert.Must(t).Equal(`hello-world`, v1.Get(t)) diff --git a/Suite_test.go b/Suite_test.go index cfe4414..b148f96 100644 --- a/Suite_test.go +++ b/Suite_test.go @@ -12,7 +12,7 @@ func TestRunSuite(t *testing.T) { t.Run(`when TB is testing.TB`, func(t *testing.T) { sT := &RunContractContract{} var tb testing.TB = &doubles.TB{} - tb = testcase.NewT(tb, testcase.NewSpec(tb)) + tb = testcase.NewTWithSpec(tb, testcase.NewSpec(tb)) testcase.RunSuite(&tb, sT) assert.Must(t).True(sT.SpecWasCalled) assert.Must(t).True(!sT.TestWasCalled) @@ -53,7 +53,7 @@ func TestRunOpenSuite(t *testing.T) { t.Run(`when TB is *testcase.T with *testing.T under the hood`, func(t *testing.T) { sT := &RunContractOpenContract{} - testcase.RunOpenSuite(testcase.NewT(t, nil), sT) + testcase.RunOpenSuite(testcase.NewTWithSpec(t, nil), sT) assert.Must(t).True(sT.TestWasCalled) assert.Must(t).True(!sT.BenchmarkWasCalled) }) @@ -91,7 +91,7 @@ func BenchmarkTestRunOpenSuite(b *testing.B) { b.Run(`when TB is *testcase.T with *testing.B under the hood`, func(b *testing.B) { sT := &RunContractOpenContract{} - testcase.RunOpenSuite(testcase.NewT(b, nil), sT) + testcase.RunOpenSuite(testcase.NewTWithSpec(b, nil), sT) assert.Must(b).True(!sT.TestWasCalled) assert.Must(b).True(sT.BenchmarkWasCalled) b.SkipNow() diff --git a/T.go b/T.go index be28bb3..3aaea07 100644 --- a/T.go +++ b/T.go @@ -13,7 +13,12 @@ import ( ) // NewT returns a *testcase.T prepared for the given testing.TB -func NewT(tb testing.TB, spec *Spec) *T { +func NewT(tb testing.TB) *T { + return NewTWithSpec(tb, nil) +} + +// NewTWithSpec returns a *testcase.T prepared for the given testing.TB using the context of the passed *Spec. +func NewTWithSpec(tb testing.TB, spec *Spec) *T { if tb == nil { return nil } diff --git a/TB.go b/TB.go index ac7a956..cd1829e 100644 --- a/TB.go +++ b/TB.go @@ -1,8 +1,10 @@ package testcase import ( + "reflect" "testing" + "go.llib.dev/testcase/assert" "go.llib.dev/testcase/internal/doubles" ) @@ -51,7 +53,7 @@ type testingHelper interface { } type anyTB interface { - *T | *testing.T | *testing.B | *doubles.TB | *testing.TB | *TBRunner + *T | *testing.T | *testing.B | *testing.F | *doubles.TB | *testing.TB | *TBRunner | assert.It } type anyTBOrSpec interface { @@ -78,20 +80,57 @@ func ToSpec[TBS anyTBOrSpec](tbs TBS) *Spec { } func ToT[TBs anyTB](tb TBs) *T { + return toT(tb) +} + +func toT(tb any) *T { switch tbs := (any)(tb).(type) { case *T: return tbs case *testing.T: - return NewT(tbs, NewSpec(tbs)) + return NewT(tbs) case *testing.B: - return NewT(tbs, NewSpec(tbs)) + return NewT(tbs) case *doubles.TB: - return NewT(tbs, NewSpec(tbs)) + return NewT(tbs) case *testing.TB: - return NewT(*tbs, NewSpec(*tbs)) + return toT(*tbs) + case assert.It: + return toT(tbs.TB) + case testing.TB: + return NewT(unwrapTestingTB(tbs)) case *TBRunner: - return NewT(*tbs, NewSpec(*tbs)) + return toT(*tbs) default: panic("not implemented") } } + +var reflectTypeTestingTB = reflect.TypeOf((*testing.TB)(nil)).Elem() + +func unwrapTestingTB(tb testing.TB) testing.TB { + rtb := reflect.ValueOf(tb) + if rtb.Kind() == reflect.Pointer { + rtb = rtb.Elem() + } + if rtb.Kind() != reflect.Struct { + return tb + } + + rtbType := rtb.Type() + NumField := rtbType.NumField() + for i := 0; i < NumField; i++ { + fieldType := rtbType.Field(i) + // Implementing testing.TB is only possible when a struct includes an embedded field from the testing package. + // This requirement arises because testing.TB has a private function method expectation that can only be implemented within the testing package scope. + // As a result, we can identify the embedded field regardless of whether it is *testing.T, *testing.B, *testing.F, testing.TB, etc + // by checking if the field itself is an embedded field and implements testing.TB. + if fieldType.Anonymous && fieldType.Type.Implements(reflectTypeTestingTB) { + testingTB, ok := rtb.Field(i).Interface().(testing.TB) + if ok && testingTB != nil { + return testingTB + } + } + } + return tb +} diff --git a/TB_test.go b/TB_test.go index 7b80c3c..53479db 100644 --- a/TB_test.go +++ b/TB_test.go @@ -28,7 +28,7 @@ func TestToSpec_smoke(t *testing.T) { assertToSpec(t, ToSpec(NewSpec(t))) assertToSpec(t, ToSpec(t)) - assertToSpec(t, ToSpec(NewT(t, nil))) + assertToSpec(t, ToSpec(NewTWithSpec(t, nil))) dtb := &doubles.TB{} assertToSpec(t, ToSpec(dtb)) @@ -54,5 +54,70 @@ func BenchmarkTestToSpec(b *testing.B) { } func TestToT(t *testing.T) { - + t.Run("*testcase.T", func(t *testing.T) { + tc := NewT(t) + assert.Equal(t, tc, ToT(tc)) + }) + t.Run("*testing.TB", func(t *testing.T) { + tc := NewT(t) + var tb testing.TB = tc + assert.Equal(t, tc, ToT(&tb)) + }) + t.Run("*testing.T", func(t *testing.T) { + tc := ToT(t) + assert.NotNil(t, tc) + assert.Equal[testing.TB](t, tc.TB, t) + }) + t.Run("*testing.F", func(t *testing.T) { + f := &testing.F{} + tc := ToT(f) + assert.NotNil(t, tc) + assert.Equal[testing.TB](t, f, tc.TB) + }) + t.Run("*doubles.TB", func(t *testing.T) { + dtb := &doubles.TB{} + tc := ToT(dtb) + assert.NotNil(t, tc) + tc.Log("ok") + assert.NotEmpty(t, dtb.Logs.String()) + }) + t.Run("*TBRunner", func(t *testing.T) { + dtb := &doubles.TB{} + var tbr TBRunner = dtb + tc := ToT(&tbr) + tc.Log("ok") + assert.NotEmpty(t, dtb.Logs.String()) + }) + t.Run("assert.It", func(t *testing.T) { + otc := NewT(t) + var it = assert.MakeIt(otc) + tc := ToT(it) + assert.Equal(t, otc, tc) + }) + t.Run("type that implements testing.TB and passed as *testing.TB", func(t *testing.T) { + type STB struct{ testing.TB } + dtb := &doubles.TB{} + stb := STB{TB: dtb} + var tb testing.TB = stb + tc := ToT(&tb) + tc.Log("ok") + assert.NotEmpty(t, dtb.Logs.String()) + }) + t.Run("type that implements testing.TB and has *testcase.T used as TB", func(t *testing.T) { + type STB struct{ testing.TB } + tc := NewT(t) + stb := STB{TB: tc} + var tb testing.TB = stb + got := ToT(&tb) + assert.Equal(t, tc, got) + var tb2 testing.TB = &testing.F{} + assert.NotNil(t, ToT(&tb2)) + }) + t.Run("type that implements testing.TB usign an uninitialised testing.TB embeded field", func(t *testing.T) { + dtb := &doubles.TB{} + rtb := &doubles.RecorderTB{TB: dtb} + var tb testing.TB = rtb + tc := ToT(&tb) + assert.Equal[testing.TB](t, tc.TB, dtb) + }) } diff --git a/T_test.go b/T_test.go index 66f89a2..1e85028 100644 --- a/T_test.go +++ b/T_test.go @@ -27,7 +27,7 @@ func TestT_implementsTestingTB(t *testing.T) { Subject: func(t *testcase.T) testing.TB { stub := &doubles.TB{} t.Cleanup(stub.Finish) - return testcase.NewT(stub, nil) + return testcase.NewTWithSpec(stub, nil) }, }) } @@ -520,7 +520,17 @@ func TestT_Eventually(t *testing.T) { }) } -func TestNewT(t *testing.T) { +func ExampleNewTWithSpec() { + s := testcase.NewSpec(nil) + // some spec specific configuration + s.Before(func(t *testcase.T) {}) + + var tb testing.TB // placeholder + tc := testcase.NewTWithSpec(tb, s) + _ = tc +} + +func TestNewTWithSpec(t *testing.T) { rnd := random.New(random.CryptoSeed{}) y := testcase.Var[int]{ID: "Y"} v := testcase.Var[int]{ @@ -533,7 +543,7 @@ func TestNewT(t *testing.T) { s := testcase.NewSpec(tb) expectedY := rnd.Int() y.LetValue(s, expectedY) - subject := testcase.NewT(tb, s) + subject := testcase.NewTWithSpec(tb, s) assert.Must(t).Equal(expectedY, y.Get(subject), "use the passed spec's runtime context after set-up") assert.Must(t).Equal(v.Get(subject), v.Get(subject), `has test variable cache`) }) @@ -541,7 +551,7 @@ func TestNewT(t *testing.T) { tb := &doubles.TB{} t.Cleanup(tb.Finish) expectedY := rnd.Int() - subject := testcase.NewT(tb, nil) + subject := testcase.NewTWithSpec(tb, nil) y.Set(subject, expectedY) assert.Must(t).Equal(expectedY, y.Get(subject)) assert.Must(t).Equal(v.Get(subject), v.Get(subject), `has test variable cache`) @@ -549,12 +559,12 @@ func TestNewT(t *testing.T) { t.Run(`with *testcase.T, same returned`, func(t *testing.T) { tb := &doubles.TB{} t.Cleanup(tb.Finish) - tcT1 := testcase.NewT(tb, nil) - tcT2 := testcase.NewT(tcT1, nil) + tcT1 := testcase.NewTWithSpec(tb, nil) + tcT2 := testcase.NewTWithSpec(tcT1, nil) assert.Must(t).Equal(tcT1, tcT2) }) t.Run(`when nil received, nil is returned`, func(t *testing.T) { - assert.Must(t).Nil(testcase.NewT(nil, nil)) + assert.Must(t).Nil(testcase.NewTWithSpec(nil, nil)) }) t.Run(`when NewT is retrieved multiple times, hooks executed only once`, func(t *testing.T) { stb := &doubles.TB{} @@ -563,14 +573,47 @@ func TestNewT(t *testing.T) { s.Before(func(t *testcase.T) { out = append(out, struct{}{}) }) - tct := testcase.NewT(stb, s) - tct = testcase.NewT(tct, s) - tct = testcase.NewT(tct, s) + tct := testcase.NewTWithSpec(stb, s) + tct = testcase.NewTWithSpec(tct, s) + tct = testcase.NewTWithSpec(tct, s) stb.Finish() assert.Equal(t, 1, len(out)) }) } +func ExampleNewT() { + var tb testing.TB // placeholder + _ = testcase.NewT(tb) +} + +func TestNewT(t *testing.T) { + rnd := random.New(random.CryptoSeed{}) + y := testcase.Var[int]{ID: "Y"} + v := testcase.Var[int]{ + ID: "the answer", + Init: func(t *testcase.T) int { return t.Random.Int() }, + } + t.Run(`smoke`, func(t *testing.T) { + tb := &doubles.TB{} + t.Cleanup(tb.Finish) + expectedY := rnd.Int() + subject := testcase.NewT(tb) + y.Set(subject, expectedY) + assert.Must(t).Equal(expectedY, y.Get(subject)) + assert.Must(t).Equal(v.Get(subject), v.Get(subject), `has test variable cache`) + }) + t.Run(`with *testcase.T, same returned`, func(t *testing.T) { + tb := &doubles.TB{} + t.Cleanup(tb.Finish) + tcT1 := testcase.NewT(tb) + tcT2 := testcase.NewT(tcT1) + assert.Must(t).Equal(tcT1, tcT2) + }) + t.Run(`when nil received, nil is returned`, func(t *testing.T) { + assert.Must(t).Nil(testcase.NewT(nil)) + }) +} + func BenchmarkT_varDoesNotCountTowardsRun(b *testing.B) { s := testcase.NewSpec(b) diff --git a/Var_test.go b/Var_test.go index 5129c31..890c21c 100644 --- a/Var_test.go +++ b/Var_test.go @@ -30,7 +30,7 @@ func TestVar(t *testing.T) { stub := &doubles.TB{} willFatal := willFatalWithMessageFn(stub) willFatalWithVariableNotFoundMessage := func(s *testcase.Spec, tb testing.TB, varName string, blk func(*testcase.T)) { - tct := testcase.NewT(stub, s) + tct := testcase.NewTWithSpec(stub, s) assert.Must(tb).Contain(willFatal(t, func() { blk(tct) }), fmt.Sprintf("Variable %q is not found.", varName)) } @@ -208,7 +208,7 @@ func TestVar(t *testing.T) { }) willFatalWithOnLetMissing := func(s *testcase.Spec, tb testing.TB, varName string, blk func(*testcase.T)) { - tct := testcase.NewT(stub, s) + tct := testcase.NewTWithSpec(stub, s) assert.Must(tb).Contain(willFatal(t, func() { blk(tct) }), fmt.Sprintf("%s Var has Var.OnLet. You must use Var.Let, Var.LetValue to initialize it properly.", varName)) } @@ -1069,7 +1069,7 @@ func TestVar_Before(t *testing.T) { func TestVar_missingID(t *testing.T) { varWithoutID := testcase.Var[string]{} stub := &doubles.TB{} - tct := testcase.NewT(stub, nil) + tct := testcase.NewTWithSpec(stub, nil) assert.Panic(t, func() { _ = varWithoutID.Get(tct) }) assert.Contain(t, stub.Logs.String(), "ID for testcase.Var[string] is missing. Maybe it's uninitialized?") } @@ -1112,7 +1112,7 @@ func TestVar_dependencies(t *testing.T) { dtb := &doubles.TB{} s := testcase.NewSpec(dtb) - tct := testcase.NewT(dtb, s) + tct := testcase.NewTWithSpec(dtb, s) var n int assert.NotPanic(t, func() { n = v1.Get(tct) }) @@ -1147,7 +1147,7 @@ func TestVar_dependencies(t *testing.T) { t.Run("it fails if the dependent variable is not bound to the Spec", func(t *testing.T) { dtb := &doubles.TB{} s := testcase.NewSpec(dtb) - tct := testcase.NewT(dtb, s) + tct := testcase.NewTWithSpec(dtb, s) assert.Panic(t, func() { v2.Get(tct) }) assert.True(t, dtb.Failed()) @@ -1157,7 +1157,7 @@ func TestVar_dependencies(t *testing.T) { dtb := &doubles.TB{} s := testcase.NewSpec(dtb) v2.Bind(s) - tct := testcase.NewT(dtb, s) + tct := testcase.NewTWithSpec(dtb, s) assert.NotPanic(t, func() { v2.Get(tct) }) assert.False(t, dtb.Failed()) @@ -1199,7 +1199,7 @@ func TestVar_dependencies(t *testing.T) { dtb := &doubles.TB{} s := testcase.NewSpec(dtb) v3.Bind(s) - tct := testcase.NewT(dtb, s) + tct := testcase.NewTWithSpec(dtb, s) assert.False(t, okV1) assert.NotPanic(t, func() { v3.Get(tct) }) diff --git a/clock/Clock_test.go b/clock/Clock_test.go index 2a1e4a9..42959df 100644 --- a/clock/Clock_test.go +++ b/clock/Clock_test.go @@ -297,7 +297,7 @@ func TestNewTicker(t *testing.T) { var adjust = func(n int64) int64 { return int64(float64(n) * failureRateMultiplier) } - s := testcase.NewSpec(t) + s := testcase.NewSpec(t, testcase.Flaky(2)) duration := testcase.Let[time.Duration](s, nil) ticker := testcase.Let(s, func(t *testcase.T) *clock.Ticker { diff --git a/docs/testing-double/fake_test.go b/docs/testing-double/fake_test.go index 55d90b6..71eca94 100644 --- a/docs/testing-double/fake_test.go +++ b/docs/testing-double/fake_test.go @@ -41,7 +41,7 @@ func TestFakeXYEntityStorage_suppliesXYStorageContract(t *testing.T) { return context.Background() }, MakeXY: func(tb testing.TB) *XY { - t := testcase.NewT(tb, nil) + t := testcase.NewTWithSpec(tb, nil) return t.Random.Make(new(XY)).(*XY) }, }.Test(t) diff --git a/dsl/let.go b/dsl/dsl.go similarity index 100% rename from dsl/let.go rename to dsl/dsl.go diff --git a/dsl/let_test.go b/dsl/dsl_test.go similarity index 100% rename from dsl/let_test.go rename to dsl/dsl_test.go diff --git a/examples_test.go b/examples_test.go index ce7d864..f265a57 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1095,14 +1095,14 @@ func ExampleFlaky_retryNTimes() { }, testcase.Flaky(42)) } -func ExampleNewT() { +func ExampleNewTWithSpec_withVariables() { variable := testcase.Var[int]{ID: "variable", Init: func(t *testcase.T) int { return t.Random.Int() }} // flat test case with test runtime variable caching var tb testing.TB - t := testcase.NewT(tb, testcase.NewSpec(tb)) + t := testcase.NewTWithSpec(tb, testcase.NewSpec(tb)) value1 := variable.Get(t) value2 := variable.Get(t) t.Logf(`test case variable caching works even in flattened tests: v1 == v2 -> %v`, value1 == value2) diff --git a/faultinject/fault_test.go b/faultinject/fault_test.go index 20549e5..8a0576f 100644 --- a/faultinject/fault_test.go +++ b/faultinject/fault_test.go @@ -107,7 +107,7 @@ func (d ctxThatWillNeverGetsDone) Err() error { } func TestCheck_faultInjectWhenCancelContextTriesToSwallowTheFault(tt *testing.T) { - t := testcase.NewT(tt, testcase.NewSpec(tt)) + t := testcase.NewTWithSpec(tt, testcase.NewSpec(tt)) faultinject.EnableForTest(t) expectedErr := t.Random.Error() callerFault := faultinject.CallerFault{Function: "helperTestCheckFaultInjectWhenCancelContextTriesToSwallowTheFault"} diff --git a/internal/doubles/RecorderTB.go b/internal/doubles/RecorderTB.go index 6e99327..852ffaf 100644 --- a/internal/doubles/RecorderTB.go +++ b/internal/doubles/RecorderTB.go @@ -19,8 +19,8 @@ type RecorderTB struct { // records might be written concurrently, but it is not expected to receive reads during concurrent writes. // That is considered a mistake in the testing suite. - records []*record - recordsMutex sync.Mutex + _records []*record + m sync.Mutex } type record struct { @@ -42,12 +42,20 @@ func (r record) play(passthrough bool) { } } +func (rtb *RecorderTB) records() []*record { + rtb.m.Lock() + defer rtb.m.Unlock() + var out []*record = make([]*record, len(rtb._records)) + copy(out, rtb._records) + return out +} + func (rtb *RecorderTB) record(blk func(r *record)) { - rtb.recordsMutex.Lock() - defer rtb.recordsMutex.Unlock() + rtb.m.Lock() + defer rtb.m.Unlock() rec := &record{} blk(rec) - rtb.records = append(rtb.records, rec) + rtb._records = append(rtb._records, rec) rec.play(rtb.Config.Passthrough) } @@ -55,7 +63,7 @@ func (rtb *RecorderTB) Forward() { rtb.TB.Helper() // set passthrough for future events like Recorder used from a .Cleanup callback. _ = rtb.withPassthrough() - for _, record := range rtb.records { + for _, record := range rtb.records() { if !record.Skip { record.Forward() } @@ -66,7 +74,7 @@ func (rtb *RecorderTB) CleanupNow() { rtb.TB.Helper() defer rtb.withPassthrough()() td := &teardown.Teardown{} - for _, event := range rtb.records { + for _, event := range rtb.records() { if event.Cleanup != nil && !event.Skip { td.Defer(event.Cleanup) event.Skip = true @@ -76,9 +84,15 @@ func (rtb *RecorderTB) CleanupNow() { } func (rtb *RecorderTB) withPassthrough() func() { + rtb.m.Lock() + defer rtb.m.Unlock() currentPassthrough := rtb.Config.Passthrough rtb.Config.Passthrough = true - return func() { rtb.Config.Passthrough = currentPassthrough } + return func() { + rtb.m.Lock() + defer rtb.m.Unlock() + rtb.Config.Passthrough = currentPassthrough + } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/internal/doubles/RecorderTB_test.go b/internal/doubles/RecorderTB_test.go index 2bbc4d7..9b42902 100644 --- a/internal/doubles/RecorderTB_test.go +++ b/internal/doubles/RecorderTB_test.go @@ -60,9 +60,9 @@ func TestRecorderTB(t *testing.T) { } ) - thenTBWillMarkedAsFailed := func(s *testcase.Spec, subject func(t *testcase.T)) { + thenTBWillMarkedAsFailed := func(s *testcase.Spec, act func(t *testcase.T)) { s.Then(`it will make the TB object failed`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).True(recorder.Get(t).IsFailed) }) @@ -81,13 +81,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Fail`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Fail() } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.True(stub.IsFailed) }) @@ -95,13 +95,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.FailNow`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { expectToExitGoroutine(t, recorder.Get(t).FailNow) } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.True(stub.IsFailed) }) @@ -109,13 +109,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Error`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Error(`foo`) } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.Contain(stub.Logs.String(), `foo`) }) @@ -123,13 +123,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Errorf`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Errorf(`%s -`, `errorf`) } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.Contain(stub.Logs.String(), `errorf -`) }) @@ -137,13 +137,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Fatal`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { expectToExitGoroutine(t, func() { recorder.Get(t).Fatal(`fatal`) }) } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.Contain(stub.Logs.String(), `fatal`) }) @@ -151,13 +151,13 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Fatalf`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { expectToExitGoroutine(t, func() { recorder.Get(t).Fatalf(`%s -`, `fatalf`) }) } - thenTBWillMarkedAsFailed(s, subject) + thenTBWillMarkedAsFailed(s, act) - thenUnderlyingTBWillExpect(s, subject, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, act, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.Contain(stub.Logs.String(), `fatalf -`) }) @@ -165,7 +165,7 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.Failed`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) bool { + var act = func(t *testcase.T) bool { return recorder.Get(t).Failed() } @@ -180,10 +180,10 @@ func TestRecorderTB(t *testing.T) { isFailed.LetValue(s, true) s.Then(`failed will be true`, func(t *testcase.T) { - assert.Must(t).True(subject(t)) + assert.Must(t).True(act(t)) }) - thenUnderlyingTBWillExpect(s, func(t *testcase.T) { _ = subject(t) }, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, func(t *testcase.T) { _ = act(t) }, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.False(stub.Failed(), "expect that IsFailed don't affect the testing.TB") }) @@ -194,10 +194,10 @@ func TestRecorderTB(t *testing.T) { isFailed.LetValue(s, false) s.Then(`failed will be false`, func(t *testcase.T) { - assert.Must(t).False(subject(t)) + assert.Must(t).False(act(t)) }) - thenUnderlyingTBWillExpect(s, func(t *testcase.T) { _ = subject(t) }, func(t *testcase.T, stub *doubles2.TB) { + thenUnderlyingTBWillExpect(s, func(t *testcase.T) { _ = act(t) }, func(t *testcase.T, stub *doubles2.TB) { t.Cleanup(func() { t.Must.False(stub.Failed()) }) @@ -208,12 +208,12 @@ func TestRecorderTB(t *testing.T) { s.Describe(`.Log`, func(s *testcase.Spec) { rndInterfaceListArgs.Let(s, nil) - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Log(rndInterfaceListArgs.Get(t)...) } s.Test(`when no reply is done`, func(t *testcase.T) { - subject(t) + act(t) }) s.Test(`on recorder records forward`, func(t *testcase.T) { @@ -222,7 +222,7 @@ func TestRecorderTB(t *testing.T) { expected := fmt.Sprintf(rndInterfaceListFormat.Get(t)+"\n", rndInterfaceListArgs.Get(t)...) t.Must.Contain(stubTB.Get(t).Logs.String(), expected) }) - subject(t) + act(t) recorder.Get(t).Forward() }) }) @@ -230,12 +230,12 @@ func TestRecorderTB(t *testing.T) { s.Describe(`.Logf`, func(s *testcase.Spec) { rndInterfaceListArgs.Let(s, nil) rndInterfaceListFormat.Let(s, nil) - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Logf(rndInterfaceListFormat.Get(t), rndInterfaceListArgs.Get(t)...) } s.Test(`when no reply is done`, func(t *testcase.T) { - subject(t) + act(t) }) s.Test(`on recorder records forward`, func(t *testcase.T) { @@ -243,22 +243,22 @@ func TestRecorderTB(t *testing.T) { expected := fmt.Sprintf(rndInterfaceListFormat.Get(t), rndInterfaceListArgs.Get(t)...) t.Must.Contain(stubTB.Get(t).Logs.String(), expected) }) - subject(t) + act(t) recorder.Get(t).Forward() }) }) s.Describe(`.Helper`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Helper() } s.Test(`when no Forward is done`, func(t *testcase.T) { - subject(t) + act(t) }) s.Test(`on recorder records forward`, func(t *testcase.T) { - subject(t) + act(t) recorder.Get(t).Forward() }) }) @@ -346,10 +346,10 @@ func TestRecorderTB(t *testing.T) { } return td } - subject = func(t *testcase.T) string { - return getTempDirer(t).TempDir() - } ) + act := func(t *testcase.T) string { + return getTempDirer(t).TempDir() + } s.Before(func(t *testcase.T) { // early load to force skip for go1.14 @@ -359,7 +359,7 @@ func TestRecorderTB(t *testing.T) { s.Test(`should forward event to parent TB`, func(t *testcase.T) { tempDir := t.Random.String() stubTB.Get(t).StubTempDir = tempDir - assert.Must(t).Equal(tempDir, subject(t)) + assert.Must(t).Equal(tempDir, act(t)) }) }) @@ -368,7 +368,7 @@ func TestRecorderTB(t *testing.T) { cleanupFn := testcase.Let(s, func(t *testcase.T) func() { return func() { counter.Set(t, counter.Get(t)+1) } }) - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).Cleanup(cleanupFn.Get(t)) } @@ -376,7 +376,7 @@ func TestRecorderTB(t *testing.T) { // nothing to do to fulfil this context s.Then(`cleanup will never run`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).Equal(0, counter.Get(t)) }) @@ -392,7 +392,7 @@ func TestRecorderTB(t *testing.T) { recorder.Get(t).Log(`foo`) recorder.Get(t).Log(`bar`) recorder.Get(t).Log(`baz`) - subject(t) + act(t) recorder.Get(t).Forward() assert.Must(t).Equal(0, counter.Get(t), `Cleanup should not run during reply`) stub.Finish() @@ -403,7 +403,7 @@ func TestRecorderTB(t *testing.T) { recorder.Get(t).Log(`foo`) recorder.Get(t).Log(`bar`) recorder.Get(t).Log(`baz`) - subject(t) + act(t) assert.Must(t).Equal(0, counter.Get(t), `Cleanup should not ran yet`) recorder.Get(t).CleanupNow() @@ -426,7 +426,7 @@ func TestRecorderTB(t *testing.T) { }) s.Then(`it should not exit the goroutine that calls #CleanupNow`, func(t *testcase.T) { - subject(t) + act(t) recorder.Get(t).CleanupNow() assert.Must(t).True(hasRunFlag.Get(t)) }) @@ -467,7 +467,7 @@ func TestRecorderTB(t *testing.T) { }) s.Describe(`.CleanupNow`, func(s *testcase.Spec) { - var subject = func(t *testcase.T) { + var act = func(t *testcase.T) { recorder.Get(t).CleanupNow() } @@ -481,7 +481,7 @@ func TestRecorderTB(t *testing.T) { passthrough.LetValue(s, false) s.Then(`config remains unchanged after the play`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).Equal(passthrough.Get(t), recorder.Get(t).Config.Passthrough) }) @@ -491,7 +491,7 @@ func TestRecorderTB(t *testing.T) { passthrough.LetValue(s, true) s.Then(`config remains unchanged after the play`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).Equal(passthrough.Get(t), recorder.Get(t).Config.Passthrough) }) @@ -500,7 +500,7 @@ func TestRecorderTB(t *testing.T) { s.When(`no cleanup was called`, func(s *testcase.Spec) { s.Then(`it just returns without an issue`, func(t *testcase.T) { - subject(t) + act(t) }) }) @@ -518,7 +518,7 @@ func TestRecorderTB(t *testing.T) { }) s.Then(`it will execute cleanups`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).Equal([]int{4, 2}, cleanupFootprint.Get(t)) }) @@ -533,11 +533,11 @@ func TestRecorderTB(t *testing.T) { }) s.Then(`it will execute cleanups without affecting the current goroutine`, func(t *testcase.T) { - subject(t) + act(t) }) s.Then(`it will mark the test failed`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).True(recorder.Get(t).IsFailed) }) @@ -593,13 +593,27 @@ func TestRecorderTB(t *testing.T) { }) }) + s.Describe(`.Name`, func(s *testcase.Spec) { + act := func(t *testcase.T) string { + return recorder.Get(t).Name() + } + + s.Then("it returns a non-empty name", func(t *testcase.T) { + assert.NotEmpty(t, act(t)) + }) + + s.Then("the name returned is consistent", func(t *testcase.T) { + assert.Equal(t, act(t), act(t)) + }) + }) + s.Describe(`.Run`, func(s *testcase.Spec) { var ( name = testcase.Let(s, func(t *testcase.T) string { return t.Random.String() }) - blk = testcase.Var[func(testing.TB)]{ID: `blk`} - subject = func(t *testcase.T) bool { + blk = testcase.Var[func(testing.TB)]{ID: `blk`} + act = func(t *testcase.T) bool { return recorder.Get(t).Run(name.Get(t), blk.Get(t)) } ) @@ -610,11 +624,11 @@ func TestRecorderTB(t *testing.T) { }) s.Then(`it will report the success`, func(t *testcase.T) { - assert.Must(t).True(subject(t)) + assert.Must(t).True(act(t)) }) s.Then(`it will not mark the parent as failed`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).True(!recorder.Get(t).IsFailed) }) @@ -626,11 +640,11 @@ func TestRecorderTB(t *testing.T) { }) s.Then(`it will report the markFailed`, func(t *testcase.T) { - assert.Must(t).True(!subject(t)) + assert.Must(t).True(!act(t)) }) s.Then(`it will mark the parent as failed`, func(t *testcase.T) { - subject(t) + act(t) assert.Must(t).True(recorder.Get(t).IsFailed) }) diff --git a/let/std.go b/let/std.go index cae4af2..c92e54c 100644 --- a/let/std.go +++ b/let/std.go @@ -80,3 +80,9 @@ func ElementFrom[V any](s *testcase.Spec, vs ...V) testcase.Var[V] { return t.Random.SliceElement(vs).(V) }) } + +func DurationBetween(s *testcase.Spec, min, max time.Duration) testcase.Var[time.Duration] { + return testcase.Let(s, func(t *testcase.T) time.Duration { + return t.Random.DurationBetween(min, max) + }) +} diff --git a/let/std_test.go b/let/std_test.go index a9bfa59..e0a7715 100644 --- a/let/std_test.go +++ b/let/std_test.go @@ -26,6 +26,7 @@ func TestSTD_smoke(t *testing.T) { 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 { @@ -54,6 +55,9 @@ func TestSTD_smoke(t *testing.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))) diff --git a/random/Random.go b/random/Random.go index 5c43321..69b107a 100644 --- a/random/Random.go +++ b/random/Random.go @@ -61,11 +61,16 @@ func (r *Random) Float64() float64 { return r.rnd().Float64() } -// IntBetween returns, as an int, a non-negative pseudo-random number based on the received int range's [min,max]. +// IntBetween returns an int based on the received int range's [min,max]. func (r *Random) IntBetween(min, max int) int { return min + r.IntN((max+1)-min) } +// DurationBetween returns an duration based on the received duration range's [min,max]. +func (r *Random) DurationBetween(min, max time.Duration) time.Duration { + return time.Duration(r.IntBetween(int(min), int(max))) +} + // IntB returns, as an int, a non-negative pseudo-random number based on the received int range's [min,max]. func (r *Random) IntB(min, max int) int { return r.IntBetween(min, max) diff --git a/random/Random_test.go b/random/Random_test.go index 2868a4d..572009d 100644 --- a/random/Random_test.go +++ b/random/Random_test.go @@ -129,6 +129,12 @@ func SpecRandomMethods(s *testcase.Spec, rnd testcase.Var[*random.Random]) { }) }) + s.Describe(`DurationBetween`, func(s *testcase.Spec) { + SpecDurationBetween(s, rnd, func(t *testcase.T) func(min, max time.Duration) time.Duration { + return rnd.Get(t).DurationBetween + }) + }) + s.Describe(`SliceElement`, func(s *testcase.Spec) { s.Test(`E2E`, func(t *testcase.T) { pool := []int{1, 2, 3, 4, 5} @@ -802,6 +808,46 @@ func SpecIntBetween(s *testcase.Spec, rnd testcase.Var[*random.Random], sbj func }) } +func SpecDurationBetween(s *testcase.Spec, rnd testcase.Var[*random.Random], sbj func(*testcase.T) func(min, max time.Duration) time.Duration) { + var ( + min = testcase.Let(s, func(t *testcase.T) time.Duration { + return time.Duration(rnd.Get(t).IntN(42)) + }) + max = testcase.Let(s, func(t *testcase.T) time.Duration { + // +1 in the end to ensure that `max` is bigger than `min` + return time.Duration(rnd.Get(t).IntN(42)) + min.Get(t) + 1 + }) + subject = func(t *testcase.T) time.Duration { + return sbj(t)(min.Get(t), max.Get(t)) + } + ) + + s.Then(`it will return a value between the range`, func(t *testcase.T) { + out := subject(t) + assert.Must(t).True(min.Get(t) <= out, `expected that from <= than out`) + assert.Must(t).True(out <= max.Get(t), `expected that out is <= than max`) + }) + + s.And(`min and max is in the negative range`, func(s *testcase.Spec) { + min.LetValue(s, -128) + max.LetValue(s, -64) + + s.Then(`it will return a value between the range`, func(t *testcase.T) { + out := subject(t) + assert.Must(t).True(min.Get(t) <= out, `expected that from <= than out`) + assert.Must(t).True(out <= max.Get(t), `expected that out is <= than max`) + }) + }) + + s.And(`min and max equal`, func(s *testcase.Spec) { + max.Let(s, func(t *testcase.T) time.Duration { return min.Get(t) }) + + s.Then(`it returns the min and max value since the range can only have one value`, func(t *testcase.T) { + t.Must.Equal(max.Get(t), subject(t)) + }) + }) +} + func SpecTimeBetween(s *testcase.Spec, rnd testcase.Var[*random.Random], sbj func(*testcase.T) func(from, to time.Time) time.Time) { fromTime := testcase.Let(s, func(t *testcase.T) time.Time { return time.Now().UTC() diff --git a/random/examples_test.go b/random/examples_test.go index 0bb16f7..ac13475 100644 --- a/random/examples_test.go +++ b/random/examples_test.go @@ -188,3 +188,9 @@ func ExampleRandom_Repeat() { _ = n // is the number of times, the function block was repeated. } + +func ExampleRandom_DurationBetween() { + rnd := random.New(random.CryptoSeed{}) + + rnd.DurationBetween(time.Second, time.Minute) +}