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:
Ben Clayton 2022-04-20 19:10:42 +00:00
parent 4c9b72b4fa
commit 1a10b73552
6 changed files with 1425 additions and 1 deletions

View File

@ -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, "]")
}

View File

@ -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]`)
}

View File

@ -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)
}

View File

@ -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:

404
tools/src/cts/query/tree.go Normal file
View File

@ -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
}

View File

@ -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)
}
}