blob: 1b20a70416efc31a5fc89bb1e7a49698529281ac [file] [log] [blame]
// 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 provides helpers for parsing and mutating WebGPU CTS queries.
//
// The full query syntax is described at:
// https://github.com/gpuweb/cts/blob/main/docs/terms.md#queries
//
// Note that this package supports a superset of the official CTS query syntax,
// as this package permits parsing and printing of queries that do not end in a
// wildcard, whereas the CTS requires that all queries end in wildcards unless
// they identify a specific test.
// For example, the following queries are considered valid by this package, but
// would be rejected by the CTS:
// `suite`, `suite:file`, `suite:file,file`, `suite:file,file:test`.
//
// This relaxation is intentional as the Query type is used for constructing and
// reducing query trees, and always requiring a wildcard adds unnecessary
// complexity.
package query
import (
"fmt"
"strings"
)
// Query represents a WebGPU test query
// Example queries:
//
// 'suite'
// 'suite:*'
// 'suite:file'
// 'suite:file,*'
// 'suite:file,file'
// 'suite:file,file,*'
// 'suite:file,file,file:test'
// 'suite:file,file,file:test:*'
// 'suite:file,file,file:test,test:case;*'
type Query struct {
Suite string
Files string
Tests string
Cases string
}
// Target is the target of a query, either a Suite, File, Test or Case.
type Target int
// Enumerators of Target
const (
// The query targets a suite
Suite Target = iota
// The query targets one or more files
Files
// The query targets one or more tests
Tests
// The query targets one or more test cases
Cases
TargetCount
)
// Format writes the Target to the fmt.State
func (l Target) Format(f fmt.State, verb rune) {
switch l {
case Suite:
fmt.Fprint(f, "suite")
case Files:
fmt.Fprint(f, "files")
case Tests:
fmt.Fprint(f, "tests")
case Cases:
fmt.Fprint(f, "cases")
default:
fmt.Fprint(f, "<invalid>")
}
}
// Delimiter constants used by the query format
const (
TargetDelimiter = ":"
FileDelimiter = ","
TestDelimiter = ","
CaseDelimiter = ";"
)
// Parse parses a query string
func Parse(s string) Query {
parts := strings.Split(s, TargetDelimiter)
q := Query{}
switch len(parts) {
default:
q.Cases = strings.Join(parts[3:], TargetDelimiter)
fallthrough
case 3:
q.Tests = parts[2]
fallthrough
case 2:
q.Files = parts[1]
fallthrough
case 1:
q.Suite = parts[0]
}
return q
}
// AppendFiles returns a new query with the strings appended to the 'files'
func (q Query) AppendFiles(f ...string) Query {
if len(f) > 0 {
if q.Files == "" {
q.Files = strings.Join(f, FileDelimiter)
} else {
q.Files = q.Files + FileDelimiter + strings.Join(f, FileDelimiter)
}
}
return q
}
// SplitFiles returns the separated 'files' part of the query
func (q Query) SplitFiles() []string {
if q.Files != "" {
return strings.Split(q.Files, FileDelimiter)
}
return nil
}
// AppendTests returns a new query with the strings appended to the 'tests'
func (q Query) AppendTests(t ...string) Query {
if len(t) > 0 {
if q.Tests == "" {
q.Tests = strings.Join(t, TestDelimiter)
} else {
q.Tests = q.Tests + TestDelimiter + strings.Join(t, TestDelimiter)
}
}
return q
}
// SplitTests returns the separated 'tests' part of the query
func (q Query) SplitTests() []string {
if q.Tests != "" {
return strings.Split(q.Tests, TestDelimiter)
}
return nil
}
// AppendCases returns a new query with the strings appended to the 'cases'
func (q Query) AppendCases(c ...string) Query {
if len(c) > 0 {
if q.Cases == "" {
q.Cases = strings.Join(c, CaseDelimiter)
} else {
q.Cases = q.Cases + CaseDelimiter + strings.Join(c, CaseDelimiter)
}
}
return q
}
// SplitCases returns the separated 'cases' part of the query
func (q Query) SplitCases() []string {
if q.Cases != "" {
return strings.Split(q.Cases, CaseDelimiter)
}
return nil
}
// Case parameters is a map of parameter name to parameter value
type CaseParameters map[string]string
// CaseParameters returns all the case parameters of the query
func (q Query) CaseParameters() CaseParameters {
if q.Cases != "" {
out := CaseParameters{}
for _, c := range strings.Split(q.Cases, CaseDelimiter) {
idx := strings.IndexRune(c, '=')
if idx < 0 {
out[c] = ""
} else {
k, v := c[:idx], c[idx+1:]
out[k] = v
}
}
return out
}
return nil
}
// 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:
return q.AppendTests(n...)
case Cases:
return q.AppendCases(n...)
}
panic("invalid target")
}
// Target returns the target of the query
func (q Query) Target() Target {
if q.Files != "" {
if q.Tests != "" {
if q.Cases != "" {
return Cases
}
return Tests
}
return Files
}
return Suite
}
// IsWildcard returns true if the query ends with a wildcard
func (q Query) IsWildcard() bool {
switch q.Target() {
case Suite:
return q.Suite == "*"
case Files:
return strings.HasSuffix(q.Files, "*")
case Tests:
return strings.HasSuffix(q.Tests, "*")
case Cases:
return strings.HasSuffix(q.Cases, "*")
}
panic("invalid target")
}
// String returns the query formatted as a string
func (q Query) String() string {
sb := strings.Builder{}
sb.WriteString(q.Suite)
if q.Files != "" {
sb.WriteString(TargetDelimiter)
sb.WriteString(q.Files)
if q.Tests != "" {
sb.WriteString(TargetDelimiter)
sb.WriteString(q.Tests)
if q.Cases != "" {
sb.WriteString(TargetDelimiter)
sb.WriteString(q.Cases)
}
}
}
return sb.String()
}
// Compare compares the relative order of q and o, returning:
//
// -1 if q should come before o
// 1 if q should come after o
// 0 if q and o are identical
func (q Query) Compare(o Query) int {
for _, cmp := range []struct{ a, b string }{
{q.Suite, o.Suite},
{q.Files, o.Files},
{q.Tests, o.Tests},
{q.Cases, o.Cases},
} {
if cmp.a < cmp.b {
return -1
}
if cmp.a > cmp.b {
return 1
}
}
return 0
}
// Contains returns true if q is a superset of o
func (q Query) Contains(o Query) bool {
if q.Suite != o.Suite {
return false
}
{
a, b := q.SplitFiles(), o.SplitFiles()
for i, f := range a {
if f == "*" {
return true
}
if i >= len(b) || b[i] != f {
return false
}
}
if len(a) < len(b) {
return false
}
}
{
a, b := q.SplitTests(), o.SplitTests()
for i, f := range a {
if f == "*" {
return true
}
if i >= len(b) || b[i] != f {
return false
}
}
if len(a) < len(b) {
return false
}
}
{
a, b := q.CaseParameters(), o.CaseParameters()
for key, av := range a {
if bv, found := b[key]; found && av != bv {
return false
}
}
}
return true
}
// Callback function for Query.Walk()
//
// q is the query for the current segment.
// t is the target of the query q.
// n is the name of the new segment.
type WalkCallback func(q Query, t Target, n string) error
// Walk calls 'f' for each suite, file, test segment, and calls f once for all
// cases. If f returns an error then walking is immediately terminated and the
// error is returned.
func (q Query) Walk(f WalkCallback) error {
p := Query{Suite: q.Suite}
if err := f(p, Suite, q.Suite); err != nil {
return err
}
for _, file := range q.SplitFiles() {
p = p.AppendFiles(file)
if err := f(p, Files, file); err != nil {
return err
}
}
for _, test := range q.SplitTests() {
p = p.AppendTests(test)
if err := f(p, Tests, test); err != nil {
return err
}
}
if q.Cases != "" {
if err := f(q, Cases, q.Cases); err != nil {
return err
}
}
return nil
}