| // Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| package tap | |
| import ( | |
| "errors" | |
| "fmt" | |
| "log" | |
| "strconv" | |
| "strings" | |
| "go.fuchsia.dev/fuchsia/tools/testing/tap/tokenizer" | |
| ) | |
| // Parse parses the given input string into a Document. The input is allowed to contain | |
| // garbage lines; The parser will skip them and parse as much of the input as possible. | |
| // The only exception is that the first line of input must be a TAP version header of the | |
| // form "TAP version XXX". | |
| func Parse(input []byte) (*Document, error) { | |
| output := make(chan *Document) | |
| go parse(tokenizer.NewTokenStream(input), output) | |
| return <-output, nil | |
| } | |
| // State represents a parser state. Each state takes the current stream of input tokens | |
| // and the current Document and attempts to parse the next line of input. A state must | |
| // return the next state to use, even when an error is encountered. If nil is returned, | |
| // parsing stops. | |
| type state func(*tokenizer.TokenStream, *Document) (state, error) | |
| // Parse parses a Document from the given Token stream. The result is emitted on the | |
| // output channel. | |
| func parse(tokens *tokenizer.TokenStream, output chan<- *Document) { | |
| document := &Document{} | |
| for state := parseVersion; state != nil; { | |
| next, err := state(tokens, document) | |
| if err != nil { | |
| // Garbage lines are allowed; Treat errors as non-fatal. | |
| log.Println(err) | |
| } | |
| state = next | |
| } | |
| output <- document | |
| } | |
| // DiscardLine is a parser state that throws away every token until a newline or EOF. | |
| func discardLine(tokens *tokenizer.TokenStream, _ *Document) (state, error) { | |
| for { | |
| token := tokens.Peek() | |
| switch { | |
| case token.Type == tokenizer.TypeEOF: | |
| return nil, nil | |
| case token.Type != tokenizer.TypeNewline: | |
| tokens.Next() | |
| default: | |
| tokens.Next() // Skip the newline. | |
| return parseNextLine, nil | |
| } | |
| } | |
| } | |
| func parseNextLine(tokens *tokenizer.TokenStream, doc *Document) (state, error) { | |
| rtokens := tokens.Raw() | |
| if rtokens.Peek().Type == tokenizer.TypeEOF { | |
| return nil, nil | |
| } | |
| if rtokens.Peek().Type == tokenizer.TypeSpace { | |
| return parseYAMLBlock, nil | |
| } | |
| if rtokens.Peek().Type == tokenizer.TypeNumber { | |
| return parsePlan, nil | |
| } | |
| if rtokens.Peek().Value == "ok" || rtokens.Peek().Value == "not" { | |
| return parseTestLine, nil | |
| } | |
| return parseNextLine, unexpectedTokenError("one of 'ok', 'not' or a number", tokens.Next()) | |
| } | |
| func parseVersion(tokens *tokenizer.TokenStream, doc *Document) (state, error) { | |
| token := tokens.Next() | |
| if token.Value != "TAP" { | |
| return nil, unexpectedTokenError("'TAP'", token) | |
| } | |
| token = tokens.Next() | |
| if token.Value != "version" { | |
| return nil, unexpectedTokenError("'version'", token) | |
| } | |
| token = tokens.Next() | |
| if token.Type != tokenizer.TypeNumber { | |
| return nil, unexpectedTokenError("a version number", token) | |
| } | |
| version, err := strconv.ParseInt(token.Value, 10, 64) | |
| if err != nil { | |
| return nil, parserError(err.Error()) | |
| } | |
| doc.Version = Version(version) | |
| return parseNextLine, tokens.Eat(tokenizer.TypeNewline) | |
| } | |
| func parsePlan(tokens *tokenizer.TokenStream, doc *Document) (state, error) { | |
| if doc.Plan.Start != 0 || doc.Plan.End != 0 { | |
| return discardLine, errors.New("plan has already been parsed") | |
| } | |
| token := tokens.Peek() | |
| if token.Type != tokenizer.TypeNumber { | |
| return discardLine, unexpectedTokenError("a number", token) | |
| } | |
| start, err := strconv.ParseInt(tokens.Next().Value, 10, 64) | |
| if err != nil { | |
| return discardLine, parserError(err.Error()) | |
| } | |
| if err := tokens.Eat(tokenizer.TypeDot); err != nil { | |
| return discardLine, err | |
| } | |
| if err := tokens.Eat(tokenizer.TypeDot); err != nil { | |
| return discardLine, err | |
| } | |
| token = tokens.Peek() | |
| if token.Type != tokenizer.TypeNumber { | |
| return discardLine, unexpectedTokenError("a number > 1", token) | |
| } | |
| end, err := strconv.ParseInt(tokens.Next().Value, 10, 64) | |
| if err != nil { | |
| return discardLine, parserError(err.Error()) | |
| } | |
| doc.Plan = Plan{Start: int(start), End: int(end)} | |
| return parseNextLine, tokens.Eat(tokenizer.TypeNewline) | |
| } | |
| func parseTestLine(tokens *tokenizer.TokenStream, doc *Document) (state, error) { | |
| var testLine TestLine | |
| // Parse test status. | |
| token := tokens.Next() | |
| switch token.Value { | |
| case "not": | |
| testLine.Ok = false | |
| token = tokens.Next() | |
| if token.Value != "ok" { | |
| return discardLine, unexpectedTokenError("'ok'", token) | |
| } | |
| case "ok": | |
| testLine.Ok = true | |
| default: | |
| return discardLine, unexpectedTokenError("'ok' or 'not ok'", token) | |
| } | |
| // Parse optional test number. | |
| testLine.Count = len(doc.TestLines) + 1 | |
| if tokens.Peek().Type == tokenizer.TypeNumber { | |
| count, err := strconv.ParseInt(tokens.Next().Value, 10, 64) | |
| if err != nil { | |
| return discardLine, parserError(err.Error()) | |
| } | |
| testLine.Count = int(count) | |
| } | |
| // Parse optional description. Stop at a TypePound token which marks the start of a | |
| // diagnostic. | |
| description := tokens.Raw().ConcatUntil(tokenizer.TypePound, tokenizer.TypeNewline) | |
| testLine.Description = strings.TrimSpace(description) | |
| switch tokens.Peek().Type { | |
| case tokenizer.TypeEOF: | |
| doc.TestLines = append(doc.TestLines, testLine) | |
| return nil, nil | |
| case tokenizer.TypeNewline: | |
| doc.TestLines = append(doc.TestLines, testLine) | |
| return discardLine, nil | |
| case tokenizer.TypePound: | |
| tokens.Eat(tokenizer.TypePound) | |
| } | |
| // Parse optional directive. | |
| token = tokens.Next() | |
| switch token.Value { | |
| case "TODO": | |
| testLine.Directive = Todo | |
| case "SKIP": | |
| testLine.Directive = Skip | |
| default: | |
| return discardLine, unexpectedTokenError("a directive", token) | |
| } | |
| // Parse explanation. | |
| explanation := tokens.Raw().ConcatUntil(tokenizer.TypeNewline) | |
| testLine.Explanation = strings.TrimSpace(explanation) | |
| doc.TestLines = append(doc.TestLines, testLine) | |
| if tokens.Peek().Type == tokenizer.TypeEOF { | |
| return nil, nil | |
| } | |
| tokens.Eat(tokenizer.TypeNewline) | |
| return parseNextLine, nil | |
| } | |
| // Parses a YAML block. The block must begin as a line containing three dashes and end | |
| // with a line containing three dots. | |
| func parseYAMLBlock(tokens *tokenizer.TokenStream, doc *Document) (state, error) { | |
| rtokens := tokens.Raw() | |
| if len(doc.TestLines) == 0 { | |
| return discardLine, parserError("found YAML with no parent test line") | |
| } | |
| testLine := &doc.TestLines[len(doc.TestLines)-1] | |
| if len(testLine.YAML) > 0 { | |
| return discardLine, parserError("found YAML with no parent test line") | |
| } | |
| // Expect the header to match /\s+---/ | |
| header := rtokens.ConcatUntil(tokenizer.TypeNewline) | |
| if len(header) < 4 || !strings.HasPrefix(strings.TrimSpace(header), "---") { | |
| return discardLine, fmt.Errorf("expected line matching /^\\s+---/ but got %q", header) | |
| } | |
| if err := rtokens.Eat(tokenizer.TypeNewline); err != nil { | |
| return discardLine, unexpectedTokenError("a newline", rtokens.Peek()) | |
| } | |
| var body string | |
| for { | |
| line := rtokens.ConcatUntil(tokenizer.TypeNewline) | |
| // Expect the footer to match /\s+.../ | |
| if len(line) >= 4 && strings.HasPrefix(strings.TrimSpace(line), "...") { | |
| break | |
| } | |
| body += strings.TrimSpace(line) + "\n" | |
| if rtokens.Peek().Type == tokenizer.TypeEOF { | |
| break | |
| } | |
| rtokens.Eat(tokenizer.TypeNewline) | |
| } | |
| testLine.YAML = body | |
| return parseNextLine, nil | |
| } | |
| func unexpectedTokenError(wanted string, token tokenizer.Token) error { | |
| return parserError("got %q but wanted %s", token, wanted) | |
| } | |
| func parserError(format string, args ...interface{}) error { | |
| return fmt.Errorf("parse error: "+format, args...) | |
| } |