From cf4c6aa3feb1c5c6d689833b4386b3ad000eb686 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Thu, 27 Feb 2025 12:51:18 +0000 Subject: [PATCH 1/4] Add the file/line to the Junit XML output --- cmd/tool/parser/parser.go | 55 ++++++++++++++++++++++++++++++++++ cmd/tool/parser/parser_test.go | 20 +++++++++++++ internal/junitxml/report.go | 20 +++++++++++-- 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 cmd/tool/parser/parser.go create mode 100644 cmd/tool/parser/parser_test.go diff --git a/cmd/tool/parser/parser.go b/cmd/tool/parser/parser.go new file mode 100644 index 00000000..ab878c54 --- /dev/null +++ b/cmd/tool/parser/parser.go @@ -0,0 +1,55 @@ +// 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" + "os" + "path/filepath" + "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) { + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + outputLine := scanner.Text() + // Usually the failure would contain a line like this: + // Error Trace: /Users/user/proje/path/to/package/some_test.go:42 + // where the full path to the file is in the same line as "Error Trace:" + if strings.Contains(outputLine, "Error Trace") { + absolutePath := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(outputLine), "Error Trace:")) + currentPath, err := os.Getwd() + if err != nil { + return "", 0, fmt.Errorf("failed getting current path: %v", err) + } + + relPathRaw, err := filepath.Rel(currentPath, absolutePath) + if err != nil { + log.Debugf("failed to get relative path from trace: %v", err) + return "", 0, err + } + // in case we're deeply nested, remove any repeating dots + relPath := filepath.Clean(strings.TrimLeft(relPathRaw, "./")) + parts := strings.Split(relPath, ":") + if len(parts) != 2 { + log.Debugf("failed to split the trace path: %s", relPath) + return "", 0, nil + } + 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, err +} diff --git a/cmd/tool/parser/parser_test.go b/cmd/tool/parser/parser_test.go new file mode 100644 index 00000000..7ee5cab7 --- /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 := `Error Trace: /project/path/to/package/some_test.go:42` + + // when + file, line, err := ParseFailure(failure) + + // then + assert.NilError(t, err) + assert.Equal(t, file, "project/path/to/package/some_test.go") + assert.Equal(t, line, 42) +} diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go index db466045..09a60ce3 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 failure output: %v", err) + } + jtc.File = file + jtc.Line = line +} From 937e59c6c6e49792492ef7a78aa84188b95e5fa6 Mon Sep 17 00:00:00 2001 From: Deel IT Release Manager Date: Sat, 7 Jun 2025 11:35:43 +0100 Subject: [PATCH 2/4] fixup! Add the file/line to the Junit XML output --- internal/junitxml/report.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go index 09a60ce3..51bb72ce 100644 --- a/internal/junitxml/report.go +++ b/internal/junitxml/report.go @@ -254,7 +254,7 @@ func write(out io.Writer, suites JUnitTestSuites) error { func appendFailFileLine(jtc *JUnitTestCase, failureOutput string) { file, line, err := parser.ParseFailure(failureOutput) if err != nil { - log.Warnf("Failed to parse failure output: %v", err) + log.Warnf("Failed to parse file:line from test output: %v", err) } jtc.File = file jtc.Line = line From aed39040102934612815f4d8f26b88de2f99e68b Mon Sep 17 00:00:00 2001 From: Deel IT Release Manager Date: Sat, 7 Jun 2025 11:37:35 +0100 Subject: [PATCH 3/4] fixup! Add the file/line to the Junit XML output --- cmd/tool/parser/parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tool/parser/parser.go b/cmd/tool/parser/parser.go index ab878c54..1c915498 100644 --- a/cmd/tool/parser/parser.go +++ b/cmd/tool/parser/parser.go @@ -51,5 +51,5 @@ func ParseFailure(output string) (file string, line int, err error) { break } } - return file, line, err + return file, line, scanner.Err() } From c3a3dad355b76bde41e9f42519525acf96d8115a Mon Sep 17 00:00:00 2001 From: Deel IT Release Manager Date: Sat, 7 Jun 2025 12:18:52 +0100 Subject: [PATCH 4/4] do not rely on 'Error Trace' match filename.go:1 instead as per discussion & comments: https://github.com/gotestyourself/gotestsum/pull/470#pullrequestreview-2680259477 --- cmd/tool/parser/parser.go | 33 +++++++++++---------------------- cmd/tool/parser/parser_test.go | 4 ++-- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/cmd/tool/parser/parser.go b/cmd/tool/parser/parser.go index 1c915498..544a7bb4 100644 --- a/cmd/tool/parser/parser.go +++ b/cmd/tool/parser/parser.go @@ -6,8 +6,7 @@ package parser import ( "bufio" "fmt" - "os" - "path/filepath" + "regexp" "strconv" "strings" @@ -17,31 +16,21 @@ import ( // 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: - // Error Trace: /Users/user/proje/path/to/package/some_test.go:42 - // where the full path to the file is in the same line as "Error Trace:" - if strings.Contains(outputLine, "Error Trace") { - absolutePath := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(outputLine), "Error Trace:")) - currentPath, err := os.Getwd() - if err != nil { - return "", 0, fmt.Errorf("failed getting current path: %v", err) - } + // some_test.go:42 (surrounded by white-space) + // the full path to the file is not available + matches := re.FindStringSubmatch(outputLine) - relPathRaw, err := filepath.Rel(currentPath, absolutePath) - if err != nil { - log.Debugf("failed to get relative path from trace: %v", err) - return "", 0, err - } - // in case we're deeply nested, remove any repeating dots - relPath := filepath.Clean(strings.TrimLeft(relPathRaw, "./")) - parts := strings.Split(relPath, ":") - if len(parts) != 2 { - log.Debugf("failed to split the trace path: %s", relPath) - return "", 0, nil - } + if len(matches) == 3 { + parts := matches[1:] file = parts[0] line, err = strconv.Atoi(parts[1]) if err != nil { diff --git a/cmd/tool/parser/parser_test.go b/cmd/tool/parser/parser_test.go index 7ee5cab7..d75b54ed 100644 --- a/cmd/tool/parser/parser_test.go +++ b/cmd/tool/parser/parser_test.go @@ -8,13 +8,13 @@ import ( func TestParseFailure_Ok(t *testing.T) { // given - failure := `Error Trace: /project/path/to/package/some_test.go:42` + failure := ` some_1s_test.go:42: \n` // when file, line, err := ParseFailure(failure) // then assert.NilError(t, err) - assert.Equal(t, file, "project/path/to/package/some_test.go") + assert.Equal(t, file, "some_1s_test.go") assert.Equal(t, line, 42) }