From d523c736907117a25c7b35895c249a6ac0357c2c Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sun, 26 Jul 2020 04:00:51 +0000 Subject: [PATCH] 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. --- tea/env.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ tea/env_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ tea/test.go | 7 +++++ tea/tree.go | 68 +++++++++++++++++++++++++++++++++++---------- tea_test.go | 8 +++++- 5 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 tea/env.go create mode 100644 tea/env_test.go diff --git a/tea/env.go b/tea/env.go new file mode 100644 index 0000000..7c857c9 --- /dev/null +++ b/tea/env.go @@ -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 +} diff --git a/tea/env_test.go b/tea/env_test.go new file mode 100644 index 0000000..433b707 --- /dev/null +++ b/tea/env_test.go @@ -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") + } + }) +} diff --git a/tea/test.go b/tea/test.go index 2c197a5..7657431 100644 --- a/tea/test.go +++ b/tea/test.go @@ -18,3 +18,10 @@ type failure struct { } 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) {} diff --git a/tea/tree.go b/tea/tree.go index 18708db..4090524 100644 --- a/tea/tree.go +++ b/tea/tree.go @@ -6,32 +6,63 @@ import ( "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) { t.Run(tree.name, func(t *testing.T) { - test := setup(t, tree) - test.Run(t) + setup(t, tree) - for _, child := range tree.children { - if t.Failed() || t.Skipped() { + if t.Failed() || t.Skipped() { + for _, child := range tree.children { skip(t, child) - } else { - Run(t, child) } + return + } + + for _, child := range tree.children { + Run(t, child) } }) } -func setup(t *testing.T, tree *Tree) Test { - test := clone(tree.test) - if tree.parent != nil { - p := setup(t, tree.parent) - p.Run(t) - test = merge(test, p) +// setup runs all of the tests ancestor to the given tree, building up a +// testing environment from their side-effects +func setup(t *testing.T, tree *Tree) *env { + if tree == nil { + return nil + } + + 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) { t.Run(tree.name, func(t *testing.T) { 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, // indicating that the field's value should persist between tests 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"), ",") for _, part := range parts { 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 // a prior test in the chain. 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"), ",") for _, part := range parts { if part == "load" { @@ -118,6 +157,7 @@ func isLoadField(f reflect.StructField) bool { return false } +// parseName parses the name for a given test func parseName(test Test) string { if s, ok := test.(interface{ String() string }); ok { return s.String() diff --git a/tea_test.go b/tea_test.go index 8b2457a..54edeb7 100644 --- a/tea_test.go +++ b/tea_test.go @@ -9,6 +9,10 @@ import ( "./tea" ) +type empty struct{} + +func (e *empty) Run(t *testing.T) {} + type testThingSetup struct { Thing *Thing `tea:"save"` } @@ -37,6 +41,7 @@ func (test setKey) String() string { func (test *setKey) Run(t *testing.T) { 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) if !test.bad && err != nil { 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: "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.Child(&setKey{key: "car-el", value: "cherry"})