299 lines
7.4 KiB
Go
299 lines
7.4 KiB
Go
// 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 expectations
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
|
)
|
|
|
|
const (
|
|
tagHeaderStart = `BEGIN TAG HEADER`
|
|
tagHeaderEnd = `END TAG HEADER`
|
|
)
|
|
|
|
// Parse parses an expectations file, returning the Content
|
|
func Parse(body string) (Content, error) {
|
|
// LineType is an enumerator classifying the 'type' of the line.
|
|
type LineType int
|
|
const (
|
|
comment LineType = iota // The line starts with the '#'
|
|
expectation // The line declares an expectation
|
|
blank // The line is blank
|
|
)
|
|
|
|
// classifyLine returns the LineType for the given line
|
|
classifyLine := func(line string) LineType {
|
|
line = strings.TrimSpace(line)
|
|
switch {
|
|
case line == "":
|
|
return blank
|
|
case strings.HasPrefix(line, "#"):
|
|
return comment
|
|
default:
|
|
return expectation
|
|
}
|
|
}
|
|
|
|
content := Content{} // The output content
|
|
|
|
var pending Chunk // The current Chunk being parsed
|
|
|
|
// flush completes the current chunk, appending it to 'content'
|
|
flush := func() {
|
|
parseTags(&content.Tags, pending.Comments)
|
|
content.Chunks = append(content.Chunks, pending)
|
|
pending = Chunk{}
|
|
}
|
|
|
|
lastLineType := blank // The type of the last parsed line
|
|
for i, l := range strings.Split(body, "\n") { // For each line...
|
|
lineIdx := i + 1 // line index
|
|
lineType := classifyLine(l)
|
|
|
|
// Compare the new line type to the last.
|
|
// Flush the pending chunk if needed.
|
|
if i > 0 {
|
|
switch {
|
|
case
|
|
lastLineType == blank && lineType != blank, // blank -> !blank
|
|
lastLineType != blank && lineType == blank, // !blank -> blank
|
|
lastLineType == expectation && lineType != expectation: // expectation -> comment
|
|
flush()
|
|
}
|
|
}
|
|
|
|
lastLineType = lineType
|
|
|
|
// Handle blank lines and comments.
|
|
switch lineType {
|
|
case blank:
|
|
continue
|
|
case comment:
|
|
pending.Comments = append(pending.Comments, l)
|
|
continue
|
|
}
|
|
|
|
// Below this point, we're dealing with an expectation
|
|
|
|
// Split the line by whitespace to form a list of tokens
|
|
type Token struct {
|
|
str string
|
|
start, end int // line offsets (0-based)
|
|
}
|
|
tokens := []Token{}
|
|
if len(l) > 0 { // Parse the tokens
|
|
inToken, s := false, 0
|
|
for i, c := range l {
|
|
if c == ' ' {
|
|
if inToken {
|
|
tokens = append(tokens, Token{l[s:i], s, i})
|
|
inToken = false
|
|
}
|
|
} else if !inToken {
|
|
s = i
|
|
inToken = true
|
|
}
|
|
}
|
|
if inToken {
|
|
tokens = append(tokens, Token{l[s:], s, len(l)})
|
|
}
|
|
}
|
|
|
|
// syntaxErr is a helper for returning a SyntaxError with the current
|
|
// line and column index.
|
|
syntaxErr := func(at Token, msg string) error {
|
|
columnIdx := at.start + 1
|
|
if columnIdx == 1 {
|
|
columnIdx = len(l) + 1
|
|
}
|
|
return Diagnostic{Error, lineIdx, columnIdx, msg}
|
|
}
|
|
|
|
// peek returns the next token without consuming it.
|
|
// If there are no more tokens then an empty Token is returned.
|
|
peek := func() Token {
|
|
if len(tokens) > 0 {
|
|
return tokens[0]
|
|
}
|
|
return Token{}
|
|
}
|
|
|
|
// next returns the next token, consuming it and incrementing the
|
|
// column index.
|
|
// If there are no more tokens then an empty Token is returned.
|
|
next := func() Token {
|
|
if len(tokens) > 0 {
|
|
tok := tokens[0]
|
|
tokens = tokens[1:]
|
|
return tok
|
|
}
|
|
return Token{}
|
|
}
|
|
|
|
match := func(str string) bool {
|
|
if peek().str != str {
|
|
return false
|
|
}
|
|
next()
|
|
return true
|
|
}
|
|
|
|
// tags parses a [ tag ] block.
|
|
tags := func(use string) (result.Tags, error) {
|
|
if !match("[") {
|
|
return result.Tags{}, nil
|
|
}
|
|
out := result.NewTags()
|
|
for {
|
|
t := next()
|
|
switch t.str {
|
|
case "]":
|
|
return out, nil
|
|
case "":
|
|
return result.Tags{}, syntaxErr(t, "expected ']' for "+use)
|
|
default:
|
|
out.Add(t.str)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the optional bug
|
|
var bug string
|
|
if strings.HasPrefix(peek().str, "crbug.com") {
|
|
bug = next().str
|
|
}
|
|
|
|
// Parse the optional test tags
|
|
testTags, err := tags("tags")
|
|
if err != nil {
|
|
return Content{}, err
|
|
}
|
|
|
|
// Parse the query
|
|
if t := peek(); t.str == "" || t.str[0] == '#' || t.str[0] == '[' {
|
|
return Content{}, syntaxErr(t, "expected test query")
|
|
}
|
|
query := next().str
|
|
|
|
// Parse the expected status
|
|
if t := peek(); !strings.HasPrefix(t.str, "[") {
|
|
return Content{}, syntaxErr(t, "expected status")
|
|
}
|
|
status, err := tags("status")
|
|
if err != nil {
|
|
return Content{}, err
|
|
}
|
|
|
|
// Parse any optional trailing comment
|
|
comment := ""
|
|
if t := peek(); strings.HasPrefix(t.str, "#") {
|
|
comment = l[t.start:]
|
|
}
|
|
|
|
// Append the expectation to the list.
|
|
pending.Expectations = append(pending.Expectations, Expectation{
|
|
Line: lineIdx,
|
|
Bug: bug,
|
|
Tags: testTags,
|
|
Query: query,
|
|
Status: status.List(),
|
|
Comment: comment,
|
|
})
|
|
}
|
|
|
|
if lastLineType != blank {
|
|
flush()
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
// parseTags parses the tag information found between tagHeaderStart and
|
|
// tagHeaderEnd comments.
|
|
func parseTags(tags *Tags, lines []string) {
|
|
// Flags for whether we're currently parsing a TAG HEADER and whether we're
|
|
// also within a tag-set.
|
|
inTagsHeader, inTagSet := false, false
|
|
tagSet := TagSet{} // The currently parsed tag-set
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(line), "#"))
|
|
if strings.Contains(line, tagHeaderStart) {
|
|
if tags.ByName == nil {
|
|
*tags = Tags{
|
|
ByName: map[string]TagSetAndPriority{},
|
|
Sets: []TagSet{},
|
|
}
|
|
}
|
|
inTagsHeader = true
|
|
continue
|
|
}
|
|
if strings.Contains(line, tagHeaderEnd) {
|
|
return // Reached the end of the TAG HEADER
|
|
}
|
|
if !inTagsHeader {
|
|
continue // Still looking for a tagHeaderStart
|
|
}
|
|
|
|
// Below this point, we're in a TAG HEADER.
|
|
tokens := removeEmpty(strings.Split(line, " "))
|
|
for len(tokens) > 0 {
|
|
if inTagSet {
|
|
// Parsing tags in a tag-set (between the '[' and ']')
|
|
if tokens[0] == "]" {
|
|
// End of the tag-set.
|
|
tags.Sets = append(tags.Sets, tagSet)
|
|
inTagSet = false
|
|
break
|
|
} else {
|
|
// Still inside the tag-set. Consume the tag.
|
|
tag := tokens[0]
|
|
tags.ByName[tag] = TagSetAndPriority{
|
|
Set: tagSet.Name,
|
|
Priority: len(tagSet.Tags),
|
|
}
|
|
tagSet.Tags.Add(tag)
|
|
}
|
|
tokens = tokens[1:]
|
|
} else {
|
|
// Outside of tag-set. Scan for 'tags: ['
|
|
if len(tokens) > 2 && tokens[0] == "tags:" && tokens[1] == "[" {
|
|
inTagSet = true
|
|
tagSet.Tags = result.NewTags()
|
|
tokens = tokens[2:] // Skip 'tags:' and '['
|
|
} else {
|
|
// Tag set names are on their own line.
|
|
// Remember the content of the line, in case the next line
|
|
// starts a tag-set.
|
|
tagSet.Name = strings.Join(tokens, " ")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// removeEmpty returns the list of strings with all empty strings removed.
|
|
func removeEmpty(in []string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for _, s := range in {
|
|
if s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|