Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
Jordan Orelli | d5e30c26fe | 4 years ago |
Jordan Orelli | df36c8073c | 4 years ago |
Jordan Orelli | 00ba88a520 | 4 years ago |
Jordan Orelli | 31fa2eb711 | 4 years ago |
Jordan Orelli | 500b02db34 | 4 years ago |
Jordan Orelli | 4d5a33dbdb | 4 years ago |
Jordan Orelli | 75849b2f36 | 4 years ago |
Jordan Orelli | 8659b230c8 | 4 years ago |
Jordan Orelli | acb92cc4d6 | 4 years ago |
@ -0,0 +1,74 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
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")
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestXLabels(t *testing.T) {
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,219 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue