blob: 579b26d1f1d8f376862cbbbe0f3010a8e916fec8 [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 expectations
import (
"fmt"
"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(path, body string) (Content, error) {
// Normalize CRLF -> LF
body = strings.ReplaceAll(body, "\r\n", "\n")
// 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 fmt.Errorf("%v:%v:%v error: %v", path, 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
}