Skip to content

Commit

Permalink
add DurationBetween random generation
Browse files Browse the repository at this point in the history
- add support for random time duration generation random.Random#DurationBetween
- improve testcase.ToT logic to cast a testing TB into a *testcase.T
  • Loading branch information
adamluzsi committed Oct 8, 2024
1 parent b01afa1 commit 0188472
Show file tree
Hide file tree
Showing 20 changed files with 347 additions and 100 deletions.
2 changes: 1 addition & 1 deletion Spec_bc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions Suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion T.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
51 changes: 45 additions & 6 deletions TB.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package testcase

import (
"reflect"
"testing"

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/doubles"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
69 changes: 67 additions & 2 deletions TB_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
})
}
63 changes: 53 additions & 10 deletions T_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
})
}
Expand Down Expand Up @@ -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]{
Expand All @@ -533,28 +543,28 @@ 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`)
})
t.Run(`without *Spec`, func(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`)
})
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{}
Expand All @@ -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)

Expand Down
14 changes: 7 additions & 7 deletions Var_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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?")
}
Expand Down Expand Up @@ -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) })
Expand Down Expand Up @@ -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())
Expand All @@ -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())
Expand Down Expand Up @@ -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) })
Expand Down
2 changes: 1 addition & 1 deletion clock/Clock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 0188472

Please sign in to comment.