| | | 1 | | package reporter |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "context" |
| | | 5 | | "encoding/xml" |
| | | 6 | | "fmt" |
| | | 7 | | "io" |
| | | 8 | | "time" |
| | | 9 | | |
| | | 10 | | "github.com/jedi-knights/neospec/internal/domain" |
| | | 11 | | ) |
| | | 12 | | |
| | | 13 | | // JUnit writes test results in JUnit XML format. |
| | | 14 | | // https://github.com/testmoapp/junitxml |
| | | 15 | | // Coverage data is not included — JUnit is a test-results-only format. |
| | | 16 | | type JUnit struct{} |
| | | 17 | | |
| | | 18 | | // NewJUnit creates a JUnit reporter. |
| | | 19 | | func NewJUnit() *JUnit { return &JUnit{} } |
| | | 20 | | |
| | | 21 | | type junitTestSuites struct { |
| | | 22 | | XMLName xml.Name `xml:"testsuites"` |
| | | 23 | | Tests int `xml:"tests,attr"` |
| | | 24 | | Failures int `xml:"failures,attr"` |
| | | 25 | | Errors int `xml:"errors,attr"` |
| | | 26 | | Skipped int `xml:"skipped,attr"` |
| | | 27 | | Time float64 `xml:"time,attr"` |
| | | 28 | | Timestamp string `xml:"timestamp,attr"` |
| | | 29 | | TestSuites []junitTestSuite `xml:"testsuite"` |
| | | 30 | | } |
| | | 31 | | |
| | | 32 | | type junitTestSuite struct { |
| | | 33 | | Name string `xml:"name,attr"` |
| | | 34 | | Tests int `xml:"tests,attr"` |
| | | 35 | | Failures int `xml:"failures,attr"` |
| | | 36 | | Errors int `xml:"errors,attr"` |
| | | 37 | | Skipped int `xml:"skipped,attr"` |
| | | 38 | | Time float64 `xml:"time,attr"` |
| | | 39 | | TestCases []junitTestCase `xml:"testcase"` |
| | | 40 | | } |
| | | 41 | | |
| | | 42 | | type junitTestCase struct { |
| | | 43 | | Name string `xml:"name,attr"` |
| | | 44 | | Time float64 `xml:"time,attr"` |
| | | 45 | | Failure *junitFailure `xml:"failure,omitempty"` |
| | | 46 | | Error *junitError `xml:"error,omitempty"` |
| | | 47 | | Skipped *junitSkipped `xml:"skipped,omitempty"` |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | type junitFailure struct { |
| | | 51 | | Message string `xml:"message,attr"` |
| | | 52 | | Text string `xml:",chardata"` |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | type junitError struct { |
| | | 56 | | Message string `xml:"message,attr"` |
| | | 57 | | Text string `xml:",chardata"` |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | type junitSkipped struct{} |
| | | 61 | | |
| | 3 | 62 | | func (j *JUnit) Write(_ context.Context, w io.Writer, suite *domain.SuiteResult, _ *domain.CoverageData) error { |
| | 3 | 63 | | pass, fail, skip, errors := suite.Counts() |
| | 3 | 64 | | |
| | 3 | 65 | | jSuite := junitTestSuite{ |
| | 3 | 66 | | Name: "neospec", |
| | 3 | 67 | | Tests: len(suite.Tests), |
| | 3 | 68 | | Failures: fail, |
| | 3 | 69 | | Errors: errors, |
| | 3 | 70 | | Skipped: skip, |
| | 3 | 71 | | Time: suite.Duration.Seconds(), |
| | 3 | 72 | | } |
| | 3 | 73 | | |
| | 3 | 74 | | for _, t := range suite.Tests { |
| | 4 | 75 | | tc := junitTestCase{ |
| | 4 | 76 | | Name: t.Name, |
| | 4 | 77 | | Time: t.Duration.Seconds(), |
| | 4 | 78 | | } |
| | 4 | 79 | | switch t.Status { |
| | 1 | 80 | | case domain.StatusFail: |
| | 1 | 81 | | tc.Failure = &junitFailure{Message: t.Error, Text: t.Error} |
| | 1 | 82 | | case domain.StatusError: |
| | 1 | 83 | | tc.Error = &junitError{Message: t.Error, Text: t.Error} |
| | 1 | 84 | | case domain.StatusSkip: |
| | 1 | 85 | | tc.Skipped = &junitSkipped{} |
| | | 86 | | } |
| | 4 | 87 | | jSuite.TestCases = append(jSuite.TestCases, tc) |
| | | 88 | | } |
| | | 89 | | |
| | 3 | 90 | | root := junitTestSuites{ |
| | 3 | 91 | | Tests: len(suite.Tests), |
| | 3 | 92 | | Failures: fail, |
| | 3 | 93 | | Errors: errors, |
| | 3 | 94 | | Skipped: skip, |
| | 3 | 95 | | Time: suite.Duration.Seconds(), |
| | 3 | 96 | | Timestamp: time.Now().UTC().Format(time.RFC3339), |
| | 3 | 97 | | TestSuites: []junitTestSuite{jSuite}, |
| | 3 | 98 | | } |
| | 3 | 99 | | |
| | 3 | 100 | | _ = pass // pass count is not a JUnit attribute at the suites level |
| | 3 | 101 | | |
| | 3 | 102 | | fmt.Fprintln(w, `<?xml version="1.0" encoding="UTF-8"?>`) |
| | 3 | 103 | | enc := xml.NewEncoder(w) |
| | 3 | 104 | | enc.Indent("", " ") |
| | 1 | 105 | | if err := enc.Encode(root); err != nil { |
| | 1 | 106 | | return fmt.Errorf("encoding junit XML: %w", err) |
| | 1 | 107 | | } |
| | 2 | 108 | | return enc.Flush() |
| | | 109 | | } |