Compare commits

..

1 Commits

Author SHA1 Message Date
Jordan Orelli 7363c4b2c4 update example test for clarity 4 years ago

@ -1,74 +0,0 @@
package tea
// this is just a collection of ... reusable assertions for the unit tests for
// tea itself.
import (
"errors"
"testing"
)
type labelChecker struct {
name string
wanted map[string]bool // all the strings we want
found map[string]bool // all the strings we've found
}
func wantStrings(name string, want ...string) labelChecker {
l := newLabelChecker(name)
l.want(want...)
return l
}
func newLabelChecker(name string) labelChecker {
return labelChecker{
name: name,
wanted: make(map[string]bool),
found: make(map[string]bool),
}
}
func (l *labelChecker) want(names ...string) {
for _, name := range names {
l.wanted[name] = true
}
}
func (l *labelChecker) add(name string) {
l.found[name] = true
}
func (l *labelChecker) report(t *testing.T) {
for name, _ := range l.found {
if l.wanted[name] {
t.Logf("%s saw expected value %s", l.name, name)
} else {
t.Errorf("%s saw unexpected value %s", l.name, name)
}
}
for name, _ := range l.wanted {
if !l.found[name] {
t.Errorf("%s missing expected value %s", l.name, name)
}
}
}
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)
}
}
}

@ -1,7 +0,0 @@
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
}

@ -1,33 +0,0 @@
package tea
// these constant tests are useful for testing things that manipulate the
// graph.
const (
A = Passing("A")
B = Passing("B")
C = Passing("C")
D = Passing("D")
E = Passing("E")
F = Passing("F")
G = Passing("G")
H = Passing("H")
I = Passing("I")
J = Passing("J")
K = Passing("K")
L = Passing("L")
M = Passing("M")
N = Passing("N")
O = Passing("O")
P = Passing("P")
Q = Passing("Q")
R = Passing("R")
S = Passing("S")
T = Passing("T")
U = Passing("U")
V = Passing("V")
W = Passing("W")
X = Passing("X")
Y = Passing("Y")
Z = Passing("Z")
)

@ -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 save, _ := isSaveField(f); !save {
if !isSaveField(f) {
continue
}

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

@ -8,6 +8,9 @@ import (
"github.com/jordanorelli/tea"
)
// testStartServer is a test that checks that the server can start. If this
// test passes, we retain a reference to the server for future tests to
// utilize.
type testStartServer struct {
Server *httptest.Server `tea:"save"`
}
@ -23,14 +26,16 @@ func (test *testStartServer) After(t *testing.T) {
test.Server.Close()
}
type testRequest struct {
// testHits sends a request to a hitcount server created in a previous test,
// checking that the number of hits returned matches what we expect.
type testHits struct {
Server *httptest.Server `tea:"load"`
path string
expect int
path string
hits int
}
func (test *testRequest) Run(t *testing.T) {
func (test *testHits) Run(t *testing.T) {
client := test.Server.Client()
res, err := client.Get(test.Server.URL + test.path)
@ -44,50 +49,39 @@ func (test *testRequest) Run(t *testing.T) {
t.Fatalf("response at %s was not json: %v", test.path, err)
}
if body.Hits != test.expect {
t.Errorf("expected a count of %d but saw %d", test.expect, body.Hits)
if body.Hits != test.hits {
t.Errorf("expected a count of %d hits but saw %d instead", test.hits, body.Hits)
}
}
func TestServer(t *testing.T) {
type list []testRequest
// start with a root node that creates our test server
root := tea.New(&testStartServer{})
runSeries := func(node *tea.Tree, tests list) *tea.Tree {
for i := 0; i < len(tests); i++ {
node = node.Child(&tests[i])
}
return node
}
// add a child node: this test is run if the root test passes. If the root
// test is failed, this test and all of its descendents are logged as
// skipped.
one := root.Child(&testHits{path: "/alice", hits: 1})
runParallel := func(node *tea.Tree, tests list) {
for i := 0; i < len(tests); i++ {
node.Child(&tests[i])
}
}
// the effects of the first test create the initial state for the second test.
two := one.Child(&testHits{path: "/alice", hits: 2})
root := tea.New(&testStartServer{})
// since we have never visited /bob, we know that bob should only have one hit.
two.Child(&testHits{path: "/bob", hits: 1})
// but we could also run the exact same test off of the root, like so:
root.Child(&testHits{path: "/bob", hits: 1})
// since tests are values in tea, we can re-use the exact same test from
// different initial states by saving the test as a variable.
bob := &testHits{path: "/bob", hits: 1}
runSeries(root, list{
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 2},
{path: "/users/alice", expect: 3},
{path: "/users/alice", expect: 4},
})
runSeries(root, list{
{path: "/users/bob", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 2},
{path: "/users/alice", expect: 3},
{path: "/users/bob", expect: 2},
})
runParallel(root, list{
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
})
// these two executions of the same test value are operating on different
// program states. Since they are not in the same sequence, they have no
// effect on one another, even though they're utilizing the same test
// value.
two.Child(bob)
root.Child(bob)
tea.Run(t, root)
}

@ -1,34 +0,0 @@
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())
}

@ -1,59 +0,0 @@
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
}

@ -1,95 +0,0 @@
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,158 +0,0 @@
package tea
import "fmt"
var lastID int
func nextNodeID() int {
lastID++
return lastID
}
// lnode is a node in the logical graph. Developers create a logical graph in
// which nodes may have more than one parent. Each test value written by a
// developer appears as one logical node in the logical graph. The public
// documentation refers to the logical graph as simply the test graph.
type lnode struct {
id int
name string
xnodes []xnode
test Test
parents []*lnode
children []*lnode
}
// newLNode greates a new lnode with the provided test and the list of parents.
// If there are no parents, the lnode is a root node, and the provided test is
// run once. Otherwise, the provided test is used to create xnodes that are
// children of all of the provided parent nodes' xnodes.
func newLNode(test Test, sel Selection) *lnode {
if len(sel.nodes) == 0 {
return rootLNode(test)
}
node := lnode{
id: nextNodeID(),
name: parseName(test),
test: test,
parents: make([]*lnode, len(sel.nodes)),
}
// not sure if this copy is necessary
copy(node.parents, sel.nodes)
xID := 0
for _, parent := range node.parents {
parent.children = append(parent.children, &node)
for i, _ := range parent.xnodes {
x := xnode{
id: xID,
lnode: &node,
parent: &parent.xnodes[i],
}
node.xnodes = append(node.xnodes, x)
xID++
}
}
for i, x := range node.xnodes {
x.parent.children = append(x.parent.children, &node.xnodes[i])
}
return &node
}
// rootLNode creates a root lnode. This case is a lot simpler so I split it out
// to keep newLNode a little more readable.
func rootLNode(test Test) *lnode {
id := nextNodeID()
node := lnode{
id: id,
name: parseName(test),
test: test,
}
node.xnodes = []xnode{{id: 0, lnode: &node}}
return &node
}
// xnode is a node in the execution graph, representing one instance of a test
// to be executed. xnode is the unit test in tea. every xnode is either
// unparented or has one parent.
type xnode struct {
id int // id within the parent lnode
lnode *lnode // corresponding node in the logical test graph
parent *xnode
children []*xnode
}
func (x *xnode) isOnlyTestInLNode() bool {
return len(x.lnode.xnodes) == 1
}
// label must be unique or some other shit will break, I'm using this as a way
// to globally identify xnodes, which may be very flawed and maybe I should
// have an actual global ID system.
func (x *xnode) label() string {
if x.parent == nil {
switch {
case len(x.lnode.children) < 10:
return fmt.Sprintf("%s.%d", x.lnode.name, x.id)
case len(x.lnode.children) < 100:
return fmt.Sprintf("%s.%02d", x.lnode.name, x.id)
case len(x.lnode.children) < 1000:
return fmt.Sprintf("%s.%03d", x.lnode.name, x.id)
default:
return fmt.Sprintf("%s.%04d", x.lnode.name, x.id)
}
} else {
switch {
case len(x.lnode.children) < 10:
return fmt.Sprintf("%s.%d.%s", x.lnode.name, x.id, x.parent.lnode.name)
case len(x.lnode.children) < 100:
return fmt.Sprintf("%s.%02d.%s", x.lnode.name, x.id, x.parent.lnode.name)
case len(x.lnode.children) < 1000:
return fmt.Sprintf("%s.%03d.%s", x.lnode.name, x.id, x.parent.lnode.name)
default:
return fmt.Sprintf("%s.%04d.%s", x.lnode.name, x.id, x.parent.lnode.name)
}
}
}
// ancestry gives a slice of xnodes beginning at the root of the x graph and
// terminating at the receiver xnode. The ancestry list of a leaf node in the x
// graph is a single chain of tests.
func (x *xnode) ancestry() []*xnode {
if x.parent == nil {
return []*xnode{x}
}
return append(x.parent.ancestry(), x)
}
// descendents gives a slice of all xnodes whose ancestry includes the receiver
// xnode, in depth-first order.
func (x *xnode) descendents() []*xnode {
if len(x.children) == 0 {
return nil
}
descendents := make([]*xnode, 0, len(x.children))
for _, c := range x.children {
descendents = append(descendents, c)
descendents = append(descendents, c.descendents()...)
}
return descendents
}
// leaves descends the x graph from the receiver xnode, returning a slice
// containing all of the leaves of the x graph having the receiver x as an
// ancestor.
func (x *xnode) leaves() []*xnode {
if len(x.children) == 0 {
return []*xnode{x}
}
var leaves []*xnode
for _, child := range x.children {
leaves = append(leaves, child.leaves()...)
}
return leaves
}

@ -1,8 +0,0 @@
package tea
import (
"testing"
)
func TestXLabels(t *testing.T) {
}

@ -1,73 +0,0 @@
package tea
func NewSelection(test Test) Selection {
node := newLNode(test, Selection{})
return Selection{nodes: []*lnode{node}}
}
// Selection represents a set of nodes in our graph.
type Selection struct {
nodes []*lnode
}
func (s Selection) Child(test Test) Selection {
node := newLNode(test, s)
return Selection{nodes: []*lnode{node}}
}
func (s Selection) And(other Selection) Selection {
included := make(map[int]bool)
out := make([]*lnode, 0, len(s.nodes)+len(other.nodes))
for _, n := range append(s.nodes, other.nodes...) {
if !included[n.id] {
out = append(out, n)
included[n.id] = true
}
}
return Selection{nodes: out}
}
// xnodes represents all xnodes in the selected lnodes
func (s Selection) xnodes() []*xnode {
xnodes := make([]*xnode, 0, s.countXNodes())
for _, L := range s.nodes {
for i, _ := range L.xnodes {
xnodes = append(xnodes, &L.xnodes[i])
}
}
return xnodes
}
func (s Selection) countXNodes() int {
total := 0
for _, child := range s.nodes {
total += len(child.xnodes)
}
return total
}
// xleaves looks at all of the selected xnodes, and for every selected xnode,
// traverses the x graph until we arrive at the set of all leaf nodes that have
// a selected ancestor. If the selection consists of the root node, the xleaves
// are all of the leaves of the x graph.
func (s *Selection) xleaves() []*xnode {
// honestly think that by definition every xnode in the selection has a
// non-overlapping set of leaves but thinking about this shit is extremely
// starting to hurt my brain so I'm going to write this in a way that's
// maybe very redundant.
seen := make(map[string]bool)
var leaves []*xnode
for _, x := range s.xnodes() {
for _, leaf := range x.leaves() {
if seen[leaf.label()] {
panic("double-counting leaves somehow")
}
seen[leaf.label()] = true
leaves = append(leaves, leaf)
}
}
return leaves
}

@ -1,219 +0,0 @@
package tea
import (
"testing"
)
type selectionTest struct {
label string
selection Selection
lnodes []string
xnodes []string
xleaves []string
}
func (test *selectionTest) Run(t *testing.T) {
LWanted := wantStrings("selected lnode names", test.lnodes...)
for _, L := range test.selection.nodes {
LWanted.add(L.name)
}
LWanted.report(t)
XWanted := wantStrings("selected xnode labels", test.xnodes...)
for _, X := range test.selection.xnodes() {
XWanted.add(X.label())
}
XWanted.report(t)
XLeavesWanted := wantStrings("leaf xnode labels", test.xleaves...)
for _, X := range test.selection.xnodes() {
for _, leaf := range X.leaves() {
XLeavesWanted.add(leaf.label())
}
}
XLeavesWanted.report(t)
}
func TestSelections(t *testing.T) {
tests := []selectionTest{
{
label: "new selection",
selection: NewSelection(A),
lnodes: []string{"A"},
xnodes: []string{"A.0"},
xleaves: []string{"A.0"},
},
{
label: "root with one child",
selection: NewSelection(A).Child(B),
lnodes: []string{"B"},
xnodes: []string{"B.0.A"},
xleaves: []string{"B.0.A"},
},
{
label: "two selected roots",
selection: NewSelection(A).And(NewSelection(B)),
lnodes: []string{"A", "B"},
xnodes: []string{"A.0", "B.0"},
xleaves: []string{"A.0", "B.0"},
},
}
add := func(fn func() selectionTest) { tests = append(tests, fn()) }
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
return selectionTest{
label: "root and child selected",
selection: root.And(b),
lnodes: []string{"A", "B"},
xnodes: []string{"A.0", "B.0.A"},
xleaves: []string{"B.0.A"},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
return selectionTest{
label: "an optional test",
selection: root.And(b).Child(C),
lnodes: []string{"C"},
xnodes: []string{"C.0.A", "C.1.B"},
xleaves: []string{"C.0.A", "C.1.B"},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
return selectionTest{
label: "two children selected",
selection: b.And(c),
lnodes: []string{"B", "C"},
xnodes: []string{"B.0.A", "C.0.A"},
xleaves: []string{"B.0.A", "C.0.A"},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
return selectionTest{
label: "a diamond test",
selection: b.And(c).Child(D),
lnodes: []string{"D"},
xnodes: []string{"D.0.B", "D.1.C"},
xleaves: []string{"D.0.B", "D.1.C"},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
d := b.And(c).Child(D)
return selectionTest{
label: "child of a node having multiple parents",
selection: d.Child(E),
lnodes: []string{"E"},
xnodes: []string{"E.0.D", "E.1.D"},
xleaves: []string{"E.0.D", "E.1.D"},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
d := b.And(c).Child(D)
d.Child(E)
return selectionTest{
label: "the root of a complex graph",
selection: root,
lnodes: []string{"A"},
xnodes: []string{"A.0"},
xleaves: []string{"E.0.D", "E.1.D"},
}
})
// A
// / \
// / \
// B C
// / \ / \
// / \ / \
// D E F
// / \ / \
// / \ / \
// G H I
// | | |
// | | |
// J K L
// | \ /
// | \ /
// M N
//
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
b.Child(D)
e := b.And(c).Child(E)
f := c.Child(F)
e.Child(G).Child(J).Child(M)
h := e.And(f).Child(H)
l := f.Child(I).Child(L)
k := h.Child(K)
k.And(l).Child(N)
return selectionTest{
label: "criss-crossing",
selection: root,
lnodes: []string{"A"},
xnodes: []string{"A.0"},
xleaves: []string{
"D.0.B", // A B D
"M.0.J", // A B E G J M
"M.1.J", // A C E G J M
"N.0.K", // A B E H K N
"N.1.K", // A C E H K N
"N.2.K", // A C F H K N
"N.3.L", // A C F I L N
},
}
})
add(func() selectionTest {
root := NewSelection(A)
b := root.Child(B)
c := root.Child(C)
b.Child(D)
e := b.And(c).Child(E)
f := c.Child(F)
e.Child(G).Child(J).Child(M)
h := e.And(f).Child(H)
l := f.Child(I).Child(L)
k := h.Child(K)
k.And(l).Child(N)
return selectionTest{
label: "criss-crossing-partial",
selection: e,
lnodes: []string{"E"},
xnodes: []string{"E.0.B", "E.1.C"},
xleaves: []string{
"M.0.J", // A B E G J M
"M.1.J", // A C E G J M
"N.0.K", // A B E H K N
"N.1.K", // A C E H K N
},
}
})
for _, test := range tests {
t.Run(test.label, test.Run)
}
}

@ -12,22 +12,6 @@ type Test interface {
Run(*testing.T)
}
// clone clones a test value, yielding a new test value that can be executed
// and mutated such that the original is not mutated. Tests containing pointers
// to objects that were not created by tea will probably not work right. That's
// like, kinda on you though, I can't really enforce things that the Go type
// system doesn't let me enforce.
func clone(t Test) Test {
v := reflect.ValueOf(t)
switch v.Kind() {
case reflect.Ptr:
v = v.Elem()
}
destV := reflect.New(v.Type())
destV.Elem().Set(v)
return destV.Interface().(Test)
}
// After defines the interface used for performing test cleanup. If a Test
// value also implements After, that test's After method will be called after
// all tests are run. Tests in a sequence will have their After methods called
@ -57,7 +41,6 @@ const Pass = Passing("test passed")
type Passing string
func (p Passing) Run(t *testing.T) {}
func (p Passing) String() string { return string(p) }
// parseName parses the name for a given test
func parseName(test Test) string {

@ -1,8 +1,6 @@
package tea
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
@ -102,61 +100,29 @@ 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 == ""
// clone clones a test value, yielding a new test value that can be executed
// and mutated such that the original is not mutated.
func clone(t Test) Test {
srcV := reflect.ValueOf(t).Elem()
destV := reflect.New(srcV.Type())
destV.Elem().Set(srcV)
return destV.Interface().(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, error) {
if !isExported(f) {
return false, errors.New("unexported field cannot be marked as save field")
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" {
return true, nil
}
}
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 true
}
return nil, fmt.Errorf("%w: save error encountered in %d fields of %s: [%s]", PlanError, len(errs), t.Name(), strings.Join(messages, ", "))
}
return false
}
// isLoadField takes a struct field and checks its tags for a load tag,

Loading…
Cancel
Save