tools: Add src/cts/results.List.MinimalVariantTags
MinimalVariantTags accepts a list of tag-sets (e.g GPU tags, OS tags, etc),
and returns an optimized list of variants, folding together variants that
have identical result query-to-status mappings, and removing redundant tags.
Bug: dawn:1342
Change-Id: I759c82e9a0631a9d321d376656e5a2dbbf5f5507
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/87643
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/tools/src/container/set.go b/tools/src/container/set.go
index 689c1e3..4adde5c 100644
--- a/tools/src/container/set.go
+++ b/tools/src/container/set.go
@@ -71,7 +71,7 @@
return found
}
-// Contains returns true if the set contains all the items in o
+// ContainsAll returns true if the set contains all the items in o
func (s Set[T]) ContainsAll(o Set[T]) bool {
for item := range o {
if !s.Contains(item) {
@@ -81,6 +81,16 @@
return true
}
+// ContainsAny returns true if the set contains any of the items in o
+func (s Set[T]) ContainsAny(o Set[T]) bool {
+ for item := range o {
+ if s.Contains(item) {
+ return true
+ }
+ }
+ return false
+}
+
// Intersection returns true if the set contains all the items in o
func (s Set[T]) Intersection(o Set[T]) Set[T] {
out := NewSet[T]()
diff --git a/tools/src/container/set_test.go b/tools/src/container/set_test.go
index 2e8bf53..35ebd4f 100644
--- a/tools/src/container/set_test.go
+++ b/tools/src/container/set_test.go
@@ -123,6 +123,39 @@
expectEq(t, `s.ContainsAll("c", "a", "b")`, s.ContainsAll(S("c", "a", "b")), true)
}
+func TestSetContainsAny(t *testing.T) {
+ S := container.NewSet[string]
+
+ s := container.NewSet[string]()
+ s.Add("c")
+ expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), false)
+ expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), false)
+ expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), true)
+ expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), false)
+ expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
+ expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
+ expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
+
+ s.Add("a")
+ expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), true)
+ expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), false)
+ expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), true)
+ expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), true)
+ expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
+ expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
+ expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
+
+ s.Remove("c")
+ s.Add("b")
+ expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), true)
+ expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), true)
+ expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), false)
+ expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), true)
+ expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
+ expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
+ expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
+}
+
func TestSetIntersection(t *testing.T) {
a := container.NewSet(1, 3, 4, 6)
b := container.NewSet(2, 3, 4, 5)
diff --git a/tools/src/cts/result/mvt.go b/tools/src/cts/result/mvt.go
new file mode 100644
index 0000000..2318821
--- /dev/null
+++ b/tools/src/cts/result/mvt.go
@@ -0,0 +1,146 @@
+// 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 result
+
+import (
+ "sort"
+
+ "dawn.googlesource.com/dawn/tools/src/cts/query"
+)
+
+// MinimalVariantTags accepts a list of tag-sets (e.g GPU tags, OS tags, etc),
+// and returns an optimized list of variants, folding together variants that
+// have identical result query-to-status mappings, and removing redundant tags.
+//
+// MinimalVariantTags will attempt to remove variant tags starting with the
+// first set of tags in tagSets, then second, and so on. If a tag-set cannot
+// be removed, then the tags of the set are left alone, and the algorithm will
+// progress to the next tag-set.
+//
+// MinimalVariantTags assumes that there are no duplicate results (same query,
+// same tags) in l.
+func (l List) MinimalVariantTags(tagSets []Tags) []Variant {
+ type VariantData struct {
+ // The variant tags
+ tags Variant
+ // The query -> status for all results in l that have this variant's
+ // tags.
+ queryToStatus map[query.Query]Status
+ }
+
+ variants := []VariantData{}
+
+ // Build the initial list of variants from l.
+ // Bin result [query -> status] to the variant.
+ {
+ variantIndices := map[string]int{}
+ for _, r := range l {
+ key := TagsToString(r.Tags)
+ if idx, found := variantIndices[key]; !found {
+ variantIndices[key] = len(variants)
+ variants = append(variants, VariantData{
+ tags: Variant(r.Tags.Clone()),
+ queryToStatus: map[query.Query]Status{
+ r.Query: r.Status,
+ },
+ })
+ } else {
+ variants[idx].queryToStatus[r.Query] = r.Status
+ }
+ }
+ }
+
+ // canReduce checks that the variant would match the same results if the
+ // tags were reduced to 'tags'. Returns true if the variant's tags could
+ // be reduced, otherwise false.
+ canReduce := func(variant VariantData, tags Tags) bool {
+ for _, r := range l.FilterByTags(tags) {
+ existing, found := variant.queryToStatus[r.Query]
+ if !found {
+ // Removing the tag has expanded the set of queries.
+ return false
+ }
+ if existing != r.Status {
+ // Removing the tag has resulted in two queries with different
+ // results.
+ return false
+ }
+ }
+ return true
+ }
+
+ // tryToRemoveTags will remove all the tags in 'tags' from all variants
+ // iff doing so does not affect the set of results filtered by each variant.
+ // If it was possible to remove the tags, then variants that now have the
+ // same tags may be folded together, reducing the total number of variants.
+ tryToRemoveTags := func(tags Tags) {
+ newVariants := make([]VariantData, 0, len(variants))
+
+ for _, v := range variants {
+ // Does the variant even contain these tags?
+ if !v.tags.ContainsAny(tags) {
+ // Nope. Skip the canReduce() call, and keep the variant.
+ newVariants = append(newVariants, v)
+ continue
+ }
+
+ // Build the new set of tags with 'tags' removed.
+ newTags := v.tags.Clone()
+ newTags.RemoveAll(tags)
+
+ // Check wether removal of these tags affected the outcome.
+ if !canReduce(v, newTags) {
+ // Removing these tags resulted in differences.
+ return // Abort
+ }
+ newVariants = append(newVariants, VariantData{newTags, v.queryToStatus})
+ }
+
+ // Remove variants that are now subsets of others.
+ // Start by sorting the variants by number of tags.
+ // This ensures that the variants with fewer tags (fewer constraints)
+ // come first.
+ sort.Slice(newVariants, func(i, j int) bool {
+ return len(newVariants[i].tags) < len(newVariants[j].tags)
+ })
+
+ // Now check each variant's tags against the previous variant tags.
+ // As we've sorted, we know that supersets (fewer-tags) come before
+ // subsets (more-tags).
+ variants = []VariantData{}
+
+ nextVariant:
+ for i, v1 := range newVariants { // for variants 0..N
+ for _, v2 := range newVariants[:i] { // for variants 0..i
+ if v1.tags.ContainsAll(v2.tags) {
+ continue nextVariant // v1 is a subset of v2. Omit.
+ }
+ }
+ variants = append(variants, v1)
+ }
+ }
+
+ // Attempt to remove the tag sets from the variants, one by one.
+ for _, tags := range tagSets {
+ tryToRemoveTags(tags)
+ }
+
+ // Return the final set of unique variants
+ out := make([]Variant, len(variants))
+ for i, v := range variants {
+ out[i] = v.tags
+ }
+ return out
+}
diff --git a/tools/src/cts/result/mvt_test.go b/tools/src/cts/result/mvt_test.go
new file mode 100644
index 0000000..bfa67a7
--- /dev/null
+++ b/tools/src/cts/result/mvt_test.go
@@ -0,0 +1,117 @@
+// 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 result_test
+
+import (
+ "fmt"
+ "testing"
+
+ "dawn.googlesource.com/dawn/tools/src/cts/result"
+ "dawn.googlesource.com/dawn/tools/src/utils"
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestMinimalVariantTags(t *testing.T) {
+ type Test struct {
+ location string
+ results result.List
+ expect []result.Variant
+ }
+ for _, test := range []Test{
+ { //////////////////////////////////////////////////////////////////////
+ location: utils.ThisLine(),
+ results: result.List{},
+ expect: []result.Variant{},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Single variant, that can be entirely optimized away
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c2"), Status: result.Pass},
+ },
+ expect: []result.Variant{T()},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Multiple variants on the same query.
+ // Can also be entirely optimized away.
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c2"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b2", "c0"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a2", "b1", "c0"), Status: result.Pass},
+ },
+ expect: []result.Variant{T()},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Two variants where the 1st and 2nd tag-sets are redundant.
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
+ },
+ expect: []result.Variant{T("c0"), T("c1")},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Two variants where the 1st and 3rd tag-sets are redundant.
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c1"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c0"), Status: result.Failure},
+ },
+ expect: []result.Variant{T("b0"), T("b1")},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Two variants where the 2nd and 3rd tag-sets are redundant.
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
+ {Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c1"), Status: result.Pass},
+ {Query: Q("a:b,c:d,*"), Tags: T("a1", "b0", "c0"), Status: result.Failure},
+ },
+ expect: []result.Variant{T("a0"), T("a1")},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Check that variants aren't optimized to expand the set of results
+ // they target, even if results are uniform
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d0,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
+ {Query: Q("a:b,c:d1,*"), Tags: T("a1", "b1", "c1"), Status: result.Pass},
+ },
+ expect: []result.Variant{T("c0"), T("c1")},
+ }, { ///////////////////////////////////////////////////////////////////
+ // Exercise the optimizations to skip checks on tag removals that
+ // aren't found in all variants
+ location: utils.ThisLine(),
+ results: result.List{
+ {Query: Q("a:b,c:d0,*"), Tags: T("a0"), Status: result.Pass},
+ {Query: Q("a:b,c:d1,*"), Tags: T("b0"), Status: result.Pass},
+ {Query: Q("a:b,c:d2,*"), Tags: T("c0"), Status: result.Pass},
+ },
+ expect: []result.Variant{T("a0"), T("b0"), T("c0")},
+ },
+ } {
+ preReduce := fmt.Sprint(test.results)
+ got := test.results.MinimalVariantTags([]result.Tags{
+ T("a0", "a1", "a2"),
+ T("b0", "b1", "b2"),
+ T("c0", "c1", "c2"),
+ })
+ postReduce := fmt.Sprint(test.results)
+ if diff := cmp.Diff(got, test.expect); diff != "" {
+ t.Errorf("%v MinimalVariantTags() diff:\n%v", test.location, diff)
+ }
+ if diff := cmp.Diff(preReduce, postReduce); diff != "" {
+ t.Errorf("%v MinimalVariantTags() modified original list:\n%v", test.location, diff)
+ }
+ }
+}
diff --git a/tools/src/cts/result/result.go b/tools/src/cts/result/result.go
index 99d14d2..0ec6095 100644
--- a/tools/src/cts/result/result.go
+++ b/tools/src/cts/result/result.go
@@ -97,9 +97,13 @@
// List is a list of results
type List []Result
-// Returns the list of unique tags across all results.
-func (l List) UniqueTags() []Tags {
- tags := container.NewMap[string, Tags]()
+// Variant is a collection of tags that uniquely identify a test
+// configuration (e.g the combination of OS, GPU, validation-modes, etc).
+type Variant = Tags
+
+// Variants returns the list of unique tags (variants) across all results.
+func (l List) Variants() []Variant {
+ tags := container.NewMap[string, Variant]()
for _, r := range l {
tags.Add(TagsToString(r.Tags), r.Tags)
}
diff --git a/tools/src/cts/result/result_test.go b/tools/src/cts/result/result_test.go
index bc87bc6..7eae418 100644
--- a/tools/src/cts/result/result_test.go
+++ b/tools/src/cts/result/result_test.go
@@ -91,7 +91,7 @@
}
}
-func TestUniqueTags(t *testing.T) {
+func TestVariants(t *testing.T) {
type Test struct {
results result.List
expect []result.Tags
@@ -198,7 +198,7 @@
},
},
} {
- got := test.results.UniqueTags()
+ got := test.results.Variants()
if diff := cmp.Diff(got, test.expect); diff != "" {
t.Errorf("Results:\n%v\nUniqueTags() was not as expected:\n%v", test.results, diff)
}