re-implementing the env system

the env system code is unecessarily fiddly. The current env system is
the second env system, so the layer system will be the third
implementation of the env system.

the environment in which we run our tests is a layered environment like
the scope in a programming language, but unlike with nested closures,
tea permits the user to supply match tags to skip what would be stack
frames in a system based on nested closures. It's more easily understood
as being a system of layers, and that we skip non-matching layers.

the env system also had the problem that it was a tree that was
dynamically constructed as a side-effect of the execution tree, but each
chain only needs to read its own ancestry in the environment tree.
Having a single tree poses challenges for parellelizing the execution of
different test chains, so instead, I would rather keep a list of layers
for each chain, instead of keeping a single environment tree that is
shared across all chains in the run. Additionally, the env system was
sparse; tests that save no data put no nodes in the env tree. I would
like to be able to display to users the layers available to their test
in certain error cases so that they can understand why a match may have
failed. Right now, in practice, encountering a match failure in a test
plan is confusing to understand.
selection
Jordan Orelli 4 years ago
parent df36c8073c
commit d5e30c26fe

@ -1,6 +1,12 @@
package tea
import "testing"
// this is just a collection of ... reusable assertions for the unit tests for
// tea itself.
import (
"errors"
"testing"
)
type labelChecker struct {
name string
@ -46,3 +52,23 @@ func (l *labelChecker) report(t *testing.T) {
}
}
}
func assertError(t *testing.T, fatal bool, err error, target error) {
if !errors.Is(err, target) {
if fatal {
t.Fatalf("expected error to be %s, instead found: %s", target, err)
} else {
t.Errorf("expected error to be %s, instead found: %s", target, err)
}
}
}
func assertNoError(t *testing.T, fatal bool, err error) {
if err != nil {
if fatal {
t.Fatalf("encountered unexpected error: %v", err)
} else {
t.Fatalf("encountered unexpected error: %v", err)
}
}
}

@ -0,0 +1,7 @@
package tea
// xchain is a chain of xnodes. an xchain is an execution plan for executing a
// sequence of tests. somwhat ironically the nodes are actually in a slice.
type xchain struct {
xnodes []*xnode
}

@ -60,7 +60,7 @@ func (e *env) save(test Test) *env {
saved := make(map[string]interface{})
for i := 0; i < T.NumField(); i++ {
f := T.Field(i)
if !isSaveField(f) {
if save, _ := isSaveField(f); !save {
continue
}

@ -4,5 +4,7 @@ type testError string
func (e testError) Error() string { return string(e) }
const PlanError = testError("test plan error")
const RunError = testError("test run error")
const (
PlanError = testError("test plan error")
RunError = testError("test run error")
)

@ -0,0 +1,34 @@
package tea
// generator functions for creating randomized test data.
import (
"math/rand"
"time"
)
// alpha is an alphabet of letters to pick from when producing random strings.
// the selected characters are human-readable without much visual ambiguity and
// include some code points beyond the ascii range to make sure things don't
// break on unicode input.
var alpha = []rune("" +
// lower-case ascii letters
"abcdefghjkmnpqrstwxyz" +
// upper-case ascii letters
"ABCDEFGHIJKLMNPQRSTWXYZ" +
// some digits
"23456789" +
// miscellaneous non-ascii characters
"¢£¥ÐÑØæñþÆŁřƩλЖд")
func rstring(n int) string {
r := make([]rune, n)
for i, _ := range r {
r[i] = alpha[rand.Intn(len(alpha))]
}
return string(r)
}
func init() {
rand.Seed(time.Now().Unix())
}

@ -0,0 +1,59 @@
package tea
import "reflect"
type sval struct {
name string
val interface{}
}
// layerData is an ordered list of key-value pairs. We preserve the ordering of
// the fields from the structs that produced the layer so that viewing a list
// of layers shows each layer from the sam struct in the same value-order,
// which is the order those fields appear in their originating stuct (and not,
// like, alphabetical order). Although a bit more tedious to work with
// internally, this is intended to make it possible to write more descriptive,
// easily understood PlanError messages.
type layerData []sval
func (l layerData) get(key string) (val interface{}, present bool) {
for _, pair := range l {
if pair.name == key {
return pair.val, true
}
}
return nil, false
}
type layer struct {
origin *xnode
saved layerData
}
func makeLayerData(test Test) (layerData, error) {
V := reflect.ValueOf(test)
if V.Type().Kind() == reflect.Ptr {
V = V.Elem()
}
T := V.Type()
if T.Kind() != reflect.Struct {
return nil, nil
}
fields, err := getSaveFields(T)
if err != nil {
return nil, err
}
if len(fields) == 0 {
// is this weird? maybe this is weird.
return nil, nil
}
data := make(layerData, 0, len(fields))
for _, f := range fields {
fv := V.FieldByName(f.Name).Interface()
data = append(data, sval{name: f.Name, val: fv})
}
return data, nil
}

@ -0,0 +1,95 @@
package tea
import (
"math/rand"
"testing"
)
func TestLayerData(t *testing.T) {
t.Run("non-struct tests produce empty layers", func(t *testing.T) {
lr, err := makeLayerData(Pass)
if len(lr) != 0 {
t.Errorf("expected a nil layer but saw %v instead", lr)
}
if err != nil {
t.Errorf("expected no error from lay but saw %v instead", err)
}
})
t.Run("save tags on unexported fields are plan errors", func(t *testing.T) {
type T struct {
Passing
count int `tea:"save"`
}
_, err := makeLayerData(T{})
assertError(t, true, err, PlanError)
})
t.Run("mixed exported/unexported fields still an error", func(t *testing.T) {
type T struct {
Passing
count int `tea:"save"`
Bar string `tea:"save"`
}
_, err := makeLayerData(T{})
assertError(t, true, err, PlanError)
})
t.Run("mixed exported/unexported fields still an error", func(t *testing.T) {
type T struct {
Passing
count int `tea:"save"`
bar string `tea:"save"`
}
_, err := makeLayerData(T{})
assertError(t, true, err, PlanError)
})
t.Run("save one int", func(t *testing.T) {
type T struct {
Passing
Count int `tea:"save"`
}
test := T{Count: rand.Int()}
data, err := makeLayerData(test)
assertNoError(t, true, err)
if len(data) == 0 {
t.Fatalf("expected nonempty layer, saw empty layer instead")
}
if v, ok := data.get("Count"); !ok {
t.Errorf("layer data is missing expected field Count")
} else {
if v != test.Count {
t.Errorf("layer data expected Count value of %d but saw %d instead", test.Count, v)
}
}
})
t.Run("an int and a string", func(t *testing.T) {
type T struct {
Passing
Count int `tea:"save"`
Name string `tea:"save"`
}
test := T{Count: rand.Int(), Name: rstring(8)}
data, err := makeLayerData(test)
assertNoError(t, true, err)
if len(data) == 0 {
t.Fatalf("expected nonempty layer, saw empty layer instead")
}
if v, ok := data.get("Count"); !ok {
t.Errorf("layer data is missing expected field Count")
} else {
if v != test.Count {
t.Errorf("layer data expected Count value of %d but saw %d instead", test.Count, v)
}
}
if v, ok := data.get("Name"); !ok {
t.Errorf("layer data is missing expected field Count")
} else {
if v != test.Name {
t.Errorf("layer data expected Name value of %s but saw %s instead", test.Name, v)
}
}
})
}

@ -1,6 +1,8 @@
package tea
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
@ -100,20 +102,61 @@ func (t *Tree) Child(test Test) *Tree {
return child
}
func isExported(f reflect.StructField) bool {
// PkgPath is the package path that qualifies a lower case (unexported)
// field name. It is empty for upper case (exported) field names.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers
return f.PkgPath == ""
}
// 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
func isSaveField(f reflect.StructField) (bool, error) {
if !isExported(f) {
return false, errors.New("unexported field cannot be marked as save field")
}
parts := strings.Split(f.Tag.Get("tea"), ",")
for _, part := range parts {
if part == "save" {
return true
return true, nil
}
}
return false
return false, nil
}
func getSaveFields(t reflect.Type) ([]reflect.StructField, error) {
type fieldError struct {
fieldName string
err error
}
var errs []fieldError
var fields []reflect.StructField
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
save, err := isSaveField(f)
if err != nil {
errs = append(errs, fieldError{fieldName: f.Name, err: err})
continue
}
if save {
fields = append(fields, f)
}
}
switch len(errs) {
case 0:
return fields, nil
case 1:
return nil, fmt.Errorf("%w: unable to read save field %s in %s: %v", PlanError, errs[0].fieldName, t.Name(), errs[0].err)
default:
messages := make([]string, 0, len(errs))
for _, fe := range errs {
messages = append(messages, fmt.Sprintf("{%s: %v}", fe.fieldName, fe.err))
}
return nil, fmt.Errorf("%w: save error encountered in %d fields of %s: [%s]", PlanError, len(errs), t.Name(), strings.Join(messages, ", "))
}
}
// isLoadField takes a struct field and checks its tags for a load tag,

Loading…
Cancel
Save