package stack
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/openshift/origin/tools/junitreport/pkg/api"
"github.com/openshift/origin/tools/junitreport/pkg/builder"
"github.com/openshift/origin/tools/junitreport/pkg/parser"
)
// NewParser returns a new parser that's capable of parsing Go unit test output
func NewParser(builder builder.TestSuitesBuilder, testParser TestDataParser, suiteParser TestSuiteDataParser, stream bool) parser.TestOutputParser {
return &testOutputParser{
builder: builder,
testParser: testParser,
suiteParser: suiteParser,
stream: stream,
}
}
// testOutputParser uses a stack to parse test output. Critical assumptions that this parser makes are:
// 1 - packages may be nested but tests may not
// 2 - no package declarations will occur within the boundaries of a test
// 3 - all tests and packages are fully bounded by a start and result line
// 4 - if a package or test declaration occurs after the start of a package but before its result,
// the sub-package's or member test's result line will occur before that of the parent package
// i.e. any test or package overlap will necessarily mean that one package's lines are a superset
// of any lines of tests or other packages overlapping with it
// 5 - any text in the input file that doesn't match the parser regex is necessarily the output of the
// current test being built
type testOutputParser struct {
builder builder.TestSuitesBuilder
testParser TestDataParser
suiteParser TestSuiteDataParser
stream bool
}
// Parse parses output syntax of a specific class, the assumptions of which are outlined in the struct definition.
// The specific boundary markers and metadata encodings are free to vary as long as regex can be build to extract them
// from test output.
func (p *testOutputParser) Parse(input *bufio.Scanner) (*api.TestSuites, error) {
inProgress := NewTestSuiteStack()
var currentTest *api.TestCase
var currentResult api.TestResult
var currentOutput []string
var currentMessage string
for input.Scan() {
line := input.Text()
isTestOutput := true
if p.testParser.MarksBeginning(line) {
currentTest = &api.TestCase{}
currentResult = api.TestResultFail
currentOutput = []string{}
currentMessage = ""
}
if name, contained := p.testParser.ExtractName(line); contained {
currentTest.Name = name
}
if result, contained := p.testParser.ExtractResult(line); contained {
currentResult = result
}
if duration, contained := p.testParser.ExtractDuration(line); contained {
if err := currentTest.SetDuration(duration); err != nil {
return nil, err
}
}
if message, contained := p.testParser.ExtractMessage(line); contained {
currentMessage = message
}
if p.testParser.MarksCompletion(line) {
currentOutput = append(currentOutput, line)
// if we have finished the current test case, we finalize our current test, add it to the package
// at the head of our in progress package stack, and clear our current test record.
switch currentResult {
case api.TestResultSkip:
currentTest.MarkSkipped(currentMessage)
case api.TestResultFail:
output := strings.Join(currentOutput, "\n")
currentTest.MarkFailed(currentMessage, output)
}
if inProgress.Peek() == nil {
return nil, fmt.Errorf("found test case %q outside of a test suite", currentTest.Name)
}
inProgress.Peek().AddTestCase(currentTest)
currentTest = &api.TestCase{}
}
if p.suiteParser.MarksBeginning(line) {
// if we encounter the beginning of a suite, we create a new suite to be considered and
// add it to the head of our in progress package stack
inProgress.Push(&api.TestSuite{})
isTestOutput = false
}
if name, contained := p.suiteParser.ExtractName(line); contained {
inProgress.Peek().Name = name
isTestOutput = false
}
if properties, contained := p.suiteParser.ExtractProperties(line); contained {
for propertyName := range properties {
inProgress.Peek().AddProperty(propertyName, properties[propertyName])
}
isTestOutput = false
}
if p.suiteParser.MarksCompletion(line) {
if p.stream {
fmt.Fprintln(os.Stdout, line)
}
// if we encounter the end of a suite, we remove the suite at the head of the in progress stack
p.builder.AddSuite(inProgress.Pop())
isTestOutput = false
}
// we want to associate every line other than those directly involved with test suites as output of a test case
if isTestOutput {
currentOutput = append(currentOutput, line)
}
}
return p.builder.Build(), nil
}