tools/src/cts/query: Add Tree
A tree of query to data. Has utilities for reducing the tree based on a custom merger function. Bug: dawn:1342 Change-Id: If1c0503be05ee04bcf55dd5bdc9aa3caf6fb56ee Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/87222 Reviewed-by: Corentin Wallez <cwallez@chromium.org> Commit-Queue: Ben Clayton <bclayton@google.com>
This commit is contained in:
parent
4c9b72b4fa
commit
1a10b73552
|
@ -14,7 +14,10 @@
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import "sort"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
// Set is a generic unordered set, which wrap's go's builtin 'map'.
|
// Set is a generic unordered set, which wrap's go's builtin 'map'.
|
||||||
// T is the set key, which must match the 'key' constraint.
|
// T is the set key, which must match the 'key' constraint.
|
||||||
|
@ -98,3 +101,24 @@ func (s Set[T]) List() []T {
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One returns a random item from the set, or an empty item if the set is empty.
|
||||||
|
func (s Set[T]) One() T {
|
||||||
|
for item := range s {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
var zero T
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format writes the Target to the fmt.State
|
||||||
|
func (s Set[T]) Format(f fmt.State, verb rune) {
|
||||||
|
fmt.Fprint(f, "[")
|
||||||
|
for i, item := range s.List() {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprint(f, ", ")
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, item)
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, "]")
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package container_test
|
package container_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dawn.googlesource.com/dawn/tools/src/container"
|
"dawn.googlesource.com/dawn/tools/src/container"
|
||||||
|
@ -143,3 +144,19 @@ func TestSetRemoveAll(t *testing.T) {
|
||||||
expectEq(t, "len(s)", len(s), 1)
|
expectEq(t, "len(s)", len(s), 1)
|
||||||
expectEq(t, "s.List()", s.List(), []string{"b"})
|
expectEq(t, "s.List()", s.List(), []string{"b"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetOne(t *testing.T) {
|
||||||
|
expectEq(t, "NewSet[string]().One()", container.NewSet[string]().One(), "")
|
||||||
|
expectEq(t, `NewSet("x").One()`, container.NewSet("x").One(), "x")
|
||||||
|
if got := container.NewSet("x", "y").One(); got != "x" && got != "y" {
|
||||||
|
t.Errorf(`NewSet("x", "y").One() returned "%v"`, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormat(t *testing.T) {
|
||||||
|
expectEq(t, "NewSet[string]()", fmt.Sprint(container.NewSet[string]()), "[]")
|
||||||
|
expectEq(t, `NewSet("x")`, fmt.Sprint(container.NewSet("x")), `[x]`)
|
||||||
|
expectEq(t, `NewSet(1)`, fmt.Sprint(container.NewSet(1)), `[1]`)
|
||||||
|
expectEq(t, `NewSet("y", "x")`, fmt.Sprint(container.NewSet("y", "x")), `[x, y]`)
|
||||||
|
expectEq(t, `NewSet(3, 1, 2)`, fmt.Sprint(container.NewSet(3, 1, 2)), `[1, 2, 3]`)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2022 The Dawn Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package query
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ErrNoDataForQuery struct {
|
||||||
|
Query Query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrNoDataForQuery) Error() string {
|
||||||
|
return fmt.Sprintf("no data for query '%v'", e.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrDuplicateData struct {
|
||||||
|
Query Query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrDuplicateData) Error() string {
|
||||||
|
return fmt.Sprintf("duplicate data '%v'", e.Query)
|
||||||
|
}
|
|
@ -198,6 +198,18 @@ func (q Query) CaseParameters() CaseParameters {
|
||||||
// Append returns the query with the additional strings appended to the target
|
// Append returns the query with the additional strings appended to the target
|
||||||
func (q Query) Append(t Target, n ...string) Query {
|
func (q Query) Append(t Target, n ...string) Query {
|
||||||
switch t {
|
switch t {
|
||||||
|
case Suite:
|
||||||
|
switch len(n) {
|
||||||
|
case 0:
|
||||||
|
return q
|
||||||
|
case 1:
|
||||||
|
if q.Suite != "" {
|
||||||
|
panic("cannot append suite when query already contains suite")
|
||||||
|
}
|
||||||
|
return Query{Suite: n[0]}
|
||||||
|
default:
|
||||||
|
panic("cannot append more than one suite")
|
||||||
|
}
|
||||||
case Files:
|
case Files:
|
||||||
return q.AppendFiles(n...)
|
return q.AppendFiles(n...)
|
||||||
case Tests:
|
case Tests:
|
||||||
|
|
|
@ -0,0 +1,404 @@
|
||||||
|
// Copyright 2022 The Dawn Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree holds a tree structure of Query to generic Data type.
|
||||||
|
// Each separate suite, file, test of the query produces a separate tree node.
|
||||||
|
// All cases of the query produce a single leaf tree node.
|
||||||
|
type Tree[Data any] struct {
|
||||||
|
TreeNode[Data]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeNode is a single node in the Tree
|
||||||
|
type TreeNode[Data any] struct {
|
||||||
|
// The full query of the node
|
||||||
|
Query Query
|
||||||
|
// The data associated with this node. nil is used to represent no-data.
|
||||||
|
Data *Data
|
||||||
|
// Children of the node. Keyed by query.Target and name.
|
||||||
|
Children TreeNodeChildren[Data]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeNodeChildKey is the key used by TreeNode for the Children map
|
||||||
|
type TreeNodeChildKey struct {
|
||||||
|
// The child name. This is the string between `:` and `,` delimiters.
|
||||||
|
// Note: that all test cases are held by a single TreeNode.
|
||||||
|
Name string
|
||||||
|
// The target type of the child. Examples:
|
||||||
|
// Query | Target of 'child'
|
||||||
|
// -----------------+--------------------
|
||||||
|
// parent:child | Files
|
||||||
|
// parent:x,child | Files
|
||||||
|
// parent:x:child | Test
|
||||||
|
// parent:x:y,child | Test
|
||||||
|
// parent:x:y:child | Cases
|
||||||
|
//
|
||||||
|
// It's possible to have a directory and '.spec.ts' share the same name,
|
||||||
|
// hence why we include the Target as part of the child key.
|
||||||
|
Target Target
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeNodeChildren is a map of TreeNodeChildKey to TreeNode pointer.
|
||||||
|
// Data is the data type held by a TreeNode.
|
||||||
|
type TreeNodeChildren[Data any] map[TreeNodeChildKey]*TreeNode[Data]
|
||||||
|
|
||||||
|
// sortedChildKeys returns all the sorted children keys.
|
||||||
|
func (n *TreeNode[Data]) sortedChildKeys() []TreeNodeChildKey {
|
||||||
|
keys := make([]TreeNodeChildKey, 0, len(n.Children))
|
||||||
|
for key := range n.Children {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
a, b := keys[i], keys[j]
|
||||||
|
switch {
|
||||||
|
case a.Name < b.Name:
|
||||||
|
return true
|
||||||
|
case a.Name > b.Name:
|
||||||
|
return false
|
||||||
|
case a.Target < b.Target:
|
||||||
|
return true
|
||||||
|
case a.Target > b.Target:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// traverse performs a depth-first-search of the tree calling f for each visited
|
||||||
|
// node, starting with n, then visiting each of children in sorted order
|
||||||
|
// (pre-order traversal).
|
||||||
|
func (n *TreeNode[Data]) traverse(f func(n *TreeNode[Data]) error) error {
|
||||||
|
if err := f(n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, key := range n.sortedChildKeys() {
|
||||||
|
if err := n.Children[key].traverse(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merger is a function used to merge the children nodes of a tree.
|
||||||
|
// Merger is called with the Data of each child node. If the function returns a
|
||||||
|
// non-nil Data pointer, then this is used as the merged result. If the function
|
||||||
|
// returns nil, then the node will not be merged.
|
||||||
|
type Merger[Data any] func([]Data) *Data
|
||||||
|
|
||||||
|
// merge collapses tree nodes based on child node data, using the function f.
|
||||||
|
// merge operates on the leaf nodes first, working its way towards the root of
|
||||||
|
// the tree.
|
||||||
|
// Returns the merged target data for this node, or nil if the node is not a
|
||||||
|
// leaf and its children has non-uniform data.
|
||||||
|
func (n *TreeNode[Data]) merge(f Merger[Data]) *Data {
|
||||||
|
// If the node is a leaf, then simply return the node's data.
|
||||||
|
if len(n.Children) == 0 {
|
||||||
|
return n.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map of child target to merged child data.
|
||||||
|
// A nil for the value indicates that one or more children could not merge.
|
||||||
|
mergedChildren := map[Target][]Data{}
|
||||||
|
for key, child := range n.Children {
|
||||||
|
// Call merge() on the child. Even if we cannot merge this node, we want
|
||||||
|
// to do this for all children so they can merge their sub-graphs.
|
||||||
|
childData := child.merge(f)
|
||||||
|
|
||||||
|
if childData == nil {
|
||||||
|
// If merge() returned nil, then the data could not be merged.
|
||||||
|
// Mark the entire target as unmergeable.
|
||||||
|
mergedChildren[key.Target] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the merge list for this child's target.
|
||||||
|
list, found := mergedChildren[key.Target]
|
||||||
|
if !found {
|
||||||
|
// First child with the given target?
|
||||||
|
mergedChildren[key.Target] = []Data{*childData}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if list != nil {
|
||||||
|
mergedChildren[key.Target] = append(list, *childData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merge := func(in []Data) *Data {
|
||||||
|
switch len(in) {
|
||||||
|
case 0:
|
||||||
|
return nil // nothing to merge.
|
||||||
|
case 1:
|
||||||
|
return &in[0] // merge of a single item results in that item
|
||||||
|
default:
|
||||||
|
return f(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Might it possible to merge this node?
|
||||||
|
maybeMergeable := true
|
||||||
|
|
||||||
|
// The merged data, per target
|
||||||
|
mergedTargets := map[Target]Data{}
|
||||||
|
|
||||||
|
// Attempt to merge each of the target's data
|
||||||
|
for target, list := range mergedChildren {
|
||||||
|
if list != nil { // nil == unmergeable target
|
||||||
|
if data := merge(list); data != nil {
|
||||||
|
// Merge success!
|
||||||
|
mergedTargets[target] = *data
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maybeMergeable = false // Merge of this node is not possible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all children that have been merged
|
||||||
|
for key := range n.Children {
|
||||||
|
if _, merged := mergedTargets[key.Target]; merged {
|
||||||
|
delete(n.Children, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add wildcards for merged targets
|
||||||
|
for target, data := range mergedTargets {
|
||||||
|
data := data // Don't take address of iterator
|
||||||
|
n.getOrCreateChild(TreeNodeChildKey{"*", target}).Data = &data
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any of the targets are unmergeable, then we cannot merge the node itself.
|
||||||
|
if !maybeMergeable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All targets were merged. Attempt to merge each of the targets.
|
||||||
|
data := make([]Data, 0, len(mergedTargets))
|
||||||
|
for _, d := range mergedTargets {
|
||||||
|
data = append(data, d)
|
||||||
|
}
|
||||||
|
return merge(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print writes a textual representation of this node and its children to w.
|
||||||
|
// prefix is used as the line prefix for each node, which is appended with
|
||||||
|
// whitespace for each child node.
|
||||||
|
func (n *TreeNode[Data]) print(w io.Writer, prefix string) {
|
||||||
|
fmt.Fprintf(w, "%v{\n", prefix)
|
||||||
|
fmt.Fprintf(w, "%v query: '%v'\n", prefix, n.Query)
|
||||||
|
fmt.Fprintf(w, "%v data: '%v'\n", prefix, n.Data)
|
||||||
|
for _, key := range n.sortedChildKeys() {
|
||||||
|
n.Children[key].print(w, prefix+" ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%v}\n", prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format implements the io.Formatter interface.
|
||||||
|
// See https://pkg.go.dev/fmt#Formatter
|
||||||
|
func (n *TreeNode[Data]) Format(f fmt.State, verb rune) {
|
||||||
|
n.print(f, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOrCreateChild returns the child with the given key if it exists,
|
||||||
|
// otherwise the child node is created and added to n and is returned.
|
||||||
|
func (n *TreeNode[Data]) getOrCreateChild(key TreeNodeChildKey) *TreeNode[Data] {
|
||||||
|
if n.Children == nil {
|
||||||
|
child := &TreeNode[Data]{Query: n.Query.Append(key.Target, key.Name)}
|
||||||
|
n.Children = TreeNodeChildren[Data]{key: child}
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
if child, ok := n.Children[key]; ok {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
child := &TreeNode[Data]{Query: n.Query.Append(key.Target, key.Name)}
|
||||||
|
n.Children[key] = child
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryData is a pair of a Query and a generic Data type.
|
||||||
|
// Used by NewTree for constructing a tree with entries.
|
||||||
|
type QueryData[Data any] struct {
|
||||||
|
Query Query
|
||||||
|
Data Data
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTree returns a new Tree populated with the given entries.
|
||||||
|
// If entries returns duplicate queries, then ErrDuplicateData will be returned.
|
||||||
|
func NewTree[Data any](entries ...QueryData[Data]) (Tree[Data], error) {
|
||||||
|
out := Tree[Data]{}
|
||||||
|
for _, qd := range entries {
|
||||||
|
if err := out.Add(qd.Query, qd.Data); err != nil {
|
||||||
|
return Tree[Data]{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a new data to the tree.
|
||||||
|
// Returns ErrDuplicateData if the tree already contains a data for the given
|
||||||
|
func (t *Tree[Data]) Add(q Query, d Data) error {
|
||||||
|
node := &t.TreeNode
|
||||||
|
q.Walk(func(q Query, t Target, n string) error {
|
||||||
|
node = node.getOrCreateChild(TreeNodeChildKey{n, t})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if node.Data != nil {
|
||||||
|
return ErrDuplicateData{node.Query}
|
||||||
|
}
|
||||||
|
node.Data = &d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce reduces the tree using the Merger function f.
|
||||||
|
// If the Merger function returns a non-nil Data value, then this will be used
|
||||||
|
// to replace the non-leaf node with a new leaf node holding the returned Data.
|
||||||
|
// This process recurses up to the tree root.
|
||||||
|
func (t *Tree[Data]) Reduce(f Merger[Data]) {
|
||||||
|
for _, root := range t.TreeNode.Children {
|
||||||
|
root.merge(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReduceUnder reduces the sub-tree under the given query using the Merger
|
||||||
|
// function f.
|
||||||
|
// If the Merger function returns a non-nil Data value, then this will be used
|
||||||
|
// to replace the non-leaf node with a new leaf node holding the returned Data.
|
||||||
|
// This process recurses up to the node pointed at by the query to.
|
||||||
|
func (t *Tree[Data]) ReduceUnder(to Query, f Merger[Data]) error {
|
||||||
|
node := &t.TreeNode
|
||||||
|
return to.Walk(func(q Query, t Target, n string) error {
|
||||||
|
if n == "*" {
|
||||||
|
node.merge(f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
child, ok := node.Children[TreeNodeChildKey{n, t}]
|
||||||
|
if !ok {
|
||||||
|
return ErrNoDataForQuery{q}
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
if q == to {
|
||||||
|
node.merge(f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// glob calls f for every node under the given query.
|
||||||
|
func (t *Tree[Data]) glob(fq Query, f func(f *TreeNode[Data]) error) error {
|
||||||
|
node := &t.TreeNode
|
||||||
|
return fq.Walk(func(q Query, t Target, n string) error {
|
||||||
|
if n == "*" {
|
||||||
|
// Wildcard reached.
|
||||||
|
// Glob the parent, but restrict to the wildcard target type.
|
||||||
|
for _, key := range node.sortedChildKeys() {
|
||||||
|
child := node.Children[key]
|
||||||
|
if child.Query.Target() == t {
|
||||||
|
if err := child.traverse(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case Suite, Files, Tests:
|
||||||
|
child, ok := node.Children[TreeNodeChildKey{n, t}]
|
||||||
|
if !ok {
|
||||||
|
return ErrNoDataForQuery{q}
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
case Cases:
|
||||||
|
for _, key := range node.sortedChildKeys() {
|
||||||
|
child := node.Children[key]
|
||||||
|
if child.Query.Contains(fq) {
|
||||||
|
if err := f(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if q == fq {
|
||||||
|
return node.traverse(f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace replaces the sub-tree matching the query 'what' with the Data 'with'
|
||||||
|
func (t *Tree[Data]) Replace(what Query, with Data) error {
|
||||||
|
node := &t.TreeNode
|
||||||
|
return what.Walk(func(q Query, t Target, n string) error {
|
||||||
|
childKey := TreeNodeChildKey{n, t}
|
||||||
|
if q == what {
|
||||||
|
for key, child := range node.Children {
|
||||||
|
// Use Query.Contains() to handle matching of Cases
|
||||||
|
// (which are not split into tree nodes)
|
||||||
|
if q.Contains(child.Query) {
|
||||||
|
delete(node.Children, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = node.getOrCreateChild(childKey)
|
||||||
|
node.Data = &with
|
||||||
|
} else {
|
||||||
|
child, ok := node.Children[childKey]
|
||||||
|
if !ok {
|
||||||
|
return ErrNoDataForQuery{q}
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the tree nodes flattened as a list of QueryData
|
||||||
|
func (t *Tree[Data]) List() []QueryData[Data] {
|
||||||
|
out := []QueryData[Data]{}
|
||||||
|
t.traverse(func(n *TreeNode[Data]) error {
|
||||||
|
if n.Data != nil {
|
||||||
|
out = append(out, QueryData[Data]{n.Query, *n.Data})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glob returns a list of QueryData's for every node that is under the given
|
||||||
|
// query, which holds data.
|
||||||
|
// Glob handles wildcards as well as non-wildcard queries:
|
||||||
|
// * A non-wildcard query will match the node itself, along with every node
|
||||||
|
// under the query. For example: 'a:b' will match every File and Test
|
||||||
|
// node under 'a:b', including 'a:b' itself.
|
||||||
|
// * A wildcard Query will include every node under the parent node with the
|
||||||
|
// matching Query target. For example: 'a:b:*' will match every Test
|
||||||
|
// node (excluding File nodes) under 'a:b', 'a:b' will not be included.
|
||||||
|
func (t *Tree[Data]) Glob(q Query) ([]QueryData[Data], error) {
|
||||||
|
out := []QueryData[Data]{}
|
||||||
|
err := t.glob(q, func(n *TreeNode[Data]) error {
|
||||||
|
if n.Data != nil {
|
||||||
|
out = append(out, QueryData[Data]{n.Query, *n.Data})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
|
@ -0,0 +1,934 @@
|
||||||
|
package query_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/container"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/query"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/utils"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
abort = "Abort"
|
||||||
|
crash = "Crash"
|
||||||
|
failure = "Failure"
|
||||||
|
pass = "Pass"
|
||||||
|
skip = "Skip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTree[Data any](t *testing.T, entries ...query.QueryData[Data]) (query.Tree[Data], error) {
|
||||||
|
return query.NewTree(entries...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSingle(t *testing.T) {
|
||||||
|
type Tree = query.Tree[string]
|
||||||
|
type Node = query.TreeNode[string]
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
type Children = query.TreeNodeChildren[string]
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
in QueryData
|
||||||
|
expect Tree
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:*`),
|
||||||
|
Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`*`, query.Files}: {
|
||||||
|
Query: Q(`suite:*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:a,*`),
|
||||||
|
Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`*`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:a,b:*`),
|
||||||
|
Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`*`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b:*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:a,b:c:*`),
|
||||||
|
Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`c`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b:c`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`*`, query.Cases}: {
|
||||||
|
Query: Q(`suite:a,b:c:*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:a,b,c:d,e:f="g";h=[1,2,3];i=4;*`),
|
||||||
|
Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`c`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b,c`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`d`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b,c:d`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`e`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b,c:d,e`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`f="g";h=[1,2,3];i=4;*`, query.Cases}: {
|
||||||
|
Query: Q(`suite:a,b,c:d,e:f="g";h=[1,2,3];i=4;*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ /////////////////////////////////////////////////////////////////////
|
||||||
|
in: QueryData{
|
||||||
|
Query: Q(`suite:a,b:c:d="e";*`), Data: pass,
|
||||||
|
},
|
||||||
|
expect: Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`c`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b:c`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`d="e";*`, query.Cases}: {
|
||||||
|
Query: Q(`suite:a,b:c:d="e";*`),
|
||||||
|
Data: &pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
got, err := NewTree(t, test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewTree(%v): %v", test.in, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, test.expect); diff != "" {
|
||||||
|
t.Errorf("NewTree(%v) tree was not as expected:\n%v", test.in, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewMultiple(t *testing.T) {
|
||||||
|
type Tree = query.Tree[string]
|
||||||
|
type Node = query.TreeNode[string]
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
type Children = query.TreeNodeChildren[string]
|
||||||
|
|
||||||
|
got, err := NewTree(t,
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTree() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect := Tree{
|
||||||
|
TreeNode: Node{
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`suite`, query.Suite}: {
|
||||||
|
Query: Q(`suite`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`a`, query.Files}: {
|
||||||
|
Query: Q(`suite:a`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: Q(`suite:a,b`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`c`, query.Tests}: {
|
||||||
|
Query: Q(`suite:a,b:c`),
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`d="e";*`, query.Cases}: {
|
||||||
|
Query: Q(`suite:a,b:c:d="e";*`),
|
||||||
|
Data: &failure,
|
||||||
|
},
|
||||||
|
query.TreeNodeChildKey{`f="g";*`, query.Cases}: {
|
||||||
|
Query: Q(`suite:a,b:c:f="g";*`),
|
||||||
|
Data: &skip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query.TreeNodeChildKey{`h`, query.Files}: {
|
||||||
|
Query: query.Query{
|
||||||
|
Suite: `suite`,
|
||||||
|
Files: `h`,
|
||||||
|
},
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`b`, query.Files}: {
|
||||||
|
Query: query.Query{
|
||||||
|
Suite: `suite`,
|
||||||
|
Files: `h,b`,
|
||||||
|
},
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`c`, query.Tests}: {
|
||||||
|
Query: query.Query{
|
||||||
|
Suite: `suite`,
|
||||||
|
Files: `h,b`,
|
||||||
|
Tests: `c`,
|
||||||
|
},
|
||||||
|
Children: Children{
|
||||||
|
query.TreeNodeChildKey{`f="g";*`, query.Cases}: {
|
||||||
|
Query: query.Query{
|
||||||
|
Suite: `suite`,
|
||||||
|
Files: `h,b`,
|
||||||
|
Tests: `c`,
|
||||||
|
Cases: `f="g";*`,
|
||||||
|
},
|
||||||
|
Data: &abort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, expect); diff != "" {
|
||||||
|
t.Errorf("NewTree() was not as expected:\n%v", diff)
|
||||||
|
t.Errorf("got:\n%v", got)
|
||||||
|
t.Errorf("expect:\n%v", expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithCollision(t *testing.T) {
|
||||||
|
type Tree = query.Tree[string]
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
got, err := NewTree(t,
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:*`), Data: skip},
|
||||||
|
)
|
||||||
|
expect := Tree{}
|
||||||
|
expectErr := query.ErrDuplicateData{
|
||||||
|
Query: Q(`suite:a,b:c:*`),
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(err, expectErr); diff != "" {
|
||||||
|
t.Errorf("NewTree() error was not as expected:\n%v", diff)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, expect); diff != "" {
|
||||||
|
t.Errorf("NewTree() was not as expected:\n%v", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
tree, err := NewTree(t,
|
||||||
|
QueryData{Query: Q(`suite:*`), Data: skip},
|
||||||
|
QueryData{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTree() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := tree.List()
|
||||||
|
expect := []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, expect); diff != "" {
|
||||||
|
t.Errorf("List() was not as expected:\n%v", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reducer is used by Reduce() and ReduceUnder() tests for reducing the tree.
|
||||||
|
// reducer returns a pointer to the common string if all strings in data are
|
||||||
|
// equal, otherwise returns nil
|
||||||
|
func reducer(data []string) *string {
|
||||||
|
if s := container.NewSet(data...); len(s) == 1 {
|
||||||
|
item := s.One()
|
||||||
|
return &item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReduce(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
name string
|
||||||
|
in []QueryData
|
||||||
|
expect []QueryData
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Different file results - A",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Different file results - B",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,d,*`), Data: skip},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,d,*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Different test results",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c:*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c:*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Same file results",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: failure},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Same test results",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c:*`), Data: failure},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "File vs test",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a:b,c*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,c*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Sibling cases, no reduce",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a:b:c;d=e;f=g;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a:b:c;d=e;f=h;*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a:b:c;d=e;f=g;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a:b:c;d=e;f=h;*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Sibling cases, reduce to test",
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a:b:c=1;d="x";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a:b:c=1;d="y";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a:z:*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a:b:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a:z:*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tree, err := NewTree(t, test.in...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test '%v':\nNewTree() returned %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tree.Reduce(reducer)
|
||||||
|
results := tree.List()
|
||||||
|
if diff := cmp.Diff(results, test.expect); diff != "" {
|
||||||
|
t.Errorf("Test '%v':\n%v", test.name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReduceUnder(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
location string
|
||||||
|
to query.Query
|
||||||
|
in []QueryData
|
||||||
|
expect []QueryData
|
||||||
|
expectErr error
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a,b,*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a,*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:*`), Data: failure},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a,*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a,*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:x`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expectErr: query.ErrNoDataForQuery{
|
||||||
|
Query: Q(`suite:x`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
location: utils.ThisLine(),
|
||||||
|
to: Q(`suite:a,b,c,*`),
|
||||||
|
in: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: pass},
|
||||||
|
},
|
||||||
|
expectErr: query.ErrNoDataForQuery{
|
||||||
|
Query: Q(`suite:a,b,c`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tree, err := NewTree(t, test.in...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("\n%v NewTree(): %v", test.location, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = tree.ReduceUnder(test.to, reducer)
|
||||||
|
if diff := cmp.Diff(err, test.expectErr); diff != "" {
|
||||||
|
t.Errorf("\n%v ReduceUnder(): %v", test.location, err)
|
||||||
|
}
|
||||||
|
results := tree.List()
|
||||||
|
if diff := cmp.Diff(results, test.expect); diff != "" {
|
||||||
|
t.Errorf("\n%v List(): %v", test.location, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplace(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
name string
|
||||||
|
base []QueryData
|
||||||
|
replacement QueryData
|
||||||
|
expect []QueryData
|
||||||
|
expectErr error
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Replace file. Direct",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:a,b,*`), skip},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Replace file. Indirect",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,c,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,d,*`), Data: pass},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:a,b,*`), skip},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,c,*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "File vs Test",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c,*`), Data: crash},
|
||||||
|
{Query: Q(`suite:a,b:d,*`), Data: abort},
|
||||||
|
{Query: Q(`suite:a,b,c,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,d,*`), Data: pass},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:a,b,*`), skip},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Cases. * with *",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:*`), Data: failure},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:file:test:*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Cases. Mixed with *",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:a=2,*`), Data: skip},
|
||||||
|
{Query: Q(`suite:file:test:a=3,*`), Data: crash},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:file:test:*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Cases. Replace partial - (a=1)",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=x;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=y;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:a=2;b=y;*`), Data: failure},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:file:test:a=1;*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;*`), Data: pass},
|
||||||
|
{Query: Q(`suite:file:test:a=2;b=y;*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Cases. Replace partial - (b=y)",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=x;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=y;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:a=2;b=y;*`), Data: failure},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:file:test:b=y;*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=x;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:file:test:b=y;*`), Data: pass},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Error. No data for query - short",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=x;*`), Data: failure},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:missing:*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:a=1;b=x;*`), Data: failure},
|
||||||
|
},
|
||||||
|
expectErr: query.ErrNoDataForQuery{Q(`suite:missing`)},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "Error. No data for query - long",
|
||||||
|
base: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:*`), Data: failure},
|
||||||
|
},
|
||||||
|
replacement: QueryData{Q(`suite:file:test,missing,*`), pass},
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:file:test:*`), Data: failure},
|
||||||
|
},
|
||||||
|
expectErr: query.ErrNoDataForQuery{Q(`suite:file:test,missing`)},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tree, err := NewTree(t, test.base...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test '%v':\nNewTree(): %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = tree.Replace(test.replacement.Query, test.replacement.Data)
|
||||||
|
if diff := cmp.Diff(err, test.expectErr); diff != "" {
|
||||||
|
t.Errorf("Test '%v':\nReplace() error: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(tree.List(), test.expect); diff != "" {
|
||||||
|
t.Errorf("Test '%v':\n%v", test.name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlob(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
tree, err := NewTree(t,
|
||||||
|
QueryData{Query: Q(`suite:*`), Data: skip},
|
||||||
|
QueryData{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
QueryData{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTree() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
query query.Query
|
||||||
|
expect []QueryData
|
||||||
|
expectErr error
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b,*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:c:*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:c`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:c:d="e";*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:c:d;*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:c:f="g";*`),
|
||||||
|
expect: []QueryData{
|
||||||
|
{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:x,y`),
|
||||||
|
expectErr: query.ErrNoDataForQuery{Q(`suite:x`)},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
query: Q(`suite:a,b:x`),
|
||||||
|
expectErr: query.ErrNoDataForQuery{Q(`suite:a,b:x`)},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
got, err := tree.Glob(test.query)
|
||||||
|
if diff := cmp.Diff(err, test.expectErr); diff != "" {
|
||||||
|
t.Errorf("Glob('%v') error: %v", test.query, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, test.expect); diff != "" {
|
||||||
|
t.Errorf("Glob('%v'):\n%v", test.query, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormat(t *testing.T) {
|
||||||
|
type QueryData = query.QueryData[string]
|
||||||
|
|
||||||
|
tree, err := NewTree(t,
|
||||||
|
QueryData{Query: Q(`suite:*`), Data: skip},
|
||||||
|
QueryData{Query: Q(`suite:a,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b,*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d;*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:d="e";*`), Data: failure},
|
||||||
|
QueryData{Query: Q(`suite:h,b:c:f="g";*`), Data: abort},
|
||||||
|
QueryData{Query: Q(`suite:a,b:c:f="g";*`), Data: skip},
|
||||||
|
QueryData{Query: Q(`suite:a,b:d:*`), Data: failure},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTree() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callA := fmt.Sprint(tree)
|
||||||
|
callB := fmt.Sprint(tree)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(callA, callB); diff != "" {
|
||||||
|
t.Errorf("Format():\n%v", diff)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue