From ce7e5fa1e2c83503d194b6c92281c7a01782422f Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Thu, 7 Nov 2024 20:54:04 +0100 Subject: [PATCH] make testcase.Var#ID a dedicated type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many code editors’ intellisense often prioritised suggesting Var[string]#ID for string values instead of Var[string]#Get, which can easily lead to headaches when auto-completion accidentally selects the wrong option. --- Spec_test.go | 4 +-- T.go | 2 +- Var.go | 8 +++-- Var_test.go | 6 ++-- backward.go | 4 +-- docs/contracts.md | 4 +-- let.go | 18 +++++----- let/let.go | 2 +- let_test.go | 2 +- variables.go | 92 +++++++++++++++++++++++------------------------ 10 files changed, 72 insertions(+), 70 deletions(-) diff --git a/Spec_test.go b/Spec_test.go index 03eaae2..c012bed 100644 --- a/Spec_test.go +++ b/Spec_test.go @@ -127,7 +127,7 @@ func TestSpec_Context(t *testing.T) { var allSideEffect [][]string var sideEffect []string - v := testcase.Var[int]{ID: strconv.Itoa(rand.Int())} + v := testcase.Var[int]{ID: testcase.VarID(strconv.Itoa(rand.Int()))} nest1Value := rand.Int() nest2Value := rand.Int() nest3Value := rand.Int() @@ -226,7 +226,7 @@ func TestSpec_ParallelSafeVariableSupport(t *testing.T) { s := testcase.NewSpec(t) s.Parallel() - v := testcase.Var[int]{ID: strconv.Itoa(rand.Int())} + v := testcase.Var[int]{ID: testcase.VarID(strconv.Itoa(rand.Int()))} nest1Value := rand.Int() nest2Value := rand.Int() nest3Value := rand.Int() diff --git a/T.go b/T.go index dd6f6cd..c9e09ef 100644 --- a/T.go +++ b/T.go @@ -159,7 +159,7 @@ func (t *T) contexts() []*Spec { return t.cache.contexts } -func (t *T) hasOnLetHookApplied(name string) bool { +func (t *T) hasOnLetHookApplied(name VarID) bool { for _, c := range t.contexts() { if ok := c.vars.hasOnLetHookApplied(name); ok { return ok diff --git a/Var.go b/Var.go index 02f4077..9f10ee9 100644 --- a/Var.go +++ b/Var.go @@ -21,7 +21,7 @@ import ( type Var[V any] struct { // ID is the testCase spec variable group from where the cached value can be accessed later on. // ID is Mandatory when you create a variable, else the empty string will be used as the variable group. - ID string + ID VarID // Init is an optional constructor definition that will be used when Var is bonded to a *Spec without constructor function passed to the Let function. // The goal of this field to initialize a variable that can be reused across different testing suites by bounding the Var to a given testing suite. // @@ -47,17 +47,19 @@ type Var[V any] struct { Deps Vars } +type VarID string + type Vars []tetcaseVar type tetcaseVar interface { isTestcaseVar() - id() string + id() VarID get(t *T) any bind(s *Spec) } func (Var[V]) isTestcaseVar() {} -func (v Var[V]) id() string { return v.ID } +func (v Var[V]) id() VarID { return v.ID } type VarInit[V any] func(*T) V diff --git a/Var_test.go b/Var_test.go index 890c21c..1f8fa11 100644 --- a/Var_test.go +++ b/Var_test.go @@ -24,12 +24,12 @@ func TestVar(t *testing.T) { // This should not be the case for anything else outside the testing framework. s.HasSideEffect() rnd := random.New(random.CryptoSeed{}) - var testVar = testcase.Var[int]{ID: rnd.StringNWithCharset(5, "abcdefghijklmnopqrstuvwxyz")} + var testVar = testcase.Var[int]{ID: testcase.VarID(rnd.StringNWithCharset(5, "abcdefghijklmnopqrstuvwxyz"))} expected := rnd.Int() stub := &doubles.TB{} willFatal := willFatalWithMessageFn(stub) - willFatalWithVariableNotFoundMessage := func(s *testcase.Spec, tb testing.TB, varName string, blk func(*testcase.T)) { + willFatalWithVariableNotFoundMessage := func(s *testcase.Spec, tb testing.TB, varName testcase.VarID, blk func(*testcase.T)) { tct := testcase.NewTWithSpec(stub, s) assert.Must(tb).Contain(willFatal(t, func() { blk(tct) }), fmt.Sprintf("Variable %q is not found.", varName)) @@ -207,7 +207,7 @@ func TestVar(t *testing.T) { }) }) - willFatalWithOnLetMissing := func(s *testcase.Spec, tb testing.TB, varName string, blk func(*testcase.T)) { + willFatalWithOnLetMissing := func(s *testcase.Spec, tb testing.TB, varName testcase.VarID, blk func(*testcase.T)) { 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)) diff --git a/backward.go b/backward.go index 95f5592..7e4903a 100644 --- a/backward.go +++ b/backward.go @@ -7,7 +7,7 @@ import "go.llib.dev/testcase/assert" // thus Let has moved to be a pkg-level function in the package. // // DEPRECATED: use testcase.Let instead testcase#Spec.Let. -func (spec *Spec) Let(varName string, blk VarInit[any]) Var[any] { +func (spec *Spec) Let(varName VarID, blk VarInit[any]) Var[any] { return let[any](spec, varName, blk) } @@ -16,7 +16,7 @@ func (spec *Spec) Let(varName string, blk VarInit[any]) Var[any] { // thus LetValue has moved to be a pkg-level function in the package. // // DEPRECATED: use testcase.LetValue instead testcase#Spec.LetValue. -func (spec *Spec) LetValue(varName string, value any) Var[any] { +func (spec *Spec) LetValue(varName VarID, value any) Var[any] { return letValue[any](spec, varName, value) } diff --git a/docs/contracts.md b/docs/contracts.md index aa70868..a734929 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -86,8 +86,8 @@ type RoleInterfaceContract struct { type RoleInterfaceContractSubject interface { mypkg.RoleInterfaceName - FindByID(ctx context.Context, id string) (mypkg.XY, bool, error) - DeleteByID(ctx context.Context, id string) error + FindByID(ctx context.Context, id VarID) (mypkg.XY, bool, error) + DeleteByID(ctx context.Context, id VarID) error } ``` diff --git a/let.go b/let.go index 3ce994a..ba3770c 100644 --- a/let.go +++ b/let.go @@ -93,7 +93,7 @@ func LetValue[V any](spec *Spec, value V) Var[V] { return letValue[V](spec, makeVarID(spec), value) } -func let[V any](spec *Spec, varID string, blk VarInit[V]) Var[V] { +func let[V any](spec *Spec, varID VarID, blk VarInit[V]) Var[V] { helper(spec.testingTB).Helper() if spec.immutable { spec.testingTB.Fatalf(warnEventOnImmutableFormat, `Let`) @@ -108,7 +108,7 @@ func let[V any](spec *Spec, varID string, blk VarInit[V]) Var[V] { return Var[V]{ID: varID, Init: blk} } -func letValue[V any](spec *Spec, varName string, value V) Var[V] { +func letValue[V any](spec *Spec, varName VarID, value V) Var[V] { helper(spec.testingTB).Helper() if reflects.IsMutable(value) { spec.testingTB.Fatalf(panicMessageForLetValue, value) @@ -121,7 +121,7 @@ func letValue[V any](spec *Spec, varName string, value V) Var[V] { } // latest decl is the first and the deeper you want to reach back, the higher the index -func findCurrentDeclsFor(spec *Spec, varName string) []variablesInitBlock { +func findCurrentDeclsFor(spec *Spec, varName VarID) []variablesInitBlock { var decls []variablesInitBlock for _, s := range spec.specsFromCurrent() { if decl, ok := s.vars.defs[varName]; ok { @@ -131,13 +131,13 @@ func findCurrentDeclsFor(spec *Spec, varName string) []variablesInitBlock { return decls } -func makeVarID(spec *Spec) string { +func makeVarID(spec *Spec) VarID { helper(spec.testingTB).Helper() location := caller.GetLocation(false) // when variable is declared within a loop // providing a variable ID offset is required to identify the variable uniquely. - varIDIndex := make(map[string]struct{}) + varIDIndex := make(map[VarID]struct{}) for _, s := range spec.specsFromParent() { for k := range s.vars.locks { varIDIndex[k] = struct{}{} @@ -154,19 +154,19 @@ func makeVarID(spec *Spec) string { } var ( - id string + id VarID offset int ) positioning: for { // quick path for the majority of the case. - if _, ok := varIDIndex[location]; !ok { - id = location + if _, ok := varIDIndex[VarID(location)]; !ok { + id = VarID(location) break positioning } offset++ - id = fmt.Sprintf("%s#[%d]", location, offset) + id = VarID(fmt.Sprintf("%s#[%d]", location, offset)) if _, ok := varIDIndex[id]; !ok { break positioning } diff --git a/let/let.go b/let/let.go index 3459d17..9caed8f 100644 --- a/let/let.go +++ b/let/let.go @@ -39,7 +39,7 @@ func As[To, From any](Var testcase.Var[From]) testcase.Var[To] { 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), + ID: testcase.VarID(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) diff --git a/let_test.go b/let_test.go index 3cdacea..1f8da48 100644 --- a/let_test.go +++ b/let_test.go @@ -203,7 +203,7 @@ func TestLet_letVarIDInNonCoreTestcasePackage(t *testing.T) { s := testcase.NewSpec(t) resp := httpspec.LetResponseRecorder(s) - t.Logf(resp.ID) + t.Logf("id: %s", resp.ID) assert.NotContain(t, resp.ID, "_test.go") assert.NotContain(t, resp.ID, filepath.Base(frame.File)) assert.Contain(t, resp.ID, filepath.Dir(frame.File)) diff --git a/variables.go b/variables.go index 7b76efd..9ff5074 100644 --- a/variables.go +++ b/variables.go @@ -8,14 +8,14 @@ import ( func newVariables() *variables { return &variables{ - defs: make(map[string]variablesInitBlock), - defsSuper: make(map[string][]variablesInitBlock), - cache: make(map[string]interface{}), + defs: make(map[VarID]variablesInitBlock), + defsSuper: make(map[VarID][]variablesInitBlock), + cache: make(map[VarID]interface{}), cacheSuper: newVariablesSuperCache(), - onLet: make(map[string]struct{}), - locks: make(map[string]*sync.RWMutex), - before: make(map[string]struct{}), - deps: make(map[string]*sync.Once), + onLet: make(map[VarID]struct{}), + locks: make(map[VarID]*sync.RWMutex), + before: make(map[VarID]struct{}), + deps: make(map[VarID]*sync.Once), } } @@ -24,19 +24,19 @@ func newVariables() *variables { // Different test cases don't share they variables instance. type variables struct { mutex sync.RWMutex - locks map[string]*sync.RWMutex - defs map[string]variablesInitBlock - defsSuper map[string][]variablesInitBlock - onLet map[string]struct{} - before map[string]struct{} - cache map[string]any + locks map[VarID]*sync.RWMutex + defs map[VarID]variablesInitBlock + defsSuper map[VarID][]variablesInitBlock + onLet map[VarID]struct{} + before map[VarID]struct{} + cache map[VarID]any cacheSuper *variablesSuperCache - deps map[string]*sync.Once + deps map[VarID]*sync.Once } type variablesInitBlock func(t *T) any -func (v *variables) Knows(varName string) bool { +func (v *variables) Knows(varName VarID) bool { defer v.rLock(varName)() if _, found := v.defs[varName]; found { return true @@ -47,19 +47,19 @@ func (v *variables) Knows(varName string) bool { return false } -func (v *variables) Let(varName string, blk variablesInitBlock /* [interface{}] */) { +func (v *variables) Let(varName VarID, blk variablesInitBlock /* [interface{}] */) { defer v.lock(varName)() v.let(varName, blk) } -func (v *variables) let(varName string, blk variablesInitBlock /* [interface{}] */) { +func (v *variables) let(varName VarID, blk variablesInitBlock /* [interface{}] */) { v.defs[varName] = blk } // Get will return a testcase vs. // // If there is no such value, then it will panic with a "friendly" message. -func (v *variables) Get(t *T, varName string) interface{} { +func (v *variables) Get(t *T, varName VarID) interface{} { t.TB.Helper() if !v.Knows(varName) { t.Fatal(v.fatalMessageFor(varName)) @@ -72,26 +72,26 @@ func (v *variables) Get(t *T, varName string) interface{} { return t.vars.cacheGet(varName) } -func (v *variables) cacheGet(varName string) interface{} { +func (v *variables) cacheGet(varName VarID) interface{} { v.mutex.RLock() defer v.mutex.RUnlock() return v.cache[varName] } -func (v *variables) cacheHas(varName string) bool { +func (v *variables) cacheHas(varName VarID) bool { v.mutex.RLock() defer v.mutex.RUnlock() _, ok := v.cache[varName] return ok } -func (v *variables) cacheSet(varName string, data interface{}) { +func (v *variables) cacheSet(varName VarID, data interface{}) { v.mutex.Lock() defer v.mutex.Unlock() v.cache[varName] = data } -func (v *variables) Set(varName string, value interface{}) { +func (v *variables) Set(varName VarID, value interface{}) { defer v.lock(varName)() if _, ok := v.defs[varName]; !ok { v.let(varName, func(t *T) interface{} { return value }) @@ -102,14 +102,14 @@ func (v *variables) Set(varName string, value interface{}) { func (v *variables) reset() { v.mutex.Lock() defer v.mutex.Unlock() - v.cache = make(map[string]interface{}) + v.cache = make(map[VarID]interface{}) v.cacheSuper = newVariablesSuperCache() } -func (v *variables) fatalMessageFor(varName string) string { +func (v *variables) fatalMessageFor(varName VarID) string { var messages []string messages = append(messages, fmt.Sprintf(`Variable %q is not found`, varName)) - var keys []string + var keys []VarID for k := range v.defs { keys = append(keys, k) } @@ -129,11 +129,11 @@ func (v *variables) merge(oth *variables) { } } -func (v *variables) addOnLetHookSetup(name string) { +func (v *variables) addOnLetHookSetup(name VarID) { v.onLet[name] = struct{}{} } -func (v *variables) tryRegisterVarBefore(name string) bool { +func (v *variables) tryRegisterVarBefore(name VarID) bool { if _, ok := v.before[name]; ok { return false } @@ -141,24 +141,24 @@ func (v *variables) tryRegisterVarBefore(name string) bool { return true } -func (v *variables) hasOnLetHookApplied(name string) bool { +func (v *variables) hasOnLetHookApplied(name VarID) bool { _, ok := v.onLet[name] return ok } -func (v *variables) rLock(varName string) func() { +func (v *variables) rLock(varName VarID) func() { m := v.getMutex(varName) m.RLock() return m.RUnlock } -func (v *variables) lock(varName string) func() { +func (v *variables) lock(varName VarID) func() { m := v.getMutex(varName) m.Lock() return m.Unlock } -func (v *variables) getMutex(varName string) *sync.RWMutex { +func (v *variables) getMutex(varName VarID) *sync.RWMutex { v.mutex.Lock() defer v.mutex.Unlock() if _, ok := v.locks[varName]; !ok { @@ -169,11 +169,11 @@ func (v *variables) getMutex(varName string) *sync.RWMutex { //////////////////////////////////////////////////////// super ///////////////////////////////////////////////////////// -func (v *variables) SetSuper(varName string, val any) { +func (v *variables) SetSuper(varName VarID, val any) { v.cacheSuper.Set(varName, val) } -func (v *variables) LookupSuper(t *T, varName string) (any, bool) { +func (v *variables) LookupSuper(t *T, varName VarID) (any, bool) { if cv, ok := v.cacheSuper.Lookup(varName); ok { return cv, ok } @@ -191,11 +191,11 @@ func (v *variables) LookupSuper(t *T, varName string) (any, bool) { return val, true } -func (v *variables) depsInitDo(id string, fn func()) { +func (v *variables) depsInitDo(id VarID, fn func()) { v.depsInitFor(id).Do(fn) } -func (v *variables) depsInitFor(id string) *sync.Once { +func (v *variables) depsInitFor(id VarID) *sync.Once { // // FAST v.mutex.RLock() @@ -216,32 +216,32 @@ func (v *variables) depsInitFor(id string) *sync.Once { func newVariablesSuperCache() *variablesSuperCache { return &variablesSuperCache{ - cache: make(map[string]map[int]any), - currentDepth: make(map[string]int), + cache: make(map[VarID]map[int]any), + currentDepth: make(map[VarID]int), } } type variablesSuperCache struct { - cache map[string]map[int]any - currentDepth map[string]int + cache map[VarID]map[int]any + currentDepth map[VarID]int } -func (sc *variablesSuperCache) StepIn(varName string) func() { +func (sc *variablesSuperCache) StepIn(varName VarID) func() { if sc.currentDepth == nil { - sc.currentDepth = make(map[string]int) + sc.currentDepth = make(map[VarID]int) } sc.currentDepth[varName]++ return func() { sc.currentDepth[varName]-- } } -func (sc *variablesSuperCache) depthFor(varName string) int { +func (sc *variablesSuperCache) depthFor(varName VarID) int { if sc.currentDepth == nil { return 0 } return sc.currentDepth[varName] } -func (sc *variablesSuperCache) Lookup(varName string) (any, bool) { +func (sc *variablesSuperCache) Lookup(varName VarID) (any, bool) { if sc.cache == nil { return nil, false } @@ -253,9 +253,9 @@ func (sc *variablesSuperCache) Lookup(varName string) (any, bool) { return v, ok } -func (sc *variablesSuperCache) Set(varName string, v any) { +func (sc *variablesSuperCache) Set(varName VarID, v any) { if sc.cache == nil { - sc.cache = make(map[string]map[int]any) + sc.cache = make(map[VarID]map[int]any) } if _, ok := sc.cache[varName]; !ok { sc.cache[varName] = make(map[int]any) @@ -263,7 +263,7 @@ func (sc *variablesSuperCache) Set(varName string, v any) { sc.cache[varName][sc.depthFor(varName)] = v } -func (sc *variablesSuperCache) FindDecl(varName string, defs []variablesInitBlock) (variablesInitBlock, bool) { +func (sc *variablesSuperCache) FindDecl(varName VarID, defs []variablesInitBlock) (variablesInitBlock, bool) { if defs == nil { return nil, false }