diff --git a/cmd/tool/parser/parser.go b/cmd/tool/parser/parser.go new file mode 100644 index 00000000..544a7bb4 --- /dev/null +++ b/cmd/tool/parser/parser.go @@ -0,0 +1,44 @@ +// Package parser package is responsible for parsing the output of the `go test` +// command and returning additional info about failred test cases, such as file +// and line number of failed test. +package parser + +import ( + "bufio" + "fmt" + "regexp" + "strconv" + "strings" + + "gotest.tools/gotestsum/internal/log" +) + +// ParseFailure parses the output of the `go test` for a test failure and +// returns the file and line number of the failed test case. +func ParseFailure(output string) (file string, line int, err error) { + re, err := regexp.Compile(`^\s*([_\w]+\.go):(\d+):`) + if err != nil { + return "", 0, fmt.Errorf("failed to compile regexp: %v", err) + } + + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + outputLine := scanner.Text() + // Usually the failure would contain a line like this: + // some_test.go:42 (surrounded by white-space) + // the full path to the file is not available + matches := re.FindStringSubmatch(outputLine) + + if len(matches) == 3 { + parts := matches[1:] + file = parts[0] + line, err = strconv.Atoi(parts[1]) + if err != nil { + log.Debugf("failed to convert line number to int: %v", err) + return "", 0, nil + } + break + } + } + return file, line, scanner.Err() +} diff --git a/cmd/tool/parser/parser_test.go b/cmd/tool/parser/parser_test.go new file mode 100644 index 00000000..d75b54ed --- /dev/null +++ b/cmd/tool/parser/parser_test.go @@ -0,0 +1,20 @@ +package parser + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestParseFailure_Ok(t *testing.T) { + // given + failure := ` some_1s_test.go:42: \n` + + // when + file, line, err := ParseFailure(failure) + + // then + assert.NilError(t, err) + assert.Equal(t, file, "some_1s_test.go") + assert.Equal(t, line, 42) +} diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go index db466045..51bb72ce 100644 --- a/internal/junitxml/report.go +++ b/internal/junitxml/report.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "gotest.tools/gotestsum/cmd/tool/parser" "gotest.tools/gotestsum/internal/log" "gotest.tools/gotestsum/testjson" ) @@ -47,6 +48,8 @@ type JUnitTestCase struct { Classname string `xml:"classname,attr"` Name string `xml:"name,attr"` Time string `xml:"time,attr"` + File string `xml:"file,attr,omitempty"` + Line int `xml:"line,attr,omitempty"` SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` Failure *JUnitFailure `xml:"failure,omitempty"` } @@ -192,18 +195,22 @@ func packageTestCases(pkg *testjson.Package, formatClassname FormatFunc) []JUnit var buf bytes.Buffer pkg.WriteOutputTo(&buf, 0) //nolint:errcheck jtc := newJUnitTestCase(testjson.TestCase{Test: "TestMain"}, formatClassname) + failureOutput := buf.String() + appendFailFileLine(&jtc, failureOutput) jtc.Failure = &JUnitFailure{ Message: "Failed", - Contents: buf.String(), + Contents: failureOutput, } cases = append(cases, jtc) } for _, tc := range pkg.Failed { jtc := newJUnitTestCase(tc, formatClassname) + failureOutput := strings.Join(pkg.OutputLines(tc), "") + appendFailFileLine(&jtc, failureOutput) jtc.Failure = &JUnitFailure{ Message: "Failed", - Contents: strings.Join(pkg.OutputLines(tc), ""), + Contents: failureOutput, } cases = append(cases, jtc) } @@ -243,3 +250,12 @@ func write(out io.Writer, suites JUnitTestSuites) error { _, err = out.Write(doc) return err } + +func appendFailFileLine(jtc *JUnitTestCase, failureOutput string) { + file, line, err := parser.ParseFailure(failureOutput) + if err != nil { + log.Warnf("Failed to parse file:line from test output: %v", err) + } + jtc.File = file + jtc.Line = line +}