From 1a10b73552d79d28bb22961c163aeac05c5162b2 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 20 Apr 2022 19:10:42 +0000 Subject: [PATCH] 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 Commit-Queue: Ben Clayton --- tools/src/container/set.go | 26 +- tools/src/container/set_test.go | 17 + tools/src/cts/query/errors.go | 33 ++ tools/src/cts/query/query.go | 12 + tools/src/cts/query/tree.go | 404 +++++++++++++ tools/src/cts/query/tree_test.go | 934 +++++++++++++++++++++++++++++++ 6 files changed, 1425 insertions(+), 1 deletion(-) create mode 100644 tools/src/cts/query/errors.go create mode 100644 tools/src/cts/query/tree.go create mode 100644 tools/src/cts/query/tree_test.go diff --git a/tools/src/container/set.go b/tools/src/container/set.go index c48a649a33..689c1e331d 100644 --- a/tools/src/container/set.go +++ b/tools/src/container/set.go @@ -14,7 +14,10 @@ package container -import "sort" +import ( + "fmt" + "sort" +) // Set is a generic unordered set, which wrap's go's builtin 'map'. // 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] }) 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, "]") +} diff --git a/tools/src/container/set_test.go b/tools/src/container/set_test.go index ff1e28f6ba..2e8bf5343b 100644 --- a/tools/src/container/set_test.go +++ b/tools/src/container/set_test.go @@ -15,6 +15,7 @@ package container_test import ( + "fmt" "testing" "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, "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]`) +} diff --git a/tools/src/cts/query/errors.go b/tools/src/cts/query/errors.go new file mode 100644 index 0000000000..1ea605dd31 --- /dev/null +++ b/tools/src/cts/query/errors.go @@ -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) +} diff --git a/tools/src/cts/query/query.go b/tools/src/cts/query/query.go index 46efa710a7..27b3d6270a 100644 --- a/tools/src/cts/query/query.go +++ b/tools/src/cts/query/query.go @@ -198,6 +198,18 @@ func (q Query) CaseParameters() CaseParameters { // Append returns the query with the additional strings appended to the target func (q Query) Append(t Target, n ...string) Query { 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: return q.AppendFiles(n...) case Tests: diff --git a/tools/src/cts/query/tree.go b/tools/src/cts/query/tree.go new file mode 100644 index 0000000000..b52d993e1e --- /dev/null +++ b/tools/src/cts/query/tree.go @@ -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 +} diff --git a/tools/src/cts/query/tree_test.go b/tools/src/cts/query/tree_test.go new file mode 100644 index 0000000000..80af4756a2 --- /dev/null +++ b/tools/src/cts/query/tree_test.go @@ -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) + } +}