Skip to content

Commit 1c5f71a

Browse files
authored
fix: include filename in JUnit XML parse errors and consolidate directory walk (#895)
* green: ingestJunitDir error includes filename when XML parsing fails Replace junit.IngestDir() with our own filepath.Walk that calls junit.IngestFile() per file and wraps errors with the filename. This makes XML parsing errors actionable — users can now see which file caused the failure. Add test fixture with ISO-8859-15 encoding declaration and three unit tests for ingestJunitDir: filename in error, no-tests-found, and valid JUnit parsing. Refs #894 * green: getJunitFilenames error includes filename when XML parsing fails Same wrapping treatment as ingestJunitDir — wrap junit.IngestFile() errors with the filename so users can identify which file caused the failure. Refs #894 * green: consolidate JUnit dir walk into single pass ingestJunitDir now returns both parsed results and the list of JUnit filenames (for upload), eliminating the separate getJunitFilenames() function and its redundant directory walk. Refs #894 * refactor: address review feedback - Return nil instead of empty slice on Walk error for consistency - Merge TestIngestJunitDir_ReturnsFilenames into TestIngestJunitDir Refs #894 * green: user-friendly error message for unsupported XML encoding Replace the cryptic Go stdlib error (Decoder.CharsetReader is nil) with an actionable message that tells the user which file failed, that only UTF-8 is supported, and to use --results-dir if the file is not a JUnit results file. Refs #894 * refactor: address second round of review feedback - Use filepath.WalkDir instead of filepath.Walk (avoids os.Lstat per entry) - Return nil, nil on all error paths for consistency - Assert exact filename count in test Refs #894 * fix: alphabetical import ordering Refs #894
1 parent 66718f9 commit 1c5f71a

3 files changed

Lines changed: 73 additions & 43 deletions

File tree

cmd/kosli/attestJunit.go

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"io"
6+
"io/fs"
67
"net/http"
78
"net/url"
89
"os"
@@ -161,17 +162,13 @@ func (o *attestJunitOptions) run(args []string) error {
161162
return err
162163
}
163164

164-
o.payload.JUnitResults, err = ingestJunitDir(o.testResultsDir)
165+
var junitFilenames []string
166+
o.payload.JUnitResults, junitFilenames, err = ingestJunitDir(o.testResultsDir)
165167
if err != nil {
166168
return err
167169
}
168170

169171
if o.uploadResultsDir {
170-
// prepare the files to upload as attachments. We are only interested in the actual Junit XMl files
171-
junitFilenames, err := getJunitFilenames(o.testResultsDir)
172-
if err != nil {
173-
return err
174-
}
175172
o.attachments = append(o.attachments, junitFilenames...)
176173
}
177174

@@ -212,22 +209,44 @@ type JUnitResults struct {
212209
Timestamp float64 `json:"timestamp,omitempty"`
213210
}
214211

215-
func ingestJunitDir(testResultsDir string) ([]*JUnitResults, error) {
212+
func ingestJunitDir(testResultsDir string) ([]*JUnitResults, []string, error) {
216213
results := []*JUnitResults{}
217-
suites, err := junit.IngestDir(testResultsDir)
214+
var junitFilenames []string
215+
216+
var allSuites []junit.Suite
217+
err := filepath.WalkDir(testResultsDir, func(path string, d fs.DirEntry, err error) error {
218+
if err != nil {
219+
return err
220+
}
221+
if d.Type().IsRegular() && strings.HasSuffix(d.Name(), ".xml") {
222+
suites, err := junit.IngestFile(path)
223+
if err != nil {
224+
if strings.Contains(err.Error(), "Decoder.CharsetReader is nil") {
225+
return fmt.Errorf("failed to parse XML file %s: only UTF-8 encoding is supported. "+
226+
"If this is not a JUnit results file, use --results-dir to specify the directory containing only JUnit XML files", path)
227+
}
228+
return fmt.Errorf("failed to parse XML file %s: %w", path, err)
229+
}
230+
if len(suites) > 0 {
231+
allSuites = append(allSuites, suites...)
232+
junitFilenames = append(junitFilenames, path)
233+
}
234+
}
235+
return nil
236+
})
218237
if err != nil {
219-
return results, err
238+
return nil, nil, err
220239
}
221240

222-
if len(suites) == 0 {
223-
return results, fmt.Errorf("no tests found in %s directory", testResultsDir)
241+
if len(allSuites) == 0 {
242+
return nil, nil, fmt.Errorf("no tests found in %s directory", testResultsDir)
224243
}
225244

226-
for _, suite := range suites {
245+
for _, suite := range allSuites {
227246
var timestamp float64
228247
timestamp, err := parseTimestamp(suite.Properties["timestamp"])
229248
if err != nil {
230-
return results, err
249+
return nil, nil, err
231250
}
232251

233252
// The values in suite.Totals are based on the results of the tests in the suite and not in the header of the suite.
@@ -244,36 +263,7 @@ func ingestJunitDir(testResultsDir string) ([]*JUnitResults, error) {
244263
results = append(results, suiteResult)
245264
}
246265

247-
return results, nil
248-
}
249-
250-
func getJunitFilenames(directory string) ([]string, error) {
251-
var filenames []string
252-
253-
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
254-
if err != nil {
255-
return err
256-
}
257-
258-
// Add all regular files that end with ".xml"
259-
if info.Mode().IsRegular() && strings.HasSuffix(info.Name(), ".xml") {
260-
suites, err := junit.IngestFile(path)
261-
if err != nil {
262-
return err
263-
}
264-
if len(suites) > 0 {
265-
filenames = append(filenames, path)
266-
}
267-
}
268-
269-
return nil
270-
})
271-
272-
if err != nil {
273-
return filenames, err
274-
}
275-
276-
return filenames, nil
266+
return results, junitFilenames, nil
277267
}
278268

279269
func parseTimestamp(timestampStr string) (float64, error) {

cmd/kosli/attestJunit_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
79
"github.com/stretchr/testify/suite"
810
)
911

@@ -138,3 +140,34 @@ func (suite *AttestJunitCommandTestSuite) TestAttestJunitCmd() {
138140
func TestAttestJunitCommandTestSuite(t *testing.T) {
139141
suite.Run(t, new(AttestJunitCommandTestSuite))
140142
}
143+
144+
func TestIngestJunitDir(t *testing.T) {
145+
t.Run("error includes filename and user-friendly message for unsupported encoding", func(t *testing.T) {
146+
_, _, err := ingestJunitDir("testdata_junit_iso8859")
147+
require.Error(t, err)
148+
assert.Contains(t, err.Error(), "results.xml")
149+
assert.Contains(t, err.Error(), "only UTF-8 encoding is supported")
150+
assert.Contains(t, err.Error(), "--results-dir")
151+
})
152+
153+
t.Run("returns no tests found for empty directory", func(t *testing.T) {
154+
dir := t.TempDir()
155+
_, _, err := ingestJunitDir(dir)
156+
require.Error(t, err)
157+
assert.Contains(t, err.Error(), "no tests found")
158+
})
159+
160+
t.Run("parses valid JUnit XML correctly", func(t *testing.T) {
161+
results, _, err := ingestJunitDir("testdata/junit")
162+
require.NoError(t, err)
163+
assert.NotEmpty(t, results)
164+
})
165+
166+
t.Run("returns filenames of parsed JUnit XML files", func(t *testing.T) {
167+
results, filenames, err := ingestJunitDir("testdata/junit")
168+
require.NoError(t, err)
169+
assert.NotEmpty(t, results)
170+
assert.Len(t, filenames, 1)
171+
assert.Contains(t, filenames[0], ".xml")
172+
})
173+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="ISO-8859-15"?>
2+
<testsuites>
3+
<testsuite name="com.example.MyTest" tests="2" failures="0" errors="0" time="0.123">
4+
<testcase name="testPass" classname="com.example.MyTest" time="0.05"/>
5+
<testcase name="testAlsoPass" classname="com.example.MyTest" time="0.073"/>
6+
</testsuite>
7+
</testsuites>

0 commit comments

Comments
 (0)