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