pax_global_header 0000666 0000000 0000000 00000000064 14016054740 0014513 g ustar 00root root 0000000 0000000 52 comment=6145f504ca0d053d5fd6c5379da09803fc3aef64
go-junit-1.0.0/ 0000775 0000000 0000000 00000000000 14016054740 0013245 5 ustar 00root root 0000000 0000000 go-junit-1.0.0/.circleci/ 0000775 0000000 0000000 00000000000 14016054740 0015100 5 ustar 00root root 0000000 0000000 go-junit-1.0.0/.circleci/config.yml 0000664 0000000 0000000 00000001057 14016054740 0017073 0 ustar 00root root 0000000 0000000 version: 2.1
jobs:
lint:
docker:
- image: circleci/golang:1.12.9
working_directory: /go/src/github.com/joshdk/go-junit
steps:
- checkout
- run:
name: Lint Go code
command: make lint
test:
docker:
- image: circleci/golang:1.12.9
working_directory: /go/src/github.com/joshdk/go-junit
steps:
- checkout
- run:
name: Test Go code
command: make test
- store_test_results:
path: test-results
workflows:
version: 2
build:
jobs:
- lint
- test
go-junit-1.0.0/.gitignore 0000664 0000000 0000000 00000000026 14016054740 0015233 0 ustar 00root root 0000000 0000000 test-results/
vendor/
go-junit-1.0.0/.golangci.yml 0000664 0000000 0000000 00000000557 14016054740 0015640 0 ustar 00root root 0000000 0000000 linters:
enable-all: true
disable:
- lll
- wsl
- gomnd
issues:
exclude-use-default: true
exclude:
# Triggered by table tests calling t.Run. See
# https://github.com/kyoh86/scopelint/issues/4 for more information.
- Using the variable on range scope `test` in function literal
# Triggered by long table tests.
- Function 'Test\w+' is too long
go-junit-1.0.0/LICENSE.txt 0000664 0000000 0000000 00000002053 14016054740 0015070 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) Josh Komoroske
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
go-junit-1.0.0/Makefile 0000664 0000000 0000000 00000002556 14016054740 0014715 0 ustar 00root root 0000000 0000000 #### Environment ####
# Enforce usage of the Go modules system.
export GO111MODULE := on
# Determine where `go get` will install binaries to.
GOBIN := $(HOME)/go/bin
ifdef GOPATH
GOBIN := $(GOPATH)/bin
endif
# All target for when make is run on its own.
.PHONY: all
all: test lint
#### Binary Dependencies ####
# Install binary for go-junit-report.
go-junit-report := $(GOBIN)/go-junit-report
$(go-junit-report):
cd /tmp && go get -u github.com/jstemmer/go-junit-report
# Install binary for golangci-lint.
golangci-lint := $(GOBIN)/golangci-lint
$(golangci-lint):
@./scripts/install-golangci-lint $(golangci-lint)
# Install binary for goimports.
goimports := $(GOBIN)/goimports
$(goimports):
cd /tmp && go get -u golang.org/x/tools/cmd/goimports
#### Linting ####
# Run code linters.
.PHONY: lint
lint: $(golangci-lint) style
golangci-lint run
# Run code formatters. Unformatted code will fail in CircleCI.
.PHONY: style
style: $(goimports)
ifdef CI
goimports -l .
else
goimports -l -w .
endif
#### Testing ####
# Run Go tests and generate a JUnit XML style test report for ingestion by CircleCI.
.PHONY: test
test: $(go-junit-report)
@mkdir -p test-results
@go test -race -v 2>&1 | tee test-results/report.log
@cat test-results/report.log | go-junit-report -set-exit-code > test-results/report.xml
# Clean up test reports.
.PHONY: clean
clean:
@rm -rf test-results
go-junit-1.0.0/README.md 0000664 0000000 0000000 00000012372 14016054740 0014531 0 ustar 00root root 0000000 0000000 [![License][license-badge]][license-link]
[![Godoc][godoc-badge]][godoc-link]
[![Go Report Card][go-report-badge]][go-report-link]
[![CircleCI][circleci-badge]][circleci-link]
# Go JUnit
🐜 Go library for ingesting JUnit XML reports
## Installing
You can fetch this library by running the following
```bash
go get -u github.com/joshdk/go-junit
```
## Usage
### Data Ingestion
This library has a number of ingestion methods for convenience.
The simplest of which parses raw JUnit XML data.
```go
xml := []byte(`
Assertion failed
`)
suites, err := junit.Ingest(xml)
if err != nil {
log.Fatalf("failed to ingest JUnit xml %v", err)
}
```
You can then inspect the contents of the ingested suites.
```go
for _, suite := range suites {
fmt.Println(suite.Name)
for _, test := range suite.Tests {
fmt.Printf(" %s\n", test.Name)
if test.Error != nil {
fmt.Printf(" %s: %s\n", test.Status, test.Error.Error())
} else {
fmt.Printf(" %s\n", test.Status)
}
}
}
```
And observe some output like this.
```
JUnitXmlReporter
JUnitXmlReporter.constructor
should default path to an empty string
failed: Assertion failed
should default consolidate to true
skipped
should default useDotNotation to true
passed
```
### More Examples
Additionally, you can ingest an entire file.
```go
suites, err := junit.IngestFile("test-reports/report.xml")
if err != nil {
log.Fatalf("failed to ingest JUnit xml %v", err)
}
```
Or a list of multiple files.
```go
suites, err := junit.IngestFiles([]string{
"test-reports/report-1.xml",
"test-reports/report-2.xml",
})
if err != nil {
log.Fatalf("failed to ingest JUnit xml %v", err)
}
```
Or any `.xml` files inside of a directory.
```go
suites, err := junit.IngestDir("test-reports/")
if err != nil {
log.Fatalf("failed to ingest JUnit xml %v", err)
}
```
### Data Formats
Due to the lack of implementation consistency in software that generates JUnit XML files, this library needs to take a somewhat looser approach to ingestion. As a consequence, many different possible JUnit formats can easily be ingested.
A single top level `testsuite` tag, containing multiple `testcase` instances.
```xml
```
A single top level `testsuites` tag, containing multiple `testsuite` instances.
```xml
```
(Despite not technically being valid XML) Multiple top level `testsuite` tags, containing multiple `testcase` instances.
```xml
```
In all cases, omitting (or even duplicated) the XML declaration tag is allowed.
```xml
```
## Contributing
Found a bug or want to make go-junit better? Please [open a pull request](https://github.com/joshdk/go-junit/compare)!
To make things easier, try out the following:
- Running `make test` will run the test suite to verify behavior.
- Running `make lint` will format the code, and report any linting issues using [golangci/golangci-lint](https://github.com/golangci/golangci-lint).
## License
This code is distributed under the [MIT License][license-link], see [LICENSE.txt][license-file] for more information.
[circleci-badge]: https://circleci.com/gh/joshdk/go-junit.svg?&style=shield
[circleci-link]: https://circleci.com/gh/joshdk/go-junit/tree/master
[go-report-badge]: https://goreportcard.com/badge/github.com/joshdk/go-junit
[go-report-link]: https://goreportcard.com/report/github.com/joshdk/go-junit
[godoc-badge]: https://godoc.org/github.com/joshdk/go-junit?status.svg
[godoc-link]: https://godoc.org/github.com/joshdk/go-junit
[license-badge]: https://img.shields.io/badge/license-MIT-green.svg
[license-file]: https://github.com/joshdk/go-junit/blob/master/LICENSE.txt
[license-link]: https://opensource.org/licenses/MIT
go-junit-1.0.0/go.mod 0000664 0000000 0000000 00000000127 14016054740 0014353 0 ustar 00root root 0000000 0000000 module github.com/joshdk/go-junit
go 1.12
require github.com/stretchr/testify v1.4.0
go-junit-1.0.0/go.sum 0000664 0000000 0000000 00000002030 14016054740 0014373 0 ustar 00root root 0000000 0000000 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
go-junit-1.0.0/ingest.go 0000664 0000000 0000000 00000005472 14016054740 0015075 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"strconv"
"strings"
"time"
)
// findSuites performs a depth-first search through the XML document, and
// attempts to ingest any "testsuite" tags that are encountered.
func findSuites(nodes []xmlNode, suites chan Suite) {
for _, node := range nodes {
switch node.XMLName.Local {
case "testsuite":
suites <- ingestSuite(node)
default:
findSuites(node.Nodes, suites)
}
}
}
func ingestSuite(root xmlNode) Suite {
suite := Suite{
Name: root.Attr("name"),
Package: root.Attr("package"),
Properties: root.Attrs,
}
for _, node := range root.Nodes {
switch node.XMLName.Local {
case "testsuite":
testsuite := ingestSuite(node)
suite.Suites = append(suite.Suites, testsuite)
case "testcase":
testcase := ingestTestcase(node)
suite.Tests = append(suite.Tests, testcase)
case "properties":
props := ingestProperties(node)
suite.Properties = props
case "system-out":
suite.SystemOut = string(node.Content)
case "system-err":
suite.SystemErr = string(node.Content)
}
}
suite.Aggregate()
return suite
}
func ingestProperties(root xmlNode) map[string]string {
props := make(map[string]string, len(root.Nodes))
for _, node := range root.Nodes {
if node.XMLName.Local == "property" {
name := node.Attr("name")
value := node.Attr("value")
props[name] = value
}
}
return props
}
func ingestTestcase(root xmlNode) Test {
test := Test{
Name: root.Attr("name"),
Classname: root.Attr("classname"),
Duration: duration(root.Attr("time")),
Status: StatusPassed,
Properties: root.Attrs,
}
for _, node := range root.Nodes {
switch node.XMLName.Local {
case "skipped":
test.Status = StatusSkipped
test.Message = node.Attr("message")
case "failure":
test.Status = StatusFailed
test.Message = node.Attr("message")
test.Error = ingestError(node)
case "error":
test.Status = StatusError
test.Message = node.Attr("message")
test.Error = ingestError(node)
case "system-out":
test.SystemOut = string(node.Content)
case "system-err":
test.SystemErr = string(node.Content)
}
}
return test
}
func ingestError(root xmlNode) Error {
return Error{
Body: string(root.Content),
Type: root.Attr("type"),
Message: root.Attr("message"),
}
}
func duration(t string) time.Duration {
// Remove commas for larger durations
t = strings.ReplaceAll(t, ",", "")
// Check if there was a valid decimal value
if s, err := strconv.ParseFloat(t, 64); err == nil {
return time.Duration(s*1000000) * time.Microsecond
}
// Check if there was a valid duration string
if d, err := time.ParseDuration(t); err == nil {
return d
}
return 0
}
go-junit-1.0.0/ingest_test.go 0000664 0000000 0000000 00000017667 14016054740 0016145 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExamplesInTheWild(t *testing.T) {
tests := []struct {
title string
filename string
origin string
check func(*testing.T, []Suite)
}{
{
title: "catchsoftware example",
filename: "testdata/catchsoftware.xml",
origin: "https://help.catchsoftware.com/display/ET/JUnit+Format",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 2)
assert.Len(t, suites[0].Tests, 0)
assert.Len(t, suites[1].Tests, 3)
assert.EqualError(t, suites[1].Tests[0].Error, "Assertion failed")
},
},
{
title: "cubic example",
filename: "testdata/cubic.xml",
origin: "https://llg.cubic.org/docs/junit/",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 1)
assert.Equal(t, "STDOUT text", suites[0].SystemOut)
assert.Equal(t, "STDERR text", suites[0].SystemErr)
assert.Equal(t, "STDOUT text", suites[0].Tests[0].SystemOut)
assert.Equal(t, "STDERR text", suites[0].Tests[0].SystemErr)
},
},
{
title: "go-junit-report example",
filename: "testdata/go-junit-report.xml",
origin: "https://github.com/jstemmer/go-junit-report/blob/master/testdata/06-report.xml",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 2)
assert.Len(t, suites[0].Tests, 2)
assert.Len(t, suites[1].Tests, 2)
assert.Equal(t, "1.0", suites[0].Properties["go.version"])
assert.Equal(t, "1.0", suites[1].Properties["go.version"])
assert.EqualError(t, suites[1].Tests[0].Error, "file_test.go:11: Error message\nfile_test.go:11: Longer\n\terror\n\tmessage.")
},
},
{
title: "go-junit-report skipped example",
filename: "testdata/go-junit-report-skipped.xml",
origin: "https://github.com/jstemmer/go-junit-report/blob/master/testdata/03-report.xml",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 2)
assert.Equal(t, "package/name", suites[0].Name)
assert.Equal(t, "TestOne", suites[0].Tests[0].Name)
assert.Equal(t, "file_test.go:11: Skip message", suites[0].Tests[0].Message)
},
},
{
title: "ibm example",
filename: "testdata/ibm.xml",
origin: "https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.2.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 1)
assert.EqualError(t, suites[0].Tests[0].Error, "\nWARNING: Use a program name that matches the source file name\nCategory: COBOL Code Review – Naming Conventions\nFile: /project/PROGRAM.cbl\nLine: 2\n ")
},
},
{
title: "jenkinsci example",
filename: "testdata/jenkinsci.xml",
origin: "https://github.com/jenkinsci/junit-plugin/blob/master/src/test/resources/hudson/tasks/junit/junit-report-1463.xml",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 6)
assert.Equal(t, "\n", suites[0].Properties["line.separator"])
assert.Equal(t, `\`, suites[0].Properties["file.separator"])
},
},
{
title: "nose2 example",
filename: "testdata/nose2.xml",
origin: "https://nose2.readthedocs.io/en/latest/plugins/junitxml.html",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 25)
assert.EqualError(t, suites[0].Tests[22].Error, "Traceback (most recent call last):\n File \"nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py\", line 13, in test_typeerr\n raise TypeError(\"oops\")\nTypeError: oops\n")
},
},
{
title: "python junit-xml example",
filename: "testdata/python-junit-xml.xml",
origin: "https://pypi.org/project/junit-xml/",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 1)
assert.Equal(t, "\n I am stdout!\n ", suites[0].Tests[0].SystemOut)
assert.Equal(t, "\n I am stderr!\n ", suites[0].Tests[0].SystemErr)
},
},
{
title: "surefire example",
filename: "testdata/surefire.xml",
origin: "https://gist.github.com/rwbergstrom/6f0193b1a12dca9d358e6043ee6abba4",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 1)
assert.Equal(t, "\n", suites[0].Properties["line.separator"])
assert.Equal(t, "Hello, World\n", suites[0].Tests[0].SystemOut)
assert.Equal(t, "I'm an error!\n", suites[0].Tests[0].SystemErr)
var testcase = Test{
Name: "testStdoutStderr",
Classname: "com.example.FooTest",
Duration: 1234560 * time.Millisecond,
Status: StatusFailed,
Error: Error{
Type: "java.lang.AssertionError",
Body: "java.lang.AssertionError\n\tat com.example.FooTest.testStdoutStderr(FooTest.java:13)\n",
},
Properties: map[string]string{
"classname": "com.example.FooTest",
"name": "testStdoutStderr",
"time": "1,234.56",
},
SystemOut: "Hello, World\n",
SystemErr: "I'm an error!\n",
}
assert.Equal(t, testcase, suites[0].Tests[0])
},
},
{
title: "fastlane example",
filename: "testdata/fastlane-trainer.xml",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 4)
var testcase = Test{
Name: "testSomething()",
Classname: "TestClassSample",
Duration: 342 * time.Millisecond,
Status: StatusFailed,
Message: "XCTAssertTrue failed",
Error: Error{
Message: "XCTAssertTrue failed",
Body: "\n ",
},
Properties: map[string]string{
"classname": "TestClassSample",
"name": "testSomething()",
"time": "0.342",
},
}
assert.Equal(t, testcase, suites[0].Tests[2])
assert.EqualError(t, suites[0].Tests[2].Error, "XCTAssertTrue failed")
assert.EqualError(t, suites[0].Tests[3].Error, "NullPointerException")
},
},
{
title: "phpunit example",
filename: "testdata/phpunit.xml",
check: func(t *testing.T, suites []Suite) {
assert.Len(t, suites, 1)
assert.Len(t, suites[0].Tests, 0)
assert.Len(t, suites[0].Suites, 1)
suite := suites[0].Suites[0]
assert.Len(t, suite.Tests, 1)
assert.Len(t, suite.Suites, 2)
assert.Equal(t, "SampleTest", suite.Name)
assert.Equal(t, "/untitled/tests/SampleTest.php", suite.Properties["file"])
var testcase = Test{
Name: "testA",
Classname: "SampleTest",
Duration: 5917 * time.Microsecond,
Status: StatusPassed,
Properties: map[string]string{
"assertions": "1",
"class": "SampleTest",
"classname": "SampleTest",
"file": "/untitled/tests/SampleTest.php",
"line": "7",
"name": "testA",
"time": "0.005917",
},
}
assert.Equal(t, testcase, suite.Tests[0])
assert.Len(t, suite.Suites[1].Suites, 0)
assert.Len(t, suite.Suites[1].Tests, 3)
assert.Equal(t, "testC with data set #0", suite.Suites[1].Tests[0].Name)
// checking recursive aggregation
suites[0].Aggregate()
actualTotals := suites[0].Totals
expectedTotals := Totals{
Tests: 7,
Passed: 4,
Skipped: 0,
Failed: 3,
Error: 0,
Duration: 8489 * time.Microsecond,
}
assert.Equal(t, expectedTotals, actualTotals)
},
},
}
for index, test := range tests {
name := fmt.Sprintf("#%d - %s", index+1, test.title)
t.Run(name, func(t *testing.T) {
suites, err := IngestFile(test.filename)
require.NoError(t, err)
test.check(t, suites)
})
}
}
go-junit-1.0.0/ingesters.go 0000664 0000000 0000000 00000004160 14016054740 0015600 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
)
// IngestDir will search the given directory for XML files and return a slice
// of all contained JUnit test suite definitions.
func IngestDir(directory string) ([]Suite, error) {
var filenames []string
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Add all regular files that end with ".xml"
if info.Mode().IsRegular() && strings.HasSuffix(info.Name(), ".xml") {
filenames = append(filenames, path)
}
return nil
})
if err != nil {
return nil, err
}
return IngestFiles(filenames)
}
// IngestFiles will parse the given XML files and return a slice of all
// contained JUnit test suite definitions.
func IngestFiles(filenames []string) ([]Suite, error) {
var all = make([]Suite, 0)
for _, filename := range filenames {
suites, err := IngestFile(filename)
if err != nil {
return nil, err
}
all = append(all, suites...)
}
return all, nil
}
// IngestFile will parse the given XML file and return a slice of all contained
// JUnit test suite definitions.
func IngestFile(filename string) ([]Suite, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
return IngestReader(file)
}
// IngestReader will parse the given XML reader and return a slice of all
// contained JUnit test suite definitions.
func IngestReader(reader io.Reader) ([]Suite, error) {
var (
suiteChan = make(chan Suite)
suites = make([]Suite, 0)
)
nodes, err := parse(reader)
if err != nil {
return nil, err
}
go func() {
findSuites(nodes, suiteChan)
close(suiteChan)
}()
for suite := range suiteChan {
suites = append(suites, suite)
}
return suites, nil
}
// Ingest will parse the given XML data and return a slice of all contained
// JUnit test suite definitions.
func Ingest(data []byte) ([]Suite, error) {
return IngestReader(bytes.NewReader(data))
}
go-junit-1.0.0/node.go 0000664 0000000 0000000 00000001761 14016054740 0014526 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import "encoding/xml"
type xmlNode struct {
XMLName xml.Name
Attrs map[string]string `xml:"-"`
Content []byte `xml:",innerxml"`
Nodes []xmlNode `xml:",any"`
}
func (n *xmlNode) Attr(name string) string {
return n.Attrs[name]
}
func (n *xmlNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type nodeAlias xmlNode
if err := d.DecodeElement((*nodeAlias)(n), &start); err != nil {
return err
}
content, err := extractContent(n.Content)
if err != nil {
return err
}
n.Content = content
n.Attrs = attrMap(start.Attr)
return nil
}
func attrMap(attrs []xml.Attr) map[string]string {
if len(attrs) == 0 {
return nil
}
attributes := make(map[string]string, len(attrs))
for _, attr := range attrs {
attributes[attr.Name.Local] = attr.Value
}
return attributes
}
go-junit-1.0.0/parse.go 0000664 0000000 0000000 00000007732 14016054740 0014717 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"bytes"
"encoding/xml"
"errors"
"html"
"io"
)
// reparentXML will wrap the given reader (which is assumed to be valid XML),
// in a fake root nodeAlias.
//
// This action is useful in the event that the original XML document does not
// have a single root nodeAlias, which is required by the XML specification.
// Additionally, Go's XML parser will silently drop all nodes after the first
// that is encountered, which can lead to data loss from a parser perspective.
// This function also enables the ingestion of blank XML files, which would
// normally cause a parsing error.
func reparentXML(reader io.Reader) io.Reader {
return io.MultiReader(
bytes.NewReader([]byte("")),
reader,
bytes.NewReader([]byte("")),
)
}
// extractContent parses the raw contents from an XML node, and returns it in a
// more consumable form.
//
// This function deals with two distinct classes of node data; Encoded entities
// and CDATA tags. These Encoded entities are normal (html escaped) text that
// you typically find between tags like so:
// • "Hello, world!" → "Hello, world!"
// • "I </3 XML" → "I 3 XML"
// CDATA tags are a special way to embed data that would normally require
// escaping, without escaping it, like so:
// • "" → "Hello, world!"
// • "" → "I </3 XML"
// • "" → "I 3 XML"
//
// This function specifically allows multiple interleaved instances of either
// encoded entities or cdata, and will decode them into one piece of normalized
// text, like so:
// • "I </3 XML . You probably too." → "I 3 XML a lot. You probably 3 XML too."
// └─────┬─────┘ └─┬─┘ └──────┬──────┘ └──┬──┘ └─┬─┘
// "I 3 XML " │ ". You probably " │ " too."
// "a lot" "3 XML"
//
// Errors are returned only when there are unmatched CDATA tags, although these
// should cause proper XML unmarshalling errors first, if encountered in an
// actual XML document.
func extractContent(data []byte) ([]byte, error) {
var (
cdataStart = []byte("")
mode int
output []byte
)
for {
if mode == 0 {
offset := bytes.Index(data, cdataStart)
if offset == -1 {
// The string "" appears in the data. This is an error!
return nil, errors.New("unmatched CDATA end tag")
}
output = append(output, html.UnescapeString(string(data))...)
break
}
// The string "" does not appear in the data. This is an error!
return nil, errors.New("unmatched CDATA start tag")
}
// The string "]]>" appears at some offset. Read up to that offset. Discard "]]>" prefix.
output = append(output, data[:offset]...)
data = data[offset:]
data = data[3:]
mode = 0
}
}
return output, nil
}
// parse unmarshalls the given XML data into a graph of nodes, and then returns
// a slice of all top-level nodes.
func parse(reader io.Reader) ([]xmlNode, error) {
var (
dec = xml.NewDecoder(reparentXML(reader))
root xmlNode
)
if err := dec.Decode(&root); err != nil {
return nil, err
}
return root.Nodes, nil
}
go-junit-1.0.0/parse_test.go 0000664 0000000 0000000 00000014521 14016054740 0015750 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReparent(t *testing.T) {
tests := []struct {
title string
input []byte
expected string
}{
{
title: "nil input",
expected: "",
},
{
title: "empty input",
input: []byte(""),
expected: "",
},
{
title: "xml input",
input: []byte(``),
expected: ``,
},
}
for index, test := range tests {
name := fmt.Sprintf("#%d - %s", index+1, test.title)
t.Run(name, func(t *testing.T) {
reader := reparentXML(bytes.NewReader(test.input))
actual, err := ioutil.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, test.expected, string(actual))
})
}
}
func TestParse(t *testing.T) {
tests := []struct {
title string
input []byte
expected []xmlNode
}{
{
title: "nil input",
},
{
title: "empty input",
input: []byte(``),
},
{
title: "plaintext input",
input: []byte(`This is some data that does not look like xml.`),
},
{
title: "json input",
input: []byte(`{"This is some data": "that looks like json"}`),
},
{
title: "single xml node",
input: []byte(``),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
},
},
},
{
title: "multiple xml nodes",
input: []byte(`
`),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
},
{
XMLName: xml.Name{
Local: "this-is-also-a-tag",
},
},
},
},
{
title: "single xml node with content",
input: []byte(`This is some content.`),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
Content: []byte("This is some content."),
},
},
},
{
title: "single xml node with encoded content",
input: []byte(`<sender>John Smith</sender>`),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
Content: []byte("John Smith"),
},
},
},
{
title: "single xml node with cdata content",
input: []byte(`John Smith]]>`),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
Content: []byte("John Smith"),
},
},
},
{
title: "single xml node with attributes",
input: []byte(``),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
Attrs: map[string]string{
"name": "my name",
"status": "passed",
},
},
},
},
{
title: "single xml node with encoded attributes",
input: []byte(``),
expected: []xmlNode{
{
XMLName: xml.Name{
Local: "this-is-a-tag",
},
Attrs: map[string]string{
"name": "John Smith",
},
},
},
},
}
for index, test := range tests {
name := fmt.Sprintf("#%d - %s", index+1, test.title)
t.Run(name, func(t *testing.T) {
actual, err := parse(bytes.NewReader(test.input))
require.Nil(t, err)
assert.Equal(t, test.expected, actual)
})
}
}
func TestExtract(t *testing.T) {
tests := []struct {
title string
input []byte
expected []byte
err string
}{
{
title: "nil content",
},
{
title: "empty content",
input: []byte(""),
},
{
title: "simple content",
input: []byte("hello world"),
expected: []byte("hello world"),
},
{
title: "complex content",
input: []byte("No bugs 🐜"),
expected: []byte("No bugs 🐜"),
},
{
title: "simple encoded content",
input: []byte("I </3 XML"),
expected: []byte("I 3 XML"),
},
{
title: "complex encoded content",
input: []byte(`<[['/\"]]>`),
expected: []byte(`<[['/\"]]>`),
},
{
title: "empty cdata content",
input: []byte(""),
},
{
title: "simple cdata content",
input: []byte(""),
expected: []byte("hello world"),
},
{
title: "complex cdata content",
input: []byte(""),
expected: []byte("I 3 XML"),
},
{
title: "complex encoded cdata content",
input: []byte(""),
expected: []byte("I </3 XML"),
},
{
title: "encoded content then cdata content",
input: []byte("I want to say that "),
expected: []byte("I want to say that I 3 XML"),
},
{
title: "cdata content then encoded content",
input: []byte(" a lot"),
expected: []byte("I 3 XML a lot"),
},
{
title: "mixture of encoded and cdata content",
input: []byte("I </3 XML . 🐜 You probably too."),
expected: []byte("I 3 XML a lot. 🐜 You probably 3 XML too."),
},
{
title: "unmatched cdata start tag",
input: []byte(""),
err: "unmatched CDATA end tag",
},
}
for index, test := range tests {
name := fmt.Sprintf("#%d - %s", index+1, test.title)
t.Run(name, func(t *testing.T) {
actual, err := extractContent(test.input)
checkError(t, test.err, err)
assert.Equal(t, test.expected, actual)
})
}
}
func checkError(t *testing.T, expected string, actual error) {
t.Helper()
switch {
case expected == "" && actual == nil:
return
case expected == "" && actual != nil:
t.Fatalf("expected no error but got %q", actual.Error())
case expected != "" && actual == nil:
t.Fatalf("expected %q but got nil", expected)
case expected == actual.Error():
return
default:
t.Fatalf("expected %q but got %q", expected, actual.Error())
}
}
go-junit-1.0.0/scripts/ 0000775 0000000 0000000 00000000000 14016054740 0014734 5 ustar 00root root 0000000 0000000 go-junit-1.0.0/scripts/install-golangci-lint 0000775 0000000 0000000 00000000662 14016054740 0021061 0 ustar 00root root 0000000 0000000 #!/bin/sh
set -eu
platform="$(uname -s)"
if [ "$platform" = Linux ]; then
prefix=golangci-lint-1.23.8-linux-amd64
elif [ "$platform" = Darwin ]; then
prefix=golangci-lint-1.23.8-darwin-amd64
fi
cd "$(mktemp -d)" || exit 1
wget -q "https://github.com/golangci/golangci-lint/releases/download/v1.23.8/${prefix}.tar.gz"
tar -xf "${prefix}.tar.gz"
mkdir -p "$(dirname "$1")"
install "${prefix}/golangci-lint" "$1"
rm -rf "$PWD"
go-junit-1.0.0/testdata/ 0000775 0000000 0000000 00000000000 14016054740 0015056 5 ustar 00root root 0000000 0000000 go-junit-1.0.0/testdata/catchsoftware.xml 0000664 0000000 0000000 00000002067 14016054740 0020442 0 ustar 00root root 0000000 0000000
Assertion failed
go-junit-1.0.0/testdata/cubic.xml 0000664 0000000 0000000 00000005275 14016054740 0016676 0 ustar 00root root 0000000 0000000
STDOUT text
STDERR text
STDOUT text
STDERR text
go-junit-1.0.0/testdata/fastlane-trainer.xml 0000664 0000000 0000000 00000001304 14016054740 0021035 0 ustar 00root root 0000000 0000000
go-junit-1.0.0/testdata/go-junit-report-skipped.xml 0000664 0000000 0000000 00000000667 14016054740 0022313 0 ustar 00root root 0000000 0000000
go-junit-1.0.0/testdata/go-junit-report.xml 0000664 0000000 0000000 00000001425 14016054740 0020647 0 ustar 00root root 0000000 0000000
file_test.go:11: Error message
file_test.go:11: Longer
error
message.
go-junit-1.0.0/testdata/ibm.xml 0000664 0000000 0000000 00000001346 14016054740 0016353 0 ustar 00root root 0000000 0000000
WARNING: Use a program name that matches the source file name
Category: COBOL Code Review – Naming Conventions
File: /project/PROGRAM.cbl
Line: 2
go-junit-1.0.0/testdata/jenkinsci.xml 0000664 0000000 0000000 00000020410 14016054740 0017552 0 ustar 00root root 0000000 0000000
go-junit-1.0.0/testdata/nose2.xml 0000664 0000000 0000000 00000010277 14016054740 0016635 0 ustar 00root root 0000000 0000000
Traceback (most recent call last):
File "nose2/plugins/loader/parameters.py", line 162, in func
return obj(*argSet)
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 64, in test_params_func
assert a == 1
AssertionError
Traceback (most recent call last):
File "nose2/plugins/loader/parameters.py", line 162, in func
return obj(*argSet)
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 69, in test_params_func_multi_arg
assert a == b
AssertionError
Traceback (most recent call last):
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 17, in test_failed
assert False, "I failed"
AssertionError: I failed
Traceback (most recent call last):
File "nose2/plugins/loader/parameters.py", line 144, in _method
return method(self, *argSet)
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 29, in test_params_method
self.assertEqual(a, 1)
AssertionError: 2 != 1
Traceback (most recent call last):
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 13, in test_typeerr
raise TypeError("oops")
TypeError: oops
Traceback (most recent call last):
File "nose2/plugins/loader/generators.py", line 145, in method
return func(*args)
File "nose2/tests/functional/support/scenario/tests_in_package/pkg1/test/test_things.py", line 24, in check
assert x == 1
AssertionError
go-junit-1.0.0/testdata/phpunit.xml 0000664 0000000 0000000 00000005760 14016054740 0017277 0 ustar 00root root 0000000 0000000
SampleTest::testB with data set "bool" (false)
should be true
Failed asserting that false matches expected true.
/untitled/tests/SampleTest.php:18
SampleTest::testC with data set #1 (0)
should be true
Failed asserting that 0 matches expected true.
/untitled/tests/SampleTest.php:34
SampleTest::testC with data set #2 ('')
should be true
Failed asserting that '' matches expected true.
/untitled/tests/SampleTest.php:34
go-junit-1.0.0/testdata/python-junit-xml.xml 0000664 0000000 0000000 00000000614 14016054740 0021047 0 ustar 00root root 0000000 0000000
I am stdout!
I am stderr!
go-junit-1.0.0/testdata/surefire.xml 0000664 0000000 0000000 00000006766 14016054740 0017443 0 ustar 00root root 0000000 0000000
java.lang.AssertionError
at com.example.FooTest.testStdoutStderr(FooTest.java:13)
go-junit-1.0.0/types.go 0000664 0000000 0000000 00000014240 14016054740 0014741 0 ustar 00root root 0000000 0000000 // Copyright Josh Komoroske. All rights reserved.
// Use of this source code is governed by the MIT license,
// a copy of which can be found in the LICENSE.txt file.
package junit
import (
"strings"
"time"
)
// Status represents the result of a single a JUnit testcase. Indicates if a
// testcase was run, and if it was successful.
type Status string
const (
// StatusPassed represents a JUnit testcase that was run, and did not
// result in an error or a failure.
StatusPassed Status = "passed"
// StatusSkipped represents a JUnit testcase that was intentionally
// skipped.
StatusSkipped Status = "skipped"
// StatusFailed represents a JUnit testcase that was run, but resulted in
// a failure. Failures are violations of declared test expectations,
// such as a failed assertion.
StatusFailed Status = "failed"
// StatusError represents a JUnit testcase that was run, but resulted in
// an error. Errors are unexpected violations of the test itself, such as
// an uncaught exception.
StatusError Status = "error"
)
// Totals contains aggregated results across a set of test runs. Is usually
// calculated as a sum of all given test runs, and overrides whatever was given
// at the suite level.
//
// The following relation should hold true.
// Tests == (Passed + Skipped + Failed + Error)
type Totals struct {
// Tests is the total number of tests run.
Tests int `json:"tests" yaml:"tests"`
// Passed is the total number of tests that passed successfully.
Passed int `json:"passed" yaml:"passed"`
// Skipped is the total number of tests that were skipped.
Skipped int `json:"skipped" yaml:"skipped"`
// Failed is the total number of tests that resulted in a failure.
Failed int `json:"failed" yaml:"failed"`
// Error is the total number of tests that resulted in an error.
Error int `json:"error" yaml:"error"`
// Duration is the total time taken to run all tests.
Duration time.Duration `json:"duration" yaml:"duration"`
}
// Suite represents a logical grouping (suite) of tests.
type Suite struct {
// Name is a descriptor given to the suite.
Name string `json:"name" yaml:"name"`
// Package is an additional descriptor for the hierarchy of the suite.
Package string `json:"package" yaml:"package"`
// Properties is a mapping of key-value pairs that were available when the
// tests were run.
Properties map[string]string `json:"properties,omitempty" yaml:"properties,omitempty"`
// Tests is an ordered collection of tests with associated results.
Tests []Test `json:"tests,omitempty" yaml:"tests,omitempty"`
// Suites is an ordered collection of suites with associated tests.
Suites []Suite `json:"suites,omitempty" yaml:"suites,omitempty"`
// SystemOut is textual test output for the suite. Usually output that is
// written to stdout.
SystemOut string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
// SystemErr is textual test error output for the suite. Usually output that is
// written to stderr.
SystemErr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
// Totals is the aggregated results of all tests.
Totals Totals `json:"totals" yaml:"totals"`
}
// Aggregate calculates result sums across all tests and nested suites.
func (s *Suite) Aggregate() {
totals := Totals{Tests: len(s.Tests)}
for _, test := range s.Tests {
totals.Duration += test.Duration
switch test.Status {
case StatusPassed:
totals.Passed++
case StatusSkipped:
totals.Skipped++
case StatusFailed:
totals.Failed++
case StatusError:
totals.Error++
}
}
// just summing totals from nested suites
for _, suite := range s.Suites {
suite.Aggregate()
totals.Tests += suite.Totals.Tests
totals.Duration += suite.Totals.Duration
totals.Passed += suite.Totals.Passed
totals.Skipped += suite.Totals.Skipped
totals.Failed += suite.Totals.Failed
totals.Error += suite.Totals.Error
}
s.Totals = totals
}
// Test represents the results of a single test run.
type Test struct {
// Name is a descriptor given to the test.
Name string `json:"name" yaml:"name"`
// Classname is an additional descriptor for the hierarchy of the test.
Classname string `json:"classname" yaml:"classname"`
// Duration is the total time taken to run the tests.
Duration time.Duration `json:"duration" yaml:"duration"`
// Status is the result of the test. Status values are passed, skipped,
// failure, & error.
Status Status `json:"status" yaml:"status"`
// Message is an textual description optionally included with a skipped,
// failure, or error test case.
Message string `json:"message" yaml:"message"`
// Error is a record of the failure or error of a test, if applicable.
//
// The following relations should hold true.
// Error == nil && (Status == Passed || Status == Skipped)
// Error != nil && (Status == Failed || Status == Error)
Error error `json:"error" yaml:"error"`
// Additional properties from XML node attributes.
// Some tools use them to store additional information about test location.
Properties map[string]string `json:"properties" yaml:"properties"`
// SystemOut is textual output for the test case. Usually output that is
// written to stdout.
SystemOut string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
// SystemErr is textual error output for the test case. Usually output that is
// written to stderr.
SystemErr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
}
// Error represents an erroneous test result.
type Error struct {
// Message is a descriptor given to the error. Purpose and values differ by
// environment.
Message string `json:"message,omitempty" yaml:"message,omitempty"`
// Type is a descriptor given to the error. Purpose and values differ by
// framework. Value is typically an exception class, such as an assertion.
Type string `json:"type,omitempty" yaml:"type,omitempty"`
// Body is extended text for the error. Purpose and values differ by
// framework. Value is typically a stacktrace.
Body string `json:"body,omitempty" yaml:"body,omitempty"`
}
// Error returns a textual description of the test error.
func (err Error) Error() string {
switch {
case strings.TrimSpace(err.Body) != "":
return err.Body
case strings.TrimSpace(err.Message) != "":
return err.Message
default:
return err.Type
}
}