defined env type

the env type represents a test environment. This was added as an
intermediate container that could store the saved fields of tests after
they're run, so that future tests could load them. Previously a test
could only get the attributes of the test that immediately preceded it.
This change allows tests to load attributes further back in the history.
g-counter
Jordan Orelli 4 years ago
parent 2c68d4dbd5
commit d523c73690

@ -0,0 +1,73 @@
package tea
import (
"fmt"
"reflect"
)
type env struct {
key string
value interface{}
parent *env
}
func mkenv(test Test) *env {
var e *env
return e.save(test)
}
// save looks at the Test t and saves the values of its fields marked with a
// save tag
func (e *env) save(test Test) *env {
V := reflect.ValueOf(test)
if V.Type().Kind() == reflect.Ptr {
V = V.Elem()
}
T := V.Type()
for i := 0; i < T.NumField(); i++ {
f := T.Field(i)
if !isSaveField(f) {
continue
}
fv := V.Field(i)
e = &env{
key: f.Name,
value: fv.Interface(),
parent: e,
}
}
return e
}
func (e *env) load(dest Test) error {
destV := reflect.ValueOf(dest).Elem()
destT := destV.Type()
for i := 0; i < destT.NumField(); i++ {
f := destT.Field(i)
if !isLoadField(f) {
continue
}
fv := destV.Field(i)
set := false
for e := e; e != nil; e = e.parent {
if e.key == f.Name {
ev := reflect.ValueOf(e.value)
if ev.Type().AssignableTo(fv.Type()) {
set = true
fv.Set(ev)
break
}
}
}
if !set {
return fmt.Errorf("failed to set required field: %q", f.Name)
}
}
return nil
}

@ -0,0 +1,70 @@
package tea
import (
"testing"
)
func TestSave(t *testing.T) {
type saveFoo struct {
empty
Foo int `tea:"save"`
Bar string
}
type loadFoo struct {
empty
Foo int `tea:"load"`
Bar string
}
t.Run("empty begets nil", func(t *testing.T) {
e := mkenv(new(empty))
if e != nil {
t.Errorf("saw unexpected env value looking for nil: %v", e)
}
})
t.Run("unexported fields are ignored", func(t *testing.T) {
type test struct {
empty
foo int `tea:"save"`
}
if e := mkenv(test{foo: 5}); e != nil {
t.Errorf("saw unexpected env value looking for nil: %v", e)
}
})
t.Run("save an int", func(t *testing.T) {
e := mkenv(&saveFoo{Foo: 5})
if e == nil {
t.Fatalf("saw nil env when expecting a valid env")
}
if e.key != "Foo" {
t.Errorf("expected key %q but saw %q instead", "Foo", e.key)
}
if e.value != 5 {
t.Errorf("expected value %v but saw %v instead", 5, e.value)
}
})
t.Run("load an int", func(t *testing.T) {
e := mkenv(&saveFoo{Foo: 5})
test := new(loadFoo)
e.load(test)
if test.Foo != 5 {
t.Errorf("expected value %v but saw %v instead", 5, test.Foo)
}
})
t.Run("loads can fail", func(t *testing.T) {
e := mkenv(new(empty))
test := new(loadFoo)
if err := e.load(test); err == nil {
t.Errorf("expected a load error but did not see one")
}
})
}

@ -18,3 +18,10 @@ type failure struct {
} }
func (f failure) Run(t *testing.T) { t.Error(f.cause.Error()) } func (f failure) Run(t *testing.T) { t.Error(f.cause.Error()) }
// empty is an empty test. It does nothing when run, it's just used as a
// sentinel value to create notes in the test graph and for ... testing the tea
// package itself.
type empty struct{}
func (e empty) Run(t *testing.T) {}

@ -6,32 +6,63 @@ import (
"testing" "testing"
) )
// Run runs a tree of tests, starting from its root. // Run runs a tree of tests. Tests will be run recursively starting at the
// provided node and descending to all of its children. All of its parent nodes
// will also be run since they are prerequisites, but none of its sibling node
// will be executed.
func Run(t *testing.T, tree *Tree) { func Run(t *testing.T, tree *Tree) {
t.Run(tree.name, func(t *testing.T) { t.Run(tree.name, func(t *testing.T) {
test := setup(t, tree) setup(t, tree)
test.Run(t)
for _, child := range tree.children { if t.Failed() || t.Skipped() {
if t.Failed() || t.Skipped() { for _, child := range tree.children {
skip(t, child) skip(t, child)
} else {
Run(t, child)
} }
return
}
for _, child := range tree.children {
Run(t, child)
} }
}) })
} }
func setup(t *testing.T, tree *Tree) Test { // setup runs all of the tests ancestor to the given tree, building up a
test := clone(tree.test) // testing environment from their side-effects
if tree.parent != nil { func setup(t *testing.T, tree *Tree) *env {
p := setup(t, tree.parent) if tree == nil {
p.Run(t) return nil
test = merge(test, p) }
if tree.parent == nil {
test := clone(tree.test)
test.Run(t)
return mkenv(test)
} }
return test
e := setup(t, tree.parent)
test := clone(tree.test)
e.load(test)
test.Run(t)
return e.save(test)
} }
// setup runs all of the dependencies for a given test. All of the tests are
// run in the same subtest (and therefore same goroutine).
// func setup(t *testing.T, tree *Tree) Test {
// // clone the user's values before doing anything, we don't want to pollute
// // the planning tree.
// test := clone(tree.test)
//
// if tree.parent != nil {
// p := setup(t, tree.parent)
// p.Run(t)
// test = merge(test, p)
// }
//
// return test
// }
func skip(t *testing.T, tree *Tree) { func skip(t *testing.T, tree *Tree) {
t.Run(tree.name, func(t *testing.T) { t.Run(tree.name, func(t *testing.T) {
for _, child := range tree.children { for _, child := range tree.children {
@ -96,6 +127,10 @@ func merge(dest Test, src Test) Test {
// isSaveField takes a struct field and checks its tags for a save tag, // isSaveField takes a struct field and checks its tags for a save tag,
// indicating that the field's value should persist between tests // indicating that the field's value should persist between tests
func isSaveField(f reflect.StructField) bool { func isSaveField(f reflect.StructField) bool {
// PkgPath is empty string when the identifier is unexported.
if f.PkgPath != "" {
return false
}
parts := strings.Split(f.Tag.Get("tea"), ",") parts := strings.Split(f.Tag.Get("tea"), ",")
for _, part := range parts { for _, part := range parts {
if part == "save" { if part == "save" {
@ -109,6 +144,10 @@ func isSaveField(f reflect.StructField) bool {
// indicating that the field's value should be populated by a saved value from // indicating that the field's value should be populated by a saved value from
// a prior test in the chain. // a prior test in the chain.
func isLoadField(f reflect.StructField) bool { func isLoadField(f reflect.StructField) bool {
// PkgPath is empty string when the identifier is unexported.
if f.PkgPath != "" {
return false
}
parts := strings.Split(f.Tag.Get("tea"), ",") parts := strings.Split(f.Tag.Get("tea"), ",")
for _, part := range parts { for _, part := range parts {
if part == "load" { if part == "load" {
@ -118,6 +157,7 @@ func isLoadField(f reflect.StructField) bool {
return false return false
} }
// parseName parses the name for a given test
func parseName(test Test) string { func parseName(test Test) string {
if s, ok := test.(interface{ String() string }); ok { if s, ok := test.(interface{ String() string }); ok {
return s.String() return s.String()

@ -9,6 +9,10 @@ import (
"./tea" "./tea"
) )
type empty struct{}
func (e *empty) Run(t *testing.T) {}
type testThingSetup struct { type testThingSetup struct {
Thing *Thing `tea:"save"` Thing *Thing `tea:"save"`
} }
@ -37,6 +41,7 @@ func (test setKey) String() string {
func (test *setKey) Run(t *testing.T) { func (test *setKey) Run(t *testing.T) {
t.Logf("[%s] running setKey key: %q value: %q", t.Name(), test.key, test.value) t.Logf("[%s] running setKey key: %q value: %q", t.Name(), test.key, test.value)
// test.Thing is automatically propagated from the prior test by tea!
err := test.Thing.Set(test.key, test.value) err := test.Thing.Set(test.key, test.value)
if !test.bad && err != nil { if !test.bad && err != nil {
t.Errorf("should be able to set %q=%q but saw error %v", test.key, test.value, err) t.Errorf("should be able to set %q=%q but saw error %v", test.key, test.value, err)
@ -51,7 +56,8 @@ func TestThing(t *testing.T) {
root.Child(&setKey{key: "alice", value: "apple"}) root.Child(&setKey{key: "alice", value: "apple"})
root.Child(&setKey{key: "bob", value: "banana"}) root.Child(&setKey{key: "bob", value: "banana"})
root.Child(&setKey{key: "carol", value: "cherry"}) root.Child(new(empty)).Child(&setKey{key: "carol", value: "cherry"})
root.Child(&setKey{bad: true})
bob := root.Child(&setKey{key: "b ob", value: "banana"}) bob := root.Child(&setKey{key: "b ob", value: "banana"})
bob.Child(&setKey{key: "car-el", value: "cherry"}) bob.Child(&setKey{key: "car-el", value: "cherry"})

Loading…
Cancel
Save