pax_global_header00006660000000000000000000000064140160547400014513gustar00rootroot0000000000000052 comment=6145f504ca0d053d5fd6c5379da09803fc3aef64 go-junit-1.0.0/000077500000000000000000000000001401605474000132455ustar00rootroot00000000000000go-junit-1.0.0/.circleci/000077500000000000000000000000001401605474000151005ustar00rootroot00000000000000go-junit-1.0.0/.circleci/config.yml000066400000000000000000000010571401605474000170730ustar00rootroot00000000000000version: 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/.gitignore000066400000000000000000000000261401605474000152330ustar00rootroot00000000000000test-results/ vendor/ go-junit-1.0.0/.golangci.yml000066400000000000000000000005571401605474000156400ustar00rootroot00000000000000linters: 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.txt000066400000000000000000000020531401605474000150700ustar00rootroot00000000000000MIT 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/Makefile000066400000000000000000000025561401605474000147150ustar00rootroot00000000000000#### 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.md000066400000000000000000000123721401605474000145310ustar00rootroot00000000000000[![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.mod000066400000000000000000000001271401605474000143530ustar00rootroot00000000000000module github.com/joshdk/go-junit go 1.12 require github.com/stretchr/testify v1.4.0 go-junit-1.0.0/go.sum000066400000000000000000000020301401605474000143730ustar00rootroot00000000000000github.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.go000066400000000000000000000054721401605474000150750ustar00rootroot00000000000000// 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.go000066400000000000000000000176671401605474000161450ustar00rootroot00000000000000// 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.go000066400000000000000000000041601401605474000156000ustar00rootroot00000000000000// 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.go000066400000000000000000000017611401605474000145260ustar00rootroot00000000000000// 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.go000066400000000000000000000077321401605474000147170ustar00rootroot00000000000000// 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 " → "Hello, world!" // • "" → "I </3 XML" // • "" → "I . You probably too." → "I ") 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.go000066400000000000000000000145211401605474000157500ustar00rootroot00000000000000// 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 `), }, { 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 "), 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 a lot"), expected: []byte("I . 🐜 You probably too."), expected: []byte("I "), 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/000077500000000000000000000000001401605474000147345ustar00rootroot00000000000000go-junit-1.0.0/scripts/install-golangci-lint000077500000000000000000000006621401605474000210610ustar00rootroot00000000000000#!/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/000077500000000000000000000000001401605474000150565ustar00rootroot00000000000000go-junit-1.0.0/testdata/catchsoftware.xml000066400000000000000000000020671401605474000204420ustar00rootroot00000000000000 Assertion failed go-junit-1.0.0/testdata/cubic.xml000066400000000000000000000052751401605474000166760ustar00rootroot00000000000000 STDOUT text STDERR text STDOUT text STDERR text go-junit-1.0.0/testdata/fastlane-trainer.xml000066400000000000000000000013041401605474000210350ustar00rootroot00000000000000 go-junit-1.0.0/testdata/go-junit-report-skipped.xml000066400000000000000000000006671401605474000223130ustar00rootroot00000000000000 go-junit-1.0.0/testdata/go-junit-report.xml000066400000000000000000000014251401605474000206470ustar00rootroot00000000000000 file_test.go:11: Error message file_test.go:11: Longer error message. go-junit-1.0.0/testdata/ibm.xml000066400000000000000000000013461401605474000163530ustar00rootroot00000000000000 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.xml000066400000000000000000000204101401605474000175520ustar00rootroot00000000000000 go-junit-1.0.0/testdata/nose2.xml000066400000000000000000000102771401605474000166350ustar00rootroot00000000000000 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.xml000066400000000000000000000057601401605474000172770ustar00rootroot00000000000000 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.xml000066400000000000000000000006141401605474000210470ustar00rootroot00000000000000 I am stdout! I am stderr! go-junit-1.0.0/testdata/surefire.xml000066400000000000000000000067661401605474000174430ustar00rootroot00000000000000 java.lang.AssertionError at com.example.FooTest.testStdoutStderr(FooTest.java:13) go-junit-1.0.0/types.go000066400000000000000000000142401401605474000147410ustar00rootroot00000000000000// 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 } }