Browse code

added junitreport tool

Steve Kuznetsov authored on 2015/11/20 06:33:02
Showing 70 changed files
... ...
@@ -137,6 +137,7 @@ test-int-plus: build
137 137
 endif
138 138
 test-int-plus:
139 139
 	hack/test-cmd.sh
140
+	hack/test-tools.sh
140 141
 	KUBE_RACE=" " hack/test-integration-docker.sh
141 142
 	hack/test-end-to-end-docker.sh
142 143
 ifeq ($(EXTENDED),true)
143 144
new file mode 100755
... ...
@@ -0,0 +1,24 @@
0
+#!/bin/bash
1
+
2
+# This command runs any exposed integration tests for the developer tools
3
+
4
+set -o errexit
5
+set -o nounset
6
+set -o pipefail
7
+
8
+STARTTIME=$(date +%s)
9
+OS_ROOT=$(dirname "${BASH_SOURCE}")/..
10
+cd "${OS_ROOT}"
11
+source "${OS_ROOT}/hack/util.sh"
12
+source "${OS_ROOT}/hack/cmd_util.sh"
13
+os::log::install_errexit
14
+
15
+for tool in ${OS_ROOT}/tools/*; do
16
+	test_file=${tool}/test/integration.sh
17
+	if [ -e ${test_file} ]; then
18
+		# if the tool exposes an integration test, run it
19
+		os::cmd::expect_success "${test_file}"
20
+	fi
21
+done
22
+
23
+echo "test-tools: ok"
0 24
\ No newline at end of file
1 25
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+junitreport
0 1
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+# junitreport
1
+
2
+`junitreport` is a tool that allows for the consumption of test output in order to create jUnit XML.
3
+
4
+## Installation
5
+
6
+In order to build and install `junitreport`, from the root of the OpenShift Origin repository, run `hack/build-go.sh tools/junitreport`. 
7
+
8
+## Usage 
9
+
10
+`junitreport` can read the output of different types of tests. Specify which output is being read with `--type=<type>`. Supported test output types currently include `'gotest'`, for `go test` output. The default test type is `'gotest'`. 
11
+
12
+`junitreport` can output flat or nested test suites. To choose which type of output to use, set `--suites=<type>` to either `'flat'` or `'nested'`. The default suite output structure is `'flat'`. When creating nested test suites, `junitreport` will use `/` as the delimeter between suite names: `github.com/maintainer/repository/suite` will be parsed as a hierarchy of `github.com`, `github.com/maintainer`, *etc.* If you are requesting nested test suite output but do not want the root suite(s) to be as general as `github.com`, for example, set `--roots=<root suite names>` to be a comma-delimited list of the names of the suites you wish to use as roots. If the parser encounters a package outside of those roots, it will ignore it. This allows a user to provide a root suite and only collect data for children of that root from a larger data set.
13
+
14
+Ensure that the output you are feeding `junitreport` is free of extraneous text - any lines that are not test/suite declarations, metadata, or results are interpreted as test output. Text that you do not expect to see in Jenkins, for example, while looking at the output of a failed test should not be included in the input to `junitreport`.
15
+
16
+Currently, `junitreport` does not support the parsing of parallel test output.
17
+
18
+### Examples
19
+
20
+To parse the output of `go test` into a flat collection of test suites:
21
+
22
+```sh
23
+
24
+$ go test -v -cover ./... | junitreport > report.xml
25
+```
26
+
27
+To parse the output of `go test` into a nested collection of test suites rooted at `github.com/maintainer`:
28
+
29
+```sh
30
+
31
+$ go test -v -cover ./... | junitreport --suites=nested --roots=github.com/maintainer > report.xml
32
+```
33
+
34
+### Testing
35
+
36
+`junitreport` has unit tests as well as integration tests. To run the unit tests from the `junitreport` root directory:
37
+
38
+```sh
39
+$ go test -v -cover ./...
40
+```
41
+
42
+To run the integration tests from the `junitreport` root directory:
43
+
44
+```sh
45
+$ test/integration.sh
46
+```
0 47
\ No newline at end of file
1 48
new file mode 100644
... ...
@@ -0,0 +1,134 @@
0
+package main
1
+
2
+import (
3
+	"flag"
4
+	"fmt"
5
+	"io"
6
+	"os"
7
+	"strings"
8
+
9
+	"github.com/openshift/origin/tools/junitreport/pkg/cmd"
10
+)
11
+
12
+var (
13
+	// parserType is a flag that holds the type of parser to use
14
+	parserType string
15
+
16
+	// builderType is a flag that holds the type of builder to use
17
+	builderType string
18
+
19
+	// rootSuites is a flag that holds the comma-delimited list of root suite names
20
+	rootSuites string
21
+
22
+	// testOutputFile is a flag that holds the path to the file containing test output
23
+	testOutputFile string
24
+)
25
+
26
+const (
27
+	defaultParserType     = "gotest"
28
+	defaultBuilderType    = "flat"
29
+	defaultTestOutputFile = "/dev/stdin"
30
+)
31
+
32
+func init() {
33
+	flag.StringVar(&parserType, "type", defaultParserType, "which type of test output to parse")
34
+	flag.StringVar(&builderType, "suites", defaultBuilderType, "which test suite structure to use")
35
+	flag.StringVar(&rootSuites, "roots", "", "comma-delimited list of root suite names")
36
+	flag.StringVar(&testOutputFile, "f", defaultTestOutputFile, "the path to the file containing test output to consume")
37
+}
38
+
39
+const (
40
+	junitReportUsageLong = `Consume test output to create jUnit XML files and summarize jUnit XML files.
41
+
42
+%[1]s consumes test output through Stdin and creates jUnit XML files. Currently, only the output of 'go test'
43
+is supported. jUnit XML can be build with nested or flat test suites. Sub-trees of test suites can be selected
44
+when using the nested test-suites representation to only build XML for some subset of the test output. This
45
+parser is greedy, so all output not directly related to a test suite is considered test case output.
46
+`
47
+
48
+	junitReportUsage = `Usage:
49
+  %[1]s [--type=TEST-OUTPUT-TYPE] [--suites=SUITE-TYPE] [-f=FILE]
50
+  %[1]s [-f=FILE] summarize
51
+`
52
+
53
+	junitReportExamples = `Examples:
54
+  # Consume 'go test' output to create a jUnit XML file
55
+  $ go test -v -cover ./... | %[1]s > report.xml
56
+
57
+  # Consume 'go test' output from a file to create a jUnit XML file
58
+  $ %[1]s -f testoutput.txt > report.xml
59
+
60
+  # Consume 'go test' output to create a jUnit XML file with nested test suites
61
+  $ go test -v -cover ./... | junitreport --suites=nested > report.xml
62
+
63
+  # Consume 'go test' output to create a jUnit XML file with nested test suites rooted at 'github.com/maintainer'
64
+  $ go test -v -cover ./... | junitreport --suites=nested --roots=github.com/maintainer > report.xml
65
+
66
+  # Describe failures and skipped tests in an existing jUnit XML file
67
+  $ cat report.xml | %[1]s summarize
68
+`
69
+)
70
+
71
+func main() {
72
+	flag.Usage = func() {
73
+		fmt.Fprintf(os.Stderr, junitReportUsageLong+"\n", os.Args[0])
74
+		fmt.Fprintf(os.Stderr, junitReportUsage+"\n", os.Args[0])
75
+		fmt.Fprintf(os.Stderr, junitReportExamples+"\n", os.Args[0])
76
+		fmt.Fprintln(os.Stderr, "Options:")
77
+		flag.PrintDefaults()
78
+		os.Exit(2)
79
+	}
80
+
81
+	flag.Parse()
82
+
83
+	var rootSuiteNames []string
84
+	if len(rootSuites) > 0 {
85
+		rootSuiteNames = strings.Split(rootSuites, ",")
86
+	}
87
+
88
+	var input io.Reader
89
+	if testOutputFile == defaultTestOutputFile {
90
+		input = os.Stdin
91
+	} else {
92
+		file, err := os.Open(testOutputFile)
93
+		if err != nil {
94
+			fmt.Fprintf(os.Stderr, "Error reading input file: %v\n", err)
95
+		}
96
+		defer file.Close()
97
+		input = file
98
+	}
99
+
100
+	arguments := flag.Args()
101
+	// If we are asked to summarize an XML file, that is all we do
102
+	if len(arguments) == 1 && arguments[0] == "summarize" {
103
+		summary, err := cmd.Summarize(input)
104
+		if err != nil {
105
+			fmt.Fprintf(os.Stderr, "Error summarizing jUnit XML file: %v\n", err)
106
+			os.Exit(1)
107
+		}
108
+		fmt.Fprint(os.Stdout, summary)
109
+		os.Exit(0)
110
+	}
111
+	if len(arguments) > 1 {
112
+		fmt.Fprintf(os.Stderr, "Incorrect usage of %[1]s, see '%[1]s --help' for more details.\n", os.Args[0])
113
+		os.Exit(1)
114
+	}
115
+
116
+	// Otherwise, we get ready to parse and generate XML output.
117
+	options := cmd.JUnitReportOptions{
118
+		Input:  input,
119
+		Output: os.Stdout,
120
+	}
121
+
122
+	err := options.Complete(builderType, parserType, rootSuiteNames)
123
+	if err != nil {
124
+		fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
125
+		os.Exit(1)
126
+	}
127
+
128
+	err = options.Run()
129
+	if err != nil {
130
+		fmt.Fprintf(os.Stderr, "Error generating output: %v\n", err)
131
+		os.Exit(1)
132
+	}
133
+}
0 134
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package api
1
+
2
+import "fmt"
3
+
4
+// This file implements Stringer for the API types for ease of debugging
5
+
6
+func (t *TestSuites) String() string {
7
+	return fmt.Sprintf("Test Suites with suites: %s.", t.Suites)
8
+}
9
+
10
+func (t *TestSuite) String() string {
11
+	childDescriptions := []string{}
12
+	for _, child := range t.Children {
13
+		childDescriptions = append(childDescriptions, child.String())
14
+	}
15
+	return fmt.Sprintf("Test Suite %q with properties: %s, %d test cases, of which %d failed and %d were skipped: %s, and children: %s.", t.Name, t.Properties, t.NumTests, t.NumFailed, t.NumSkipped, t.TestCases, childDescriptions)
16
+}
17
+
18
+func (t *TestCase) String() string {
19
+	var result, message, output string
20
+	result = "passed"
21
+	if t.SkipMessage != nil {
22
+		result = "skipped"
23
+		message = t.SkipMessage.Message
24
+	}
25
+	if t.FailureOutput != nil {
26
+		result = "failed"
27
+		message = t.FailureOutput.Message
28
+		output = t.FailureOutput.Output
29
+	}
30
+
31
+	return fmt.Sprintf("Test Case %q %s after %f seconds with message %q and output %q.", t.Name, result, t.Duration, message, output)
32
+}
33
+
34
+func (p *TestSuiteProperty) String() string {
35
+	return fmt.Sprintf("%q=%q", p.Name, p.Value)
36
+}
0 37
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package api
1
+
2
+import "time"
3
+
4
+// SetDuration sets the runtime duration of the test case
5
+func (t *TestCase) SetDuration(duration string) error {
6
+	parsedDuration, err := time.ParseDuration(duration)
7
+	if err != nil {
8
+		return err
9
+	}
10
+
11
+	// we round to the millisecond on duration
12
+	t.Duration = float64(int(parsedDuration.Seconds()*1000)) / 1000
13
+	return nil
14
+}
15
+
16
+// MarkSkipped marks the test as skipped with the given message
17
+func (t *TestCase) MarkSkipped(message string) {
18
+	t.SkipMessage = &SkipMessage{
19
+		Message: message,
20
+	}
21
+}
22
+
23
+// MarkFailed marks the test as failed with the given message and output
24
+func (t *TestCase) MarkFailed(message, output string) {
25
+	t.FailureOutput = &FailureOutput{
26
+		Message: message,
27
+		Output:  output,
28
+	}
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package api
1
+
2
+import "time"
3
+
4
+// AddProperty adds a property to the test suite
5
+func (t *TestSuite) AddProperty(name, value string) {
6
+	t.Properties = append(t.Properties, &TestSuiteProperty{Name: name, Value: value})
7
+}
8
+
9
+// AddTestCase adds a test case to the test suite and updates test suite metrics as necessary
10
+func (t *TestSuite) AddTestCase(testCase *TestCase) {
11
+	t.NumTests += 1
12
+
13
+	if testCase.SkipMessage != nil {
14
+		t.NumSkipped += 1
15
+	}
16
+
17
+	if testCase.FailureOutput != nil {
18
+		t.NumFailed += 1
19
+	}
20
+
21
+	t.Duration += testCase.Duration
22
+	// we round to the millisecond on duration
23
+	t.Duration = float64(int(t.Duration*1000)) / 1000
24
+
25
+	t.TestCases = append(t.TestCases, testCase)
26
+}
27
+
28
+// SetDuration sets the duration of the test suite if this value is not calculated by aggregating the durations
29
+// of all of the substituent test cases. This should *not* be used if the total duration of the test suite is
30
+// calculated as that sum, as AddTestCase will handle that case.
31
+func (t *TestSuite) SetDuration(duration string) error {
32
+	parsedDuration, err := time.ParseDuration(duration)
33
+	if err != nil {
34
+		return err
35
+	}
36
+
37
+	// we round to the millisecond on duration
38
+	t.Duration = float64(int(parsedDuration.Seconds()*1000)) / 1000
39
+	return nil
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,98 @@
0
+package api
1
+
2
+import "encoding/xml"
3
+
4
+// The below types are directly marshalled into XML. The types correspond to jUnit
5
+// XML schema, but do not contain all valid fields. For instance, the class name
6
+// field for test cases is omitted, as this concept does not directly apply to Go.
7
+// For XML specifications see http://help.catchsoftware.com/display/ET/JUnit+Format
8
+
9
+// TestSuites represents a flat collection of jUnit test suites.
10
+type TestSuites struct {
11
+	XMLName xml.Name `xml:"testsuites"`
12
+
13
+	// Suites are the jUnit test suites held in this collection
14
+	Suites []*TestSuite `xml:"testsuite"`
15
+}
16
+
17
+// TestSuite represents a single jUnit test suite, potentially holding child suites.
18
+type TestSuite struct {
19
+	XMLName xml.Name `xml:"testsuite"`
20
+
21
+	// Name is the name of the test suite
22
+	Name string `xml:"name,attr"`
23
+
24
+	// NumTests records the number of tests in the TestSuite
25
+	NumTests uint `xml:"tests,attr"`
26
+
27
+	// NumSkipped records the number of skipped tests in the suite
28
+	NumSkipped uint `xml:"skipped,attr"`
29
+
30
+	// NumFailed records the number of failed tests in the suite
31
+	NumFailed uint `xml:"failures,attr"`
32
+
33
+	// Duration is the time taken in seconds to run all tests in the suite
34
+	Duration float64 `xml:"time,attr"`
35
+
36
+	// Properties holds other properties of the test suite as a mapping of name to value
37
+	Properties []*TestSuiteProperty `xml:"properties,omitempty"`
38
+
39
+	// TestCases are the test cases contained in the test suite
40
+	TestCases []*TestCase `xml:"testcase"`
41
+
42
+	// Children holds nested test suites
43
+	Children []*TestSuite `xml:"testsuite"`
44
+}
45
+
46
+// TestSuiteProperty contains a mapping of a property name to a value
47
+type TestSuiteProperty struct {
48
+	XMLName xml.Name `xml:"propery"`
49
+
50
+	Name  string `xml:"name,attr"`
51
+	Value string `xml:"value,attr"`
52
+}
53
+
54
+// TestCase represents a jUnit test case
55
+type TestCase struct {
56
+	XMLName xml.Name `xml:"testcase"`
57
+
58
+	// Name is the name of the test case
59
+	Name string `xml:"name,attr"`
60
+
61
+	// Duration is the time taken in seconds to run the test
62
+	Duration float64 `xml:"time,attr"`
63
+
64
+	// SkipMessage holds the reason why the test was skipped
65
+	SkipMessage *SkipMessage `xml:"skipped"`
66
+
67
+	// FailureOutput holds the output from a failing test
68
+	FailureOutput *FailureOutput `xml:"failure"`
69
+}
70
+
71
+// SkipMessage holds a message explaining why a test was skipped
72
+type SkipMessage struct {
73
+	XMLName xml.Name `xml:"skipped"`
74
+
75
+	// Message explains why the test was skipped
76
+	Message string `xml:"message,attr"`
77
+}
78
+
79
+// FailureOutput holds the output from a failing test
80
+type FailureOutput struct {
81
+	XMLName xml.Name `xml:"failure"`
82
+
83
+	// Message holds the failure message from the test
84
+	Message string `xml:"message,attr"`
85
+
86
+	// Output holds verbose failure output from the test
87
+	Output string `xml:",chardata"`
88
+}
89
+
90
+// TestResult is the result of a test case
91
+type TestResult string
92
+
93
+const (
94
+	TestResultPass TestResult = "pass"
95
+	TestResultSkip TestResult = "skip"
96
+	TestResultFail TestResult = "fail"
97
+)
0 98
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package flat
1
+
2
+import (
3
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
4
+	"github.com/openshift/origin/tools/junitreport/pkg/builder"
5
+)
6
+
7
+// NewTestSuitesBuilder returns a new flat test suites builder. All test suites consumed
8
+// by this builder will be added to a flat list of suites - no suites will be children of other suites
9
+func NewTestSuitesBuilder() builder.TestSuitesBuilder {
10
+	return &flatTestSuitesBuilder{
11
+		testSuites: &api.TestSuites{},
12
+	}
13
+}
14
+
15
+// flatTestSuitesBuilder is a test suites builder that does not nest suites
16
+type flatTestSuitesBuilder struct {
17
+	testSuites *api.TestSuites
18
+}
19
+
20
+// AddSuite adds a test suite to the test suites collection being built
21
+func (b *flatTestSuitesBuilder) AddSuite(suite *api.TestSuite) error {
22
+	b.testSuites.Suites = append(b.testSuites.Suites, suite)
23
+	return nil
24
+}
25
+
26
+// Build releases the test suites collection being built at whatever current state it is in
27
+func (b *flatTestSuitesBuilder) Build() *api.TestSuites {
28
+	return b.testSuites
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,75 @@
0
+package flat
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+)
8
+
9
+func TestAddSuite(t *testing.T) {
10
+	var testCases = []struct {
11
+		name           string
12
+		seedSuites     *api.TestSuites
13
+		suitesToAdd    []*api.TestSuite
14
+		expectedSuites *api.TestSuites
15
+	}{
16
+		{
17
+			name: "empty",
18
+			suitesToAdd: []*api.TestSuite{
19
+				{
20
+					Name: "testSuite",
21
+				},
22
+			},
23
+			expectedSuites: &api.TestSuites{
24
+				Suites: []*api.TestSuite{
25
+					{
26
+						Name: "testSuite",
27
+					},
28
+				},
29
+			},
30
+		},
31
+		{
32
+			name: "populated",
33
+			seedSuites: &api.TestSuites{
34
+				Suites: []*api.TestSuite{
35
+					{
36
+						Name: "testSuite",
37
+					},
38
+				},
39
+			},
40
+			suitesToAdd: []*api.TestSuite{
41
+				{
42
+					Name: "testSuite2",
43
+				},
44
+			},
45
+			expectedSuites: &api.TestSuites{
46
+				Suites: []*api.TestSuite{
47
+					{
48
+						Name: "testSuite",
49
+					},
50
+					{
51
+						Name: "testSuite2",
52
+					},
53
+				},
54
+			},
55
+		},
56
+	}
57
+
58
+	for _, testCase := range testCases {
59
+		builder := NewTestSuitesBuilder()
60
+		if testCase.seedSuites != nil {
61
+			builder.(*flatTestSuitesBuilder).testSuites = testCase.seedSuites
62
+		}
63
+
64
+		for _, suite := range testCase.suitesToAdd {
65
+			if err := builder.AddSuite(suite); err != nil {
66
+				t.Errorf("%s: unexpected error adding test suite: %v", testCase.name, err)
67
+			}
68
+		}
69
+
70
+		if expected, actual := testCase.expectedSuites, builder.Build(); !reflect.DeepEqual(expected, actual) {
71
+			t.Errorf("%s: did not correctly add suites:\n\texpected:\n\t%v,\n\tgot\n\t%v", testCase.name, expected, actual)
72
+		}
73
+	}
74
+}
0 75
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package builder
1
+
2
+import "github.com/openshift/origin/tools/junitreport/pkg/api"
3
+
4
+// TestSuitesBuilder knows how to aggregate data to form a collection of test suites.
5
+type TestSuitesBuilder interface {
6
+	// AddSuite adds a test suite to the collection
7
+	AddSuite(suite *api.TestSuite) error
8
+
9
+	// Build retuns the built structure
10
+	Build() *api.TestSuites
11
+}
0 12
new file mode 100644
... ...
@@ -0,0 +1,181 @@
0
+package nested
1
+
2
+import (
3
+	"strings"
4
+
5
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
6
+	"github.com/openshift/origin/tools/junitreport/pkg/builder"
7
+	"github.com/openshift/origin/tools/junitreport/pkg/errors"
8
+)
9
+
10
+// NewTestSuitesBuilder returns a new nested test suites builder. All test suites consumed by
11
+// this builder will be added to a multitree of suites rooted at the suites with the given names.
12
+func NewTestSuitesBuilder(rootSuiteNames []string) builder.TestSuitesBuilder {
13
+	rootSuites := []*api.TestSuite{}
14
+	for _, name := range rootSuiteNames {
15
+		rootSuites = append(rootSuites, &api.TestSuite{Name: name})
16
+	}
17
+
18
+	return &nestedTestSuitesBuilder{
19
+		restrictedRoots: len(rootSuites) > 0, // i given they are the only roots allowed
20
+		testSuites: &api.TestSuites{
21
+			Suites: rootSuites,
22
+		},
23
+	}
24
+}
25
+
26
+const (
27
+	// TestSuiteNameDelimiter is the default delimeter for test suite names
28
+	TestSuiteNameDelimiter = "/"
29
+)
30
+
31
+// nestedTestSuitesBuilder is a test suites builder that nests suites under a root suite
32
+type nestedTestSuitesBuilder struct {
33
+	// restrictedRoots determines if the builder is able to add new roots to the tree or if all
34
+	// new suits are to be added only if they are leaves of the original set of roots created
35
+	// by the constructor
36
+	restrictedRoots bool
37
+
38
+	testSuites *api.TestSuites
39
+}
40
+
41
+// AddSuite adds a test suite to the test suites collection being built if the suite is not in
42
+// the collection, otherwise it overwrites the current record of the suite in the collection. In
43
+// both cases, it updates the metrics of any parent suites to include those of the new suite. If
44
+// the parent of the test suite to be added is found in the collection, the test suite is added
45
+// as a child of that suite. Otherwise, parent suites are created by successively removing one
46
+// layer of package specificity until the root name is found. For instance, if the suite named
47
+// "root/package/subpackage/subsubpackage" were to be added to an empty collection, the suites
48
+// named "root", "root/package", and "root/package/subpackage" would be created and added first,
49
+// then the suite could be added as a child of the latter parent package. If roots are restricted,
50
+// then test suites to be added are asssumed to be nested under one of the root suites created by
51
+// the constructor method and the attempted addition of a suite not rooted in those suites will
52
+// fail silently to allow for selective tree-building given a root.
53
+func (b *nestedTestSuitesBuilder) AddSuite(suite *api.TestSuite) error {
54
+	if recordedSuite := b.findSuite(suite.Name); recordedSuite != nil {
55
+		// if we are trying to add a suite that already exists, we just need to overwrite our
56
+		// current record with the data in the new suite to be added
57
+		recordedSuite.NumTests = suite.NumTests
58
+		recordedSuite.NumSkipped = suite.NumSkipped
59
+		recordedSuite.NumFailed = suite.NumFailed
60
+		recordedSuite.Duration = suite.Duration
61
+		recordedSuite.Properties = suite.Properties
62
+		recordedSuite.TestCases = suite.TestCases
63
+		recordedSuite.Children = suite.Children
64
+		return nil
65
+	}
66
+
67
+	if err := b.addToParent(suite); err != nil {
68
+		if errors.IsSuiteOutOfBoundsError(err) {
69
+			// if we were trying to add something out of bounds, we ignore the request but do not
70
+			// throw an error so we can selectively build sub-trees with a set of specified roots
71
+			return nil
72
+		}
73
+		return err
74
+	}
75
+
76
+	b.updateMetrics(suite)
77
+
78
+	return nil
79
+}
80
+
81
+// addToParent will find or create the parent for the test suite and add the given suite as a child
82
+func (b *nestedTestSuitesBuilder) addToParent(child *api.TestSuite) error {
83
+	name := child.Name
84
+	if !b.isChildOfRoots(name) && b.restrictedRoots {
85
+		// if we were asked to add a new test suite that isn't a child of any current root,
86
+		// and we aren't allowed to add new roots, we can't fulfill this request
87
+		return errors.NewSuiteOutOfBoundsError(name)
88
+	}
89
+
90
+	parentName := getParentName(name)
91
+	if len(parentName) == 0 {
92
+		// this suite does not have a parent, we just need to add it as a root
93
+		b.testSuites.Suites = append(b.testSuites.Suites, child)
94
+		return nil
95
+	}
96
+
97
+	parent := b.findSuite(parentName)
98
+	if parent == nil {
99
+		// no parent is currently registered, we need to create it and add it to the tree
100
+		parent = &api.TestSuite{
101
+			Name:     parentName,
102
+			Children: []*api.TestSuite{child},
103
+		}
104
+
105
+		return b.addToParent(parent)
106
+	}
107
+
108
+	parent.Children = append(parent.Children, child)
109
+	return nil
110
+}
111
+
112
+// getParentName returns the name of the parent package, if it exists in the multitree
113
+func getParentName(name string) string {
114
+	if !strings.Contains(name, TestSuiteNameDelimiter) {
115
+		return ""
116
+	}
117
+
118
+	delimeterIndex := strings.LastIndex(name, TestSuiteNameDelimiter)
119
+	return name[0:delimeterIndex]
120
+}
121
+
122
+func (b *nestedTestSuitesBuilder) isChildOfRoots(name string) bool {
123
+	for _, rootSuite := range b.testSuites.Suites {
124
+		if strings.HasPrefix(name, rootSuite.Name) {
125
+			return true
126
+		}
127
+	}
128
+	return false
129
+}
130
+
131
+// findSuite finds a test suite in a collection of test suites
132
+func (b *nestedTestSuitesBuilder) findSuite(name string) *api.TestSuite {
133
+	return findSuite(b.testSuites.Suites, name)
134
+}
135
+
136
+// findSuite walks a test suite tree to find a test suite with the given name
137
+func findSuite(suites []*api.TestSuite, name string) *api.TestSuite {
138
+	for _, suite := range suites {
139
+		if suite.Name == name {
140
+			return suite
141
+		}
142
+
143
+		if strings.HasPrefix(name, suite.Name) {
144
+			return findSuite(suite.Children, name)
145
+		}
146
+	}
147
+
148
+	return nil
149
+}
150
+
151
+// updateMetrics updates the metrics for all parents of a test suite
152
+func (b *nestedTestSuitesBuilder) updateMetrics(newSuite *api.TestSuite) {
153
+	updateMetrics(b.testSuites.Suites, newSuite)
154
+}
155
+
156
+// updateMetrics walks a test suite tree to update metrics of parents of the given suite
157
+func updateMetrics(suites []*api.TestSuite, newSuite *api.TestSuite) {
158
+	for _, suite := range suites {
159
+		if suite.Name == newSuite.Name || !strings.HasPrefix(newSuite.Name, suite.Name) {
160
+			// if we're considering the suite itself or another suite that is not a pure
161
+			// prefix of the new suite, we are not considering a parent suite and therefore
162
+			// do not need to update any metrics
163
+			continue
164
+		}
165
+
166
+		suite.NumTests += newSuite.NumTests
167
+		suite.NumSkipped += newSuite.NumSkipped
168
+		suite.NumFailed += newSuite.NumFailed
169
+		suite.Duration += newSuite.Duration
170
+		// we round to the millisecond on duration
171
+		suite.Duration = float64(int(suite.Duration*1000)) / 1000
172
+
173
+		updateMetrics(suite.Children, newSuite)
174
+	}
175
+}
176
+
177
+// Build releases the test suites collection being built at whatever current state it is in
178
+func (b *nestedTestSuitesBuilder) Build() *api.TestSuites {
179
+	return b.testSuites
180
+}
0 181
new file mode 100644
... ...
@@ -0,0 +1,182 @@
0
+package nested
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+)
8
+
9
+func TestGetParentName(t *testing.T) {
10
+	var testCases = []struct {
11
+		name               string
12
+		testName           string
13
+		expectedParentName string
14
+	}{
15
+		{
16
+			name:               "no parent",
17
+			testName:           "root",
18
+			expectedParentName: "",
19
+		},
20
+		{
21
+			name:               "one parent",
22
+			testName:           "root/package",
23
+			expectedParentName: "root",
24
+		},
25
+		{
26
+			name:               "many parents",
27
+			testName:           "root/package/subpackage/etc",
28
+			expectedParentName: "root/package/subpackage",
29
+		},
30
+	}
31
+
32
+	for _, testCase := range testCases {
33
+		if actual, expected := getParentName(testCase.testName), testCase.expectedParentName; actual != expected {
34
+			t.Errorf("%s: did not get correct parent name for test name: expected: %q, got %q", testCase.name, expected, actual)
35
+		}
36
+	}
37
+}
38
+
39
+func TestAddSuite(t *testing.T) {
40
+	var testCases = []struct {
41
+		name           string
42
+		rootSuiteNames []string
43
+		seedSuites     *api.TestSuites
44
+		suiteToAdd     *api.TestSuite
45
+		expectedSuites *api.TestSuites
46
+	}{
47
+		{
48
+			name: "empty adding root",
49
+			suiteToAdd: &api.TestSuite{
50
+				Name: "root",
51
+			},
52
+			expectedSuites: &api.TestSuites{
53
+				Suites: []*api.TestSuite{
54
+					{
55
+						Name: "root",
56
+					},
57
+				},
58
+			},
59
+		},
60
+		{
61
+			name: "empty adding child",
62
+			suiteToAdd: &api.TestSuite{
63
+				Name: "root/child",
64
+			},
65
+			expectedSuites: &api.TestSuites{
66
+				Suites: []*api.TestSuite{
67
+					{
68
+						Name: "root",
69
+						Children: []*api.TestSuite{
70
+							{
71
+								Name: "root/child",
72
+							},
73
+						},
74
+					},
75
+				},
76
+			},
77
+		},
78
+		{
79
+			name:           "empty with bounds, adding out of bounds",
80
+			rootSuiteNames: []string{"someotherroot"},
81
+			suiteToAdd: &api.TestSuite{
82
+				Name: "root/child",
83
+			},
84
+			expectedSuites: &api.TestSuites{
85
+				Suites: []*api.TestSuite{
86
+					{
87
+						Name: "someotherroot",
88
+					},
89
+				},
90
+			},
91
+		},
92
+		{
93
+			name: "populated adding child",
94
+			seedSuites: &api.TestSuites{
95
+				Suites: []*api.TestSuite{
96
+					{
97
+						Name: "root",
98
+					},
99
+				},
100
+			},
101
+			suiteToAdd: &api.TestSuite{
102
+				Name: "root/child",
103
+			},
104
+			expectedSuites: &api.TestSuites{
105
+				Suites: []*api.TestSuite{
106
+					{
107
+						Name: "root",
108
+						Children: []*api.TestSuite{
109
+							{
110
+								Name: "root/child",
111
+							},
112
+						},
113
+					},
114
+				},
115
+			},
116
+		},
117
+		{
118
+			name:           "empty with bounds, adding in bounds",
119
+			rootSuiteNames: []string{"root"},
120
+			suiteToAdd: &api.TestSuite{
121
+				Name: "root/child/grandchild",
122
+			},
123
+			expectedSuites: &api.TestSuites{
124
+				Suites: []*api.TestSuite{
125
+					{
126
+						Name: "root",
127
+						Children: []*api.TestSuite{
128
+							{
129
+								Name: "root/child",
130
+								Children: []*api.TestSuite{
131
+									{
132
+										Name: "root/child/grandchild",
133
+									},
134
+								},
135
+							},
136
+						},
137
+					},
138
+				},
139
+			},
140
+		},
141
+		{
142
+			name: "populated overwriting record",
143
+			seedSuites: &api.TestSuites{
144
+				Suites: []*api.TestSuite{
145
+					{
146
+						Name:     "root",
147
+						NumTests: 3,
148
+					},
149
+				},
150
+			},
151
+			suiteToAdd: &api.TestSuite{
152
+				Name:     "root",
153
+				NumTests: 4,
154
+			},
155
+			expectedSuites: &api.TestSuites{
156
+				Suites: []*api.TestSuite{
157
+					{
158
+						Name:     "root",
159
+						NumTests: 4,
160
+					},
161
+				},
162
+			},
163
+		},
164
+	}
165
+
166
+	for _, testCase := range testCases {
167
+		builder := NewTestSuitesBuilder(testCase.rootSuiteNames)
168
+
169
+		if testCase.seedSuites != nil {
170
+			builder.(*nestedTestSuitesBuilder).testSuites = testCase.seedSuites
171
+		}
172
+
173
+		if err := builder.AddSuite(testCase.suiteToAdd); err != nil {
174
+			t.Errorf("%s: unexpected error adding suite to suites: %v", testCase.name, err)
175
+		}
176
+
177
+		if actual, expected := builder.Build(), testCase.expectedSuites; !reflect.DeepEqual(actual, expected) {
178
+			t.Errorf("%s: did not get correct test suites after addition of test suite:\n\texpected:\n\t%s,\n\tgot\n\t%s", testCase.name, expected, actual)
179
+		}
180
+	}
181
+}
0 182
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package cmd
1
+
2
+import (
3
+	"bufio"
4
+	"encoding/xml"
5
+	"fmt"
6
+	"io"
7
+
8
+	"github.com/openshift/origin/tools/junitreport/pkg/builder"
9
+	"github.com/openshift/origin/tools/junitreport/pkg/builder/flat"
10
+	"github.com/openshift/origin/tools/junitreport/pkg/builder/nested"
11
+	"github.com/openshift/origin/tools/junitreport/pkg/parser"
12
+	"github.com/openshift/origin/tools/junitreport/pkg/parser/gotest"
13
+)
14
+
15
+type testSuitesBuilderType string
16
+
17
+const (
18
+	flatBuilderType   testSuitesBuilderType = "flat"
19
+	nestedBuilderType testSuitesBuilderType = "nested"
20
+)
21
+
22
+var supportedBuilderTypes = []testSuitesBuilderType{flatBuilderType, nestedBuilderType}
23
+
24
+type testParserType string
25
+
26
+const (
27
+	goTestParserType testParserType = "gotest"
28
+)
29
+
30
+var supportedTestParserTypes = []testParserType{goTestParserType}
31
+
32
+type JUnitReportOptions struct {
33
+	// BuilderType is the type of test suites builder to use
34
+	BuilderType testSuitesBuilderType
35
+
36
+	// RootSuiteNames is a list of root suites to be used for nested test suite output if
37
+	// the root suite is to be more specific than the suite name without any suite delimeters
38
+	// i.e. if `github.com/owner/repo` is to be used instead of `github.com`
39
+	RootSuiteNames []string
40
+
41
+	// ParserType is the parser type that will be used to parse test output
42
+	ParserType testParserType
43
+
44
+	// Input is the reader for the test output to be parsed
45
+	Input io.Reader
46
+
47
+	// Output is the writer for the file to which the XML is written
48
+	Output io.Writer
49
+}
50
+
51
+func (o *JUnitReportOptions) Complete(builderType, parserType string, rootSuiteNames []string) error {
52
+	switch testSuitesBuilderType(builderType) {
53
+	case flatBuilderType:
54
+		o.BuilderType = flatBuilderType
55
+	case nestedBuilderType:
56
+		o.BuilderType = nestedBuilderType
57
+	default:
58
+		return fmt.Errorf("unrecognized test suites builder type: got %s, expected one of %v", builderType, supportedBuilderTypes)
59
+	}
60
+
61
+	switch testParserType(parserType) {
62
+	case goTestParserType:
63
+		o.ParserType = goTestParserType
64
+	default:
65
+		return fmt.Errorf("unrecognized test parser type: got %s, expected one of %v", parserType, supportedTestParserTypes)
66
+	}
67
+
68
+	o.RootSuiteNames = rootSuiteNames
69
+
70
+	return nil
71
+}
72
+
73
+func (o *JUnitReportOptions) Run() error {
74
+	var builder builder.TestSuitesBuilder
75
+	switch o.BuilderType {
76
+	case flatBuilderType:
77
+		builder = flat.NewTestSuitesBuilder()
78
+	case nestedBuilderType:
79
+		builder = nested.NewTestSuitesBuilder(o.RootSuiteNames)
80
+	}
81
+
82
+	var testParser parser.TestOutputParser
83
+	switch o.ParserType {
84
+	case goTestParserType:
85
+		testParser = gotest.NewParser(builder)
86
+	}
87
+
88
+	testSuites, err := testParser.Parse(bufio.NewScanner(o.Input))
89
+	if err != nil {
90
+		return err
91
+	}
92
+
93
+	_, err = io.WriteString(o.Output, xml.Header)
94
+	if err != nil {
95
+		return fmt.Errorf("error writing XML header to file: %v", err)
96
+	}
97
+
98
+	encoder := xml.NewEncoder(o.Output)
99
+	encoder.Indent("", "\t") // no prefix, indent with tabs
100
+
101
+	if err := encoder.Encode(testSuites); err != nil {
102
+		return fmt.Errorf("error encoding test suites to XML: %v", err)
103
+	}
104
+
105
+	_, err = io.WriteString(o.Output, "\n")
106
+	if err != nil {
107
+		return fmt.Errorf("error writing last newline to file: %v", err)
108
+	}
109
+
110
+	return nil
111
+}
0 112
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package cmd
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/xml"
5
+	"fmt"
6
+	"io"
7
+
8
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
9
+)
10
+
11
+// Summarize reads the input into a TestSuites structure and summarizes the tests contained within,
12
+// bringing attention to tests that did not succeed.
13
+func Summarize(input io.Reader) (string, error) {
14
+	var testSuites api.TestSuites
15
+	if err := xml.NewDecoder(input).Decode(&testSuites); err != nil {
16
+		return "", err
17
+	}
18
+
19
+	var summary bytes.Buffer
20
+	var numTests, numFailed, numSkipped uint
21
+	var duration float64
22
+	for _, testSuite := range testSuites.Suites {
23
+		numTests += testSuite.NumTests
24
+		numFailed += testSuite.NumFailed
25
+		numSkipped += testSuite.NumSkipped
26
+		duration += testSuite.Duration
27
+	}
28
+
29
+	verb := "were"
30
+	if numSkipped == 1 {
31
+		verb = "was"
32
+	}
33
+	summary.WriteString(fmt.Sprintf("Of %d tests executed in %.3fs, %d succeeded, %d failed, and %d %s skipped.\n\n", numTests, duration, (numTests - numFailed - numSkipped), numFailed, numSkipped, verb))
34
+
35
+	for _, testSuite := range testSuites.Suites {
36
+		summarizeTests(testSuite, &summary)
37
+	}
38
+
39
+	return summary.String(), nil
40
+}
41
+
42
+func summarizeTests(testSuite *api.TestSuite, summary *bytes.Buffer) {
43
+	for _, testCase := range testSuite.TestCases {
44
+		if testCase.FailureOutput != nil {
45
+			summary.WriteString(fmt.Sprintf("In suite %q, test case %q failed:\n%s\n\n", testSuite.Name, testCase.Name, testCase.FailureOutput.Output))
46
+		}
47
+		if testCase.SkipMessage != nil {
48
+			summary.WriteString(fmt.Sprintf("In suite %q, test case %q was skipped:\n%s\n\n", testSuite.Name, testCase.Name, testCase.SkipMessage.Message))
49
+		}
50
+	}
51
+
52
+	for _, childSuite := range testSuite.Children {
53
+		summarizeTests(childSuite, summary)
54
+	}
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+package errors
1
+
2
+import "fmt"
3
+
4
+// NewSuiteOutOfBoundsError returns a new SuiteOutOfBounds error for the given suite name
5
+func NewSuiteOutOfBoundsError(name string) error {
6
+	return &suiteOutOfBoundsError{
7
+		suiteName: name,
8
+	}
9
+}
10
+
11
+// suiteOutOfBoundsError describes the failure to place a test suite into a test suite tree because the suite
12
+// in question is not a child of any suite in the tree
13
+type suiteOutOfBoundsError struct {
14
+	suiteName string
15
+}
16
+
17
+func (e *suiteOutOfBoundsError) Error() string {
18
+	return fmt.Sprintf("the test suite %q could not be placed under any existing roots in the tree", e.suiteName)
19
+}
20
+
21
+// IsSuiteOutOfBoundsError determines if the given error was raised because a suite could not be placed
22
+// in the test suite tree
23
+func IsSuiteOutOfBoundsError(err error) bool {
24
+	if err == nil {
25
+		return false
26
+	}
27
+
28
+	_, ok := err.(*suiteOutOfBoundsError)
29
+	return ok
30
+}
0 31
new file mode 100644
... ...
@@ -0,0 +1,130 @@
0
+package gotest
1
+
2
+import (
3
+	"regexp"
4
+
5
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
6
+)
7
+
8
+func newTestDataParser() testDataParser {
9
+	return testDataParser{
10
+		// testStartPattern matches the line in verbose `go test` output that marks the declaration of a test.
11
+		// The first submatch of this regex is the name of the test
12
+		testStartPattern: regexp.MustCompile(`=== RUN\s+(.+)$`),
13
+
14
+		// testResultPattern matches the line in verbose `go test` output that marks the result of a test.
15
+		// The first submatch of this regex is the result of the test (PASS, FAIL, or SKIP)
16
+		// The second submatch of this regex is the name of the test
17
+		// The third submatch of this regex is the time taken in seconds for the test to finish
18
+		testResultPattern: regexp.MustCompile(`--- (PASS|FAIL|SKIP):\s+(.+)\s+\((\d+\.\d+)(s| seconds)\)`),
19
+	}
20
+}
21
+
22
+type testDataParser struct {
23
+	testStartPattern  *regexp.Regexp
24
+	testResultPattern *regexp.Regexp
25
+}
26
+
27
+// MarksBeginning determines if the line marks the begining of a test case
28
+func (p *testDataParser) MarksBeginning(line string) bool {
29
+	return p.testStartPattern.MatchString(line)
30
+}
31
+
32
+// ExtractName extracts the name of the test case from test output line
33
+func (p *testDataParser) ExtractName(line string) (string, bool) {
34
+	if matches := p.testStartPattern.FindStringSubmatch(line); len(matches) > 0 && len(matches[1]) > 0 {
35
+		return matches[1], true
36
+	}
37
+
38
+	if matches := p.testResultPattern.FindStringSubmatch(line); len(matches) > 1 && len(matches[2]) > 0 {
39
+		return matches[2], true
40
+	}
41
+
42
+	return "", false
43
+}
44
+
45
+// ExtractResult extracts the test result from a test output line
46
+func (p *testDataParser) ExtractResult(line string) (api.TestResult, bool) {
47
+	if matches := p.testResultPattern.FindStringSubmatch(line); len(matches) > 0 && len(matches[1]) > 0 {
48
+		switch matches[1] {
49
+		case "PASS":
50
+			return api.TestResultPass, true
51
+		case "SKIP":
52
+			return api.TestResultSkip, true
53
+		case "FAIL":
54
+			return api.TestResultFail, true
55
+		}
56
+	}
57
+	return "", false
58
+}
59
+
60
+// ExtractDuration extracts the test duration from a test output line
61
+func (p *testDataParser) ExtractDuration(line string) (string, bool) {
62
+	if matches := p.testResultPattern.FindStringSubmatch(line); len(matches) > 2 && len(matches[3]) > 0 {
63
+		return matches[3] + "s", true
64
+	}
65
+	return "", false
66
+}
67
+
68
+func newTestSuiteDataParser() testSuiteDataParser {
69
+	return testSuiteDataParser{
70
+		// coverageOutputPattern matches coverage output on a single line.
71
+		// The first submatch of this regex is the percent coverage
72
+		coverageOutputPattern: regexp.MustCompile(`coverage:\s+(\d+\.\d+)\% of statements`),
73
+
74
+		// packageResultPattern matches the `go test` output for the end of a package.
75
+		// The first submatch of this regex matches the result of the test (ok or FAIL)
76
+		// The second submatch of this regex matches the name of the package
77
+		// The third submatch of this regex matches the time taken in seconds for tests in the package to finish
78
+		// The sixth (optional) submatch of this regex is the percent coverage
79
+		packageResultPattern: regexp.MustCompile(`(ok|FAIL)\s+(.+)[\s\t]+(\d+\.\d+(s| seconds))([\s\t]+coverage:\s+(\d+\.\d+)\% of statements)?`),
80
+	}
81
+}
82
+
83
+type testSuiteDataParser struct {
84
+	coverageOutputPattern *regexp.Regexp
85
+	packageResultPattern  *regexp.Regexp
86
+}
87
+
88
+// ExtractName extracts the name of the test suite from a test output line
89
+func (p *testSuiteDataParser) ExtractName(line string) (string, bool) {
90
+	if matches := p.packageResultPattern.FindStringSubmatch(line); len(matches) > 1 && len(matches[2]) > 0 {
91
+		return matches[2], true
92
+	}
93
+	return "", false
94
+}
95
+
96
+// ExtractDuration extracts the package duration from a test output line
97
+func (p *testSuiteDataParser) ExtractDuration(line string) (string, bool) {
98
+	if resultMatches := p.packageResultPattern.FindStringSubmatch(line); len(resultMatches) > 2 && len(resultMatches[3]) > 0 {
99
+		return resultMatches[3], true
100
+	}
101
+	return "", false
102
+}
103
+
104
+const (
105
+	coveragePropertyName string = "coverage.statements.pct"
106
+)
107
+
108
+// ExtractProperties extracts any metadata properties of the test suite from a test output line
109
+func (p *testSuiteDataParser) ExtractProperties(line string) (map[string]string, bool) {
110
+	// the only test suite properties that Go testing can create are coverage values, which can either
111
+	// be present on their own line or in the package result line
112
+	if matches := p.coverageOutputPattern.FindStringSubmatch(line); len(matches) > 0 && len(matches[1]) > 0 {
113
+		return map[string]string{
114
+			coveragePropertyName: matches[1],
115
+		}, true
116
+	}
117
+
118
+	if resultMatches := p.packageResultPattern.FindStringSubmatch(line); len(resultMatches) > 5 && len(resultMatches[6]) > 0 {
119
+		return map[string]string{
120
+			coveragePropertyName: resultMatches[6],
121
+		}, true
122
+	}
123
+	return map[string]string{}, false
124
+}
125
+
126
+// MarksCompletion determines if the line marks the completion of a test suite
127
+func (p *testSuiteDataParser) MarksCompletion(line string) bool {
128
+	return p.packageResultPattern.MatchString(line)
129
+}
0 130
new file mode 100644
... ...
@@ -0,0 +1,306 @@
0
+package gotest
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+)
8
+
9
+func TestMarksTestBeginning(t *testing.T) {
10
+	var testCases = []struct {
11
+		name     string
12
+		testLine string
13
+	}{
14
+		{
15
+			name:     "basic",
16
+			testLine: "=== RUN TestName",
17
+		},
18
+		{
19
+			name:     "numeric",
20
+			testLine: "=== RUN 1234",
21
+		},
22
+		{
23
+			name:     "url",
24
+			testLine: "=== RUN github.com/maintainer/repository/package/file",
25
+		},
26
+		{
27
+			name:     "failed print",
28
+			testLine: "some other text=== RUN github.com/maintainer/repository/package/file",
29
+		},
30
+	}
31
+
32
+	for _, testCase := range testCases {
33
+		parser := newTestDataParser()
34
+
35
+		if !parser.MarksBeginning(testCase.testLine) {
36
+			t.Errorf("%s: did not correctly determine that line %q marked test beginning", testCase.name, testCase.testLine)
37
+		}
38
+	}
39
+}
40
+
41
+func TestExtractTestName(t *testing.T) {
42
+	var testCases = []struct {
43
+		name         string
44
+		testLine     string
45
+		expectedName string
46
+	}{
47
+		{
48
+			name:         "basic start",
49
+			testLine:     "=== RUN TestName",
50
+			expectedName: "TestName",
51
+		},
52
+		{
53
+			name:         "numeric",
54
+			testLine:     "=== RUN 1234",
55
+			expectedName: "1234",
56
+		},
57
+		{
58
+			name:         "url",
59
+			testLine:     "=== RUN github.com/maintainer/repository/package/file",
60
+			expectedName: "github.com/maintainer/repository/package/file",
61
+		},
62
+		{
63
+			name:         "basic end",
64
+			testLine:     "--- PASS: Test (0.10 seconds)",
65
+			expectedName: "Test",
66
+		},
67
+		{
68
+			name:         "go1.5.1 timing",
69
+			testLine:     "--- PASS: TestTwo (0.03s)",
70
+			expectedName: "TestTwo",
71
+		},
72
+		{
73
+			name:         "skip",
74
+			testLine:     "--- SKIP: Test (0.10 seconds)",
75
+			expectedName: "Test",
76
+		},
77
+		{
78
+			name:         "fail",
79
+			testLine:     "--- FAIL: Test (0.10 seconds)",
80
+			expectedName: "Test",
81
+		},
82
+		{
83
+			name:         "failed print",
84
+			testLine:     "some other text--- FAIL: Test (0.10 seconds)",
85
+			expectedName: "Test",
86
+		},
87
+	}
88
+
89
+	for _, testCase := range testCases {
90
+		parser := newTestDataParser()
91
+
92
+		actual, contained := parser.ExtractName(testCase.testLine)
93
+		if !contained {
94
+			t.Errorf("%s: failed to extract name from line %q", testCase.name, testCase.testLine)
95
+		}
96
+		if testCase.expectedName != actual {
97
+			t.Errorf("%s: did not correctly extract name from line %q: expected %q, got %q", testCase.name, testCase.testLine, testCase.expectedName, actual)
98
+		}
99
+	}
100
+}
101
+
102
+func TestExtractResult(t *testing.T) {
103
+	var testCases = []struct {
104
+		name           string
105
+		testLine       string
106
+		expectedResult api.TestResult
107
+	}{
108
+		{
109
+			name:           "basic",
110
+			testLine:       "--- PASS: Test (0.10 seconds)",
111
+			expectedResult: api.TestResultPass,
112
+		},
113
+		{
114
+			name:           "go1.5.1 timing",
115
+			testLine:       "--- PASS: TestTwo (0.03s)",
116
+			expectedResult: api.TestResultPass,
117
+		},
118
+		{
119
+			name:           "skip",
120
+			testLine:       "--- SKIP: Test (0.10 seconds)",
121
+			expectedResult: api.TestResultSkip,
122
+		},
123
+		{
124
+			name:           "fail",
125
+			testLine:       "--- FAIL: Test (0.10 seconds)",
126
+			expectedResult: api.TestResultFail,
127
+		},
128
+		{
129
+			name:           "failed print",
130
+			testLine:       "some other text--- FAIL: Test (0.10 seconds)",
131
+			expectedResult: api.TestResultFail,
132
+		},
133
+	}
134
+
135
+	for _, testCase := range testCases {
136
+		parser := newTestDataParser()
137
+
138
+		actual, contained := parser.ExtractResult(testCase.testLine)
139
+		if !contained {
140
+			t.Errorf("%s: failed to extract result from line %q", testCase.name, testCase.testLine)
141
+		}
142
+		if testCase.expectedResult != actual {
143
+			t.Errorf("%s: did not correctly extract result from line %q: expected %q, got %q", testCase.name, testCase.testLine, testCase.expectedResult, actual)
144
+		}
145
+	}
146
+}
147
+
148
+func TestExtractDuration(t *testing.T) {
149
+	var testCases = []struct {
150
+		name             string
151
+		testLine         string
152
+		expectedDuration string
153
+	}{
154
+		{
155
+			name:             "basic",
156
+			testLine:         "--- PASS: Test (0.10 seconds)",
157
+			expectedDuration: "0.10s", // we make the conversion to time.Duration-parseable units internally
158
+		},
159
+		{
160
+			name:             "go1.5.1 timing",
161
+			testLine:         "--- PASS: TestTwo (0.03s)",
162
+			expectedDuration: "0.03s",
163
+		},
164
+		{
165
+			name:             "failed print",
166
+			testLine:         "some other text--- PASS: TestTwo (0.03s)",
167
+			expectedDuration: "0.03s",
168
+		},
169
+	}
170
+
171
+	for _, testCase := range testCases {
172
+		parser := newTestDataParser()
173
+
174
+		actual, contained := parser.ExtractDuration(testCase.testLine)
175
+		if !contained {
176
+			t.Errorf("%s: failed to extract duration from line %q", testCase.name, testCase.testLine)
177
+		}
178
+		if testCase.expectedDuration != actual {
179
+			t.Errorf("%s: did not correctly extract duration from line %q: expected %q, got %q", testCase.name, testCase.testLine, testCase.expectedDuration, actual)
180
+		}
181
+	}
182
+}
183
+
184
+func TestExtractSuiteName(t *testing.T) {
185
+	var testCases = []struct {
186
+		name         string
187
+		testLine     string
188
+		expectedName string
189
+	}{
190
+		{
191
+			name: "basic",
192
+			testLine: "ok  	package/name 0.160s",
193
+			expectedName: "package/name",
194
+		},
195
+		{
196
+			name: "go 1.5.1",
197
+			testLine: "ok  	package/name	0.160s",
198
+			expectedName: "package/name",
199
+		},
200
+		{
201
+			name: "numeric",
202
+			testLine: "ok  	1234 0.160s",
203
+			expectedName: "1234",
204
+		},
205
+		{
206
+			name: "url",
207
+			testLine: "ok  	github.com/maintainer/repository/package/file 0.160s",
208
+			expectedName: "github.com/maintainer/repository/package/file",
209
+		},
210
+		{
211
+			name: "with coverage",
212
+			testLine: `ok  	package/name 0.400s  coverage: 10.0% of statements`,
213
+			expectedName: "package/name",
214
+		},
215
+		{
216
+			name: "failed print",
217
+			testLine: `some other textok  	package/name 0.400s  coverage: 10.0% of statements`,
218
+			expectedName: "package/name",
219
+		},
220
+	}
221
+
222
+	for _, testCase := range testCases {
223
+		parser := newTestSuiteDataParser()
224
+
225
+		actual, contained := parser.ExtractName(testCase.testLine)
226
+		if !contained {
227
+			t.Errorf("%s: failed to extract name from line %q", testCase.name, testCase.testLine)
228
+		}
229
+		if testCase.expectedName != actual {
230
+			t.Errorf("%s: did not correctly extract suite name from line %q: expected %q, got %q", testCase.name, testCase.testLine, testCase.expectedName, actual)
231
+		}
232
+	}
233
+}
234
+
235
+func TestSuiteProperties(t *testing.T) {
236
+	var testCases = []struct {
237
+		name               string
238
+		testLine           string
239
+		expectedProperties map[string]string
240
+	}{
241
+		{
242
+			name:               "basic",
243
+			testLine:           `coverage: 10.0% of statements`,
244
+			expectedProperties: map[string]string{coveragePropertyName: "10.0"},
245
+		},
246
+		{
247
+			name: "with package declaration",
248
+			testLine: `ok  	package/name 0.400s  coverage: 10.0% of statements`,
249
+			expectedProperties: map[string]string{coveragePropertyName: "10.0"},
250
+		},
251
+		{
252
+			name:               "failed print",
253
+			testLine:           `some other textcoverage: 10.0% of statements`,
254
+			expectedProperties: map[string]string{coveragePropertyName: "10.0"},
255
+		},
256
+	}
257
+
258
+	for _, testCase := range testCases {
259
+		parser := newTestSuiteDataParser()
260
+
261
+		actual, contained := parser.ExtractProperties(testCase.testLine)
262
+		if !contained {
263
+			t.Errorf("%s: failed to extract properties from line %q", testCase.name, testCase.testLine)
264
+		}
265
+		if !reflect.DeepEqual(testCase.expectedProperties, actual) {
266
+			t.Errorf("%s: did not correctly extract properties from line %q: expected %q, got %q", testCase.name, testCase.testLine, testCase.expectedProperties, actual)
267
+		}
268
+	}
269
+}
270
+
271
+func TestMarksCompletion(t *testing.T) {
272
+	var testCases = []struct {
273
+		name     string
274
+		testLine string
275
+	}{
276
+		{
277
+			name: "basic",
278
+			testLine: "ok  	package/name 0.160s",
279
+		},
280
+		{
281
+			name: "numeric",
282
+			testLine: "ok  	1234 0.160s",
283
+		},
284
+		{
285
+			name: "url",
286
+			testLine: "ok  	github.com/maintainer/repository/package/file 0.160s",
287
+		},
288
+		{
289
+			name: "with coverage",
290
+			testLine: `ok  	package/name 0.400s  coverage: 10.0% of statements`,
291
+		},
292
+		{
293
+			name: "failed print",
294
+			testLine: `some other textok  	package/name 0.400s  coverage: 10.0% of statements`,
295
+		},
296
+	}
297
+
298
+	for _, testCase := range testCases {
299
+		parser := newTestSuiteDataParser()
300
+
301
+		if !parser.MarksCompletion(testCase.testLine) {
302
+			t.Errorf("%s: did not correctly determine that line %q marked the end of a suite", testCase.name, testCase.testLine)
303
+		}
304
+	}
305
+}
0 306
new file mode 100644
... ...
@@ -0,0 +1,106 @@
0
+package gotest
1
+
2
+import (
3
+	"bufio"
4
+	"strings"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+	"github.com/openshift/origin/tools/junitreport/pkg/builder"
8
+	"github.com/openshift/origin/tools/junitreport/pkg/parser"
9
+)
10
+
11
+// NewParser returns a new parser that's capable of parsing Go unit test output
12
+func NewParser(builder builder.TestSuitesBuilder) parser.TestOutputParser {
13
+	return &testOutputParser{
14
+		builder:     builder,
15
+		testParser:  newTestDataParser(),
16
+		suiteParser: newTestSuiteDataParser(),
17
+	}
18
+}
19
+
20
+type testOutputParser struct {
21
+	builder     builder.TestSuitesBuilder
22
+	testParser  testDataParser
23
+	suiteParser testSuiteDataParser
24
+}
25
+
26
+// Parse parses `go test -v` output into test suites. Test output from `go test -v` is not bookmarked for packages, so
27
+// the parsing strategy is to advance line-by-line, building up a slice of test cases until a package declaration is found,
28
+// at which point all tests cases are added to that package and the process can start again.
29
+func (p *testOutputParser) Parse(input *bufio.Scanner) (*api.TestSuites, error) {
30
+	currentSuite := &api.TestSuite{}
31
+	var currentTest *api.TestCase
32
+	var currentTestResult api.TestResult
33
+	var currentTestOutput []string
34
+
35
+	for input.Scan() {
36
+		line := input.Text()
37
+		isTestOutput := true
38
+
39
+		if p.testParser.MarksBeginning(line) || p.suiteParser.MarksCompletion(line) {
40
+			if currentTest != nil {
41
+				// we can't mark the test as failed or skipped until we have all of the test output, which we don't know
42
+				// we have until we see the next test or the beginning of suite output, so we add it here
43
+				output := strings.Join(currentTestOutput, "\n")
44
+				switch currentTestResult {
45
+				case api.TestResultSkip:
46
+					currentTest.MarkSkipped(output)
47
+				case api.TestResultFail:
48
+					currentTest.MarkFailed("", output)
49
+				}
50
+
51
+				currentSuite.AddTestCase(currentTest)
52
+			}
53
+			currentTest = &api.TestCase{}
54
+			currentTestResult = api.TestResultFail
55
+			currentTestOutput = []string{}
56
+		}
57
+
58
+		if name, matched := p.testParser.ExtractName(line); matched {
59
+			currentTest.Name = name
60
+		}
61
+
62
+		if result, matched := p.testParser.ExtractResult(line); matched {
63
+			currentTestResult = result
64
+		}
65
+
66
+		if duration, matched := p.testParser.ExtractDuration(line); matched {
67
+			if err := currentTest.SetDuration(duration); err != nil {
68
+				return nil, err
69
+			}
70
+		}
71
+
72
+		if properties, matched := p.suiteParser.ExtractProperties(line); matched {
73
+			for name := range properties {
74
+				currentSuite.AddProperty(name, properties[name])
75
+			}
76
+			isTestOutput = false
77
+		}
78
+
79
+		if name, matched := p.suiteParser.ExtractName(line); matched {
80
+			currentSuite.Name = name
81
+			isTestOutput = false
82
+		}
83
+
84
+		if duration, matched := p.suiteParser.ExtractDuration(line); matched {
85
+			if err := currentSuite.SetDuration(duration); err != nil {
86
+				return nil, err
87
+			}
88
+		}
89
+
90
+		if p.suiteParser.MarksCompletion(line) {
91
+			p.builder.AddSuite(currentSuite)
92
+
93
+			currentSuite = &api.TestSuite{}
94
+			currentTest = nil
95
+			isTestOutput = false
96
+		}
97
+
98
+		// we want to associate any line not directly related to a test suite with a test case to ensure we capture all output
99
+		if isTestOutput {
100
+			currentTestOutput = append(currentTestOutput, line)
101
+		}
102
+	}
103
+
104
+	return p.builder.Build(), nil
105
+}
0 106
new file mode 100644
... ...
@@ -0,0 +1,369 @@
0
+package gotest
1
+
2
+import (
3
+	"bufio"
4
+	"os"
5
+	"reflect"
6
+	"testing"
7
+
8
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
9
+	"github.com/openshift/origin/tools/junitreport/pkg/builder/flat"
10
+)
11
+
12
+// TestFlatParse tests that parsing the `go test` output in the test directory with a flat builder works as expected
13
+func TestFlatParse(t *testing.T) {
14
+	var testCases = []struct {
15
+		name           string
16
+		testFile       string
17
+		expectedSuites *api.TestSuites
18
+	}{
19
+		{
20
+			name:     "basic",
21
+			testFile: "1.txt",
22
+			expectedSuites: &api.TestSuites{
23
+				Suites: []*api.TestSuite{
24
+					{
25
+						Name:     "package/name",
26
+						NumTests: 2,
27
+						Duration: 0.16,
28
+						TestCases: []*api.TestCase{
29
+							{
30
+								Name:     "TestOne",
31
+								Duration: 0.06,
32
+							},
33
+							{
34
+								Name:     "TestTwo",
35
+								Duration: 0.1,
36
+							},
37
+						},
38
+					},
39
+				},
40
+			},
41
+		},
42
+		{
43
+			name:     "failure",
44
+			testFile: "2.txt",
45
+			expectedSuites: &api.TestSuites{
46
+				Suites: []*api.TestSuite{
47
+					{
48
+						Name:      "package/name",
49
+						NumTests:  2,
50
+						NumFailed: 1,
51
+						Duration:  0.15,
52
+						TestCases: []*api.TestCase{
53
+							{
54
+								Name:     "TestOne",
55
+								Duration: 0.02,
56
+								FailureOutput: &api.FailureOutput{
57
+									Output: `=== RUN TestOne
58
+--- FAIL: TestOne (0.02 seconds)
59
+	file_test.go:11: Error message
60
+	file_test.go:11: Longer
61
+		error
62
+		message.`,
63
+								},
64
+							},
65
+							{
66
+								Name:     "TestTwo",
67
+								Duration: 0.13,
68
+							},
69
+						},
70
+					},
71
+				},
72
+			},
73
+		},
74
+		{
75
+			name:     "skip",
76
+			testFile: "3.txt",
77
+			expectedSuites: &api.TestSuites{
78
+				Suites: []*api.TestSuite{
79
+					{
80
+						Name:       "package/name",
81
+						NumTests:   2,
82
+						NumSkipped: 1,
83
+						Duration:   0.15,
84
+						TestCases: []*api.TestCase{
85
+							{
86
+								Name:     "TestOne",
87
+								Duration: 0.02,
88
+								SkipMessage: &api.SkipMessage{
89
+									Message: `=== RUN TestOne
90
+--- SKIP: TestOne (0.02 seconds)
91
+	file_test.go:11: Skip message`,
92
+								},
93
+							},
94
+							{
95
+								Name:     "TestTwo",
96
+								Duration: 0.13,
97
+							},
98
+						},
99
+					},
100
+				},
101
+			},
102
+		},
103
+		{
104
+			name:     "go 1.4",
105
+			testFile: "4.txt",
106
+			expectedSuites: &api.TestSuites{
107
+				Suites: []*api.TestSuite{
108
+					{
109
+						Name:     "package/name",
110
+						NumTests: 2,
111
+						Duration: 0.16,
112
+						TestCases: []*api.TestCase{
113
+							{
114
+								Name:     "TestOne",
115
+								Duration: 0.06,
116
+							},
117
+							{
118
+								Name:     "TestTwo",
119
+								Duration: 0.1,
120
+							},
121
+						},
122
+					},
123
+				},
124
+			},
125
+		},
126
+		{
127
+			name:     "multiple suites",
128
+			testFile: "5.txt",
129
+			expectedSuites: &api.TestSuites{
130
+				Suites: []*api.TestSuite{
131
+					{
132
+						Name:     "package/name1",
133
+						NumTests: 2,
134
+						Duration: 0.16,
135
+						TestCases: []*api.TestCase{
136
+							{
137
+								Name:     "TestOne",
138
+								Duration: 0.06,
139
+							},
140
+							{
141
+								Name:     "TestTwo",
142
+								Duration: 0.1,
143
+							},
144
+						},
145
+					},
146
+					{
147
+						Name:      "package/name2",
148
+						NumTests:  2,
149
+						Duration:  0.15,
150
+						NumFailed: 1,
151
+						TestCases: []*api.TestCase{
152
+							{
153
+								Name:     "TestOne",
154
+								Duration: 0.02,
155
+								FailureOutput: &api.FailureOutput{
156
+									Output: `=== RUN TestOne
157
+--- FAIL: TestOne (0.02 seconds)
158
+	file_test.go:11: Error message
159
+	file_test.go:11: Longer
160
+		error
161
+		message.`,
162
+								},
163
+							},
164
+							{
165
+								Name:     "TestTwo",
166
+								Duration: 0.13,
167
+							},
168
+						},
169
+					},
170
+				},
171
+			},
172
+		},
173
+		{
174
+			name:     "coverage statement",
175
+			testFile: "6.txt",
176
+			expectedSuites: &api.TestSuites{
177
+				Suites: []*api.TestSuite{
178
+					{
179
+						Name:     "package/name",
180
+						NumTests: 2,
181
+						Duration: 0.16,
182
+						Properties: []*api.TestSuiteProperty{
183
+							{
184
+								Name:  "coverage.statements.pct",
185
+								Value: "13.37",
186
+							},
187
+						},
188
+						TestCases: []*api.TestCase{
189
+							{
190
+								Name:     "TestOne",
191
+								Duration: 0.06,
192
+							},
193
+							{
194
+								Name:     "TestTwo",
195
+								Duration: 0.1,
196
+							},
197
+						},
198
+					},
199
+				},
200
+			},
201
+		},
202
+		{
203
+			name:     "coverage statement in package result",
204
+			testFile: "7.txt",
205
+			expectedSuites: &api.TestSuites{
206
+				Suites: []*api.TestSuite{
207
+					{
208
+						Name:     "package/name",
209
+						NumTests: 2,
210
+						Duration: 0.16,
211
+						Properties: []*api.TestSuiteProperty{
212
+							{
213
+								Name:  "coverage.statements.pct",
214
+								Value: "10.0",
215
+							},
216
+						},
217
+						TestCases: []*api.TestCase{
218
+							{
219
+								Name:     "TestOne",
220
+								Duration: 0.06,
221
+							},
222
+							{
223
+								Name:     "TestTwo",
224
+								Duration: 0.1,
225
+							},
226
+						},
227
+					},
228
+				},
229
+			},
230
+		},
231
+		{
232
+			name:     "go 1.5",
233
+			testFile: "8.txt",
234
+			expectedSuites: &api.TestSuites{
235
+				Suites: []*api.TestSuite{
236
+					{
237
+						Name:     "package/name",
238
+						NumTests: 2,
239
+						Duration: 0.05,
240
+						TestCases: []*api.TestCase{
241
+							{
242
+								Name:     "TestOne",
243
+								Duration: 0.02,
244
+							},
245
+							{
246
+								Name:     "TestTwo",
247
+								Duration: 0.03,
248
+							},
249
+						},
250
+					},
251
+				},
252
+			},
253
+		},
254
+		{
255
+			name:     "nested ",
256
+			testFile: "9.txt",
257
+			expectedSuites: &api.TestSuites{
258
+				Suites: []*api.TestSuite{
259
+					{
260
+						Name:     "package/name",
261
+						NumTests: 2,
262
+						Duration: 0.05,
263
+						TestCases: []*api.TestCase{
264
+							{
265
+								Name:     "TestOne",
266
+								Duration: 0.02,
267
+							},
268
+							{
269
+								Name:     "TestTwo",
270
+								Duration: 0.03,
271
+							},
272
+						},
273
+					},
274
+					{
275
+						Name:       "package/name/nested",
276
+						NumTests:   2,
277
+						NumFailed:  1,
278
+						NumSkipped: 1,
279
+						Duration:   0.05,
280
+						TestCases: []*api.TestCase{
281
+							{
282
+								Name:     "TestOne",
283
+								Duration: 0.02,
284
+								FailureOutput: &api.FailureOutput{
285
+									Output: `=== RUN   TestOne
286
+--- FAIL: TestOne (0.02 seconds)
287
+	file_test.go:11: Error message
288
+	file_test.go:11: Longer
289
+		error
290
+		message.`,
291
+								},
292
+							},
293
+							{
294
+								Name:     "TestTwo",
295
+								Duration: 0.03,
296
+								SkipMessage: &api.SkipMessage{
297
+									Message: `=== RUN   TestTwo
298
+--- SKIP: TestTwo (0.03 seconds)
299
+	file_test.go:11: Skip message
300
+PASS`,
301
+								},
302
+							},
303
+						},
304
+					},
305
+					{
306
+						Name:     "package/other/nested",
307
+						NumTests: 2,
308
+						Duration: 0.3,
309
+						TestCases: []*api.TestCase{
310
+							{
311
+								Name:     "TestOne",
312
+								Duration: 0.1,
313
+							},
314
+							{
315
+								Name:     "TestTwo",
316
+								Duration: 0.2,
317
+							},
318
+						},
319
+					},
320
+				},
321
+			},
322
+		},
323
+		{
324
+			name:     "test case timing doesn't add to test suite timing",
325
+			testFile: "10.txt",
326
+			expectedSuites: &api.TestSuites{
327
+				Suites: []*api.TestSuite{
328
+					{
329
+						Name:     "package/name",
330
+						NumTests: 2,
331
+						Duration: 2.16,
332
+						TestCases: []*api.TestCase{
333
+							{
334
+								Name:     "TestOne",
335
+								Duration: 0.06,
336
+							},
337
+							{
338
+								Name:     "TestTwo",
339
+								Duration: 0.1,
340
+							},
341
+						},
342
+					},
343
+				},
344
+			},
345
+		},
346
+	}
347
+
348
+	for _, testCase := range testCases {
349
+		parser := NewParser(flat.NewTestSuitesBuilder())
350
+
351
+		testFile := "./../../../test/gotest/testdata/" + testCase.testFile
352
+
353
+		reader, err := os.Open(testFile)
354
+		if err != nil {
355
+			t.Errorf("%s: unexpected error opening file %q: %v", testCase.name, testFile, err)
356
+			continue
357
+		}
358
+		testSuites, err := parser.Parse(bufio.NewScanner(reader))
359
+		if err != nil {
360
+			t.Errorf("%s: unexpected error parsing file: %v", testCase.name, err)
361
+			continue
362
+		}
363
+
364
+		if !reflect.DeepEqual(testSuites, testCase.expectedSuites) {
365
+			t.Errorf("%s: did not produce the correct test suites from file:\n\texpected:\n\t%v,\n\tgot\n\t%v", testCase.name, testCase.expectedSuites, testSuites)
366
+		}
367
+	}
368
+}
0 369
new file mode 100644
... ...
@@ -0,0 +1,486 @@
0
+package gotest
1
+
2
+import (
3
+	"bufio"
4
+	"fmt"
5
+	"os"
6
+	"reflect"
7
+	"testing"
8
+
9
+	"k8s.io/kubernetes/pkg/util"
10
+
11
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
12
+	"github.com/openshift/origin/tools/junitreport/pkg/builder/nested"
13
+)
14
+
15
+// TestNestedParse tests that parsing the `go test` output in the test directory with a nested builder works as expected
16
+func TestNestedParse(t *testing.T) {
17
+	var testCases = []struct {
18
+		name           string
19
+		testFile       string
20
+		rootSuiteNames []string
21
+		expectedSuites *api.TestSuites
22
+	}{
23
+		{
24
+			name:     "basic",
25
+			testFile: "1.txt",
26
+			expectedSuites: &api.TestSuites{
27
+				Suites: []*api.TestSuite{
28
+					{
29
+						Name:     "package",
30
+						NumTests: 2,
31
+						Duration: 0.16,
32
+						Children: []*api.TestSuite{
33
+							{
34
+								Name:     "package/name",
35
+								NumTests: 2,
36
+								Duration: 0.16,
37
+								TestCases: []*api.TestCase{
38
+									{
39
+										Name:     "TestOne",
40
+										Duration: 0.06,
41
+									},
42
+									{
43
+										Name:     "TestTwo",
44
+										Duration: 0.1,
45
+									},
46
+								},
47
+							},
48
+						},
49
+					},
50
+				},
51
+			},
52
+		},
53
+		{
54
+			name:           "basic with restricted root",
55
+			testFile:       "1.txt",
56
+			rootSuiteNames: []string{"package/name"},
57
+			expectedSuites: &api.TestSuites{
58
+				Suites: []*api.TestSuite{
59
+					{
60
+						Name:     "package/name",
61
+						NumTests: 2,
62
+						Duration: 0.16,
63
+						TestCases: []*api.TestCase{
64
+							{
65
+								Name:     "TestOne",
66
+								Duration: 0.06,
67
+							},
68
+							{
69
+								Name:     "TestTwo",
70
+								Duration: 0.1,
71
+							},
72
+						},
73
+					},
74
+				},
75
+			},
76
+		},
77
+		{
78
+			name:     "failure",
79
+			testFile: "2.txt",
80
+			expectedSuites: &api.TestSuites{
81
+				Suites: []*api.TestSuite{
82
+					{
83
+						Name:      "package",
84
+						NumTests:  2,
85
+						NumFailed: 1,
86
+						Duration:  0.15,
87
+						Children: []*api.TestSuite{
88
+							{
89
+								Name:      "package/name",
90
+								NumTests:  2,
91
+								NumFailed: 1,
92
+								Duration:  0.15,
93
+								TestCases: []*api.TestCase{
94
+									{
95
+										Name:     "TestOne",
96
+										Duration: 0.02,
97
+										FailureOutput: &api.FailureOutput{
98
+											Output: `=== RUN TestOne
99
+--- FAIL: TestOne (0.02 seconds)
100
+	file_test.go:11: Error message
101
+	file_test.go:11: Longer
102
+		error
103
+		message.`,
104
+										},
105
+									},
106
+									{
107
+										Name:     "TestTwo",
108
+										Duration: 0.13,
109
+									},
110
+								},
111
+							},
112
+						},
113
+					},
114
+				},
115
+			},
116
+		},
117
+		{
118
+			name:     "skip",
119
+			testFile: "3.txt",
120
+			expectedSuites: &api.TestSuites{
121
+				Suites: []*api.TestSuite{
122
+					{
123
+						Name:       "package",
124
+						NumTests:   2,
125
+						NumSkipped: 1,
126
+						Duration:   0.15,
127
+						Children: []*api.TestSuite{
128
+							{
129
+								Name:       "package/name",
130
+								NumTests:   2,
131
+								NumSkipped: 1,
132
+								Duration:   0.15,
133
+								TestCases: []*api.TestCase{
134
+									{
135
+										Name:     "TestOne",
136
+										Duration: 0.02,
137
+										SkipMessage: &api.SkipMessage{
138
+											Message: `=== RUN TestOne
139
+--- SKIP: TestOne (0.02 seconds)
140
+	file_test.go:11: Skip message`,
141
+										},
142
+									},
143
+									{
144
+										Name:     "TestTwo",
145
+										Duration: 0.13,
146
+									},
147
+								},
148
+							},
149
+						},
150
+					},
151
+				},
152
+			},
153
+		},
154
+		{
155
+			name:     "go 1.4",
156
+			testFile: "4.txt",
157
+			expectedSuites: &api.TestSuites{
158
+				Suites: []*api.TestSuite{
159
+					{
160
+						Name:     "package",
161
+						NumTests: 2,
162
+						Duration: 0.16,
163
+						Children: []*api.TestSuite{
164
+							{
165
+								Name:     "package/name",
166
+								NumTests: 2,
167
+								Duration: 0.16,
168
+								TestCases: []*api.TestCase{
169
+									{
170
+										Name:     "TestOne",
171
+										Duration: 0.06,
172
+									},
173
+									{
174
+										Name:     "TestTwo",
175
+										Duration: 0.1,
176
+									},
177
+								},
178
+							},
179
+						},
180
+					},
181
+				},
182
+			},
183
+		},
184
+		{
185
+			name:     "multiple suites",
186
+			testFile: "5.txt",
187
+			expectedSuites: &api.TestSuites{
188
+				Suites: []*api.TestSuite{
189
+					{
190
+						Name:      "package",
191
+						NumTests:  4,
192
+						NumFailed: 1,
193
+						Duration:  0.31,
194
+						Children: []*api.TestSuite{
195
+							{
196
+								Name:     "package/name1",
197
+								NumTests: 2,
198
+								Duration: 0.16,
199
+								TestCases: []*api.TestCase{
200
+									{
201
+										Name:     "TestOne",
202
+										Duration: 0.06,
203
+									},
204
+									{
205
+										Name:     "TestTwo",
206
+										Duration: 0.1,
207
+									},
208
+								},
209
+							},
210
+							{
211
+								Name:      "package/name2",
212
+								NumTests:  2,
213
+								Duration:  0.15,
214
+								NumFailed: 1,
215
+								TestCases: []*api.TestCase{
216
+									{
217
+										Name:     "TestOne",
218
+										Duration: 0.02,
219
+										FailureOutput: &api.FailureOutput{
220
+											Output: `=== RUN TestOne
221
+--- FAIL: TestOne (0.02 seconds)
222
+	file_test.go:11: Error message
223
+	file_test.go:11: Longer
224
+		error
225
+		message.`,
226
+										},
227
+									},
228
+									{
229
+										Name:     "TestTwo",
230
+										Duration: 0.13,
231
+									},
232
+								},
233
+							},
234
+						},
235
+					},
236
+				},
237
+			},
238
+		},
239
+		{
240
+			name:     "coverage statement",
241
+			testFile: "6.txt",
242
+			expectedSuites: &api.TestSuites{
243
+				Suites: []*api.TestSuite{
244
+					{
245
+						Name:     "package",
246
+						NumTests: 2,
247
+						Duration: 0.16,
248
+						Children: []*api.TestSuite{
249
+							{
250
+								Name:     "package/name",
251
+								NumTests: 2,
252
+								Duration: 0.16,
253
+								Properties: []*api.TestSuiteProperty{
254
+									{
255
+										Name:  "coverage.statements.pct",
256
+										Value: "13.37",
257
+									},
258
+								},
259
+								TestCases: []*api.TestCase{
260
+									{
261
+										Name:     "TestOne",
262
+										Duration: 0.06,
263
+									},
264
+									{
265
+										Name:     "TestTwo",
266
+										Duration: 0.1,
267
+									},
268
+								},
269
+							},
270
+						},
271
+					},
272
+				},
273
+			},
274
+		},
275
+		{
276
+			name:     "coverage statement in package result",
277
+			testFile: "7.txt",
278
+			expectedSuites: &api.TestSuites{
279
+				Suites: []*api.TestSuite{
280
+					{
281
+						Name:     "package",
282
+						NumTests: 2,
283
+						Duration: 0.16,
284
+						Children: []*api.TestSuite{
285
+							{
286
+								Name:     "package/name",
287
+								NumTests: 2,
288
+								Duration: 0.16,
289
+								Properties: []*api.TestSuiteProperty{
290
+									{
291
+										Name:  "coverage.statements.pct",
292
+										Value: "10.0",
293
+									},
294
+								},
295
+								TestCases: []*api.TestCase{
296
+									{
297
+										Name:     "TestOne",
298
+										Duration: 0.06,
299
+									},
300
+									{
301
+										Name:     "TestTwo",
302
+										Duration: 0.1,
303
+									},
304
+								},
305
+							},
306
+						},
307
+					},
308
+				},
309
+			},
310
+		},
311
+		{
312
+			name:     "go 1.5",
313
+			testFile: "8.txt",
314
+			expectedSuites: &api.TestSuites{
315
+				Suites: []*api.TestSuite{
316
+					{
317
+						Name:     "package",
318
+						NumTests: 2,
319
+						Duration: 0.05,
320
+						Children: []*api.TestSuite{
321
+							{
322
+								Name:     "package/name",
323
+								NumTests: 2,
324
+								Duration: 0.05,
325
+								TestCases: []*api.TestCase{
326
+									{
327
+										Name:     "TestOne",
328
+										Duration: 0.02,
329
+									},
330
+									{
331
+										Name:     "TestTwo",
332
+										Duration: 0.03,
333
+									},
334
+								},
335
+							},
336
+						},
337
+					},
338
+				},
339
+			},
340
+		},
341
+		{
342
+			name:     "nested ",
343
+			testFile: "9.txt",
344
+			expectedSuites: &api.TestSuites{
345
+				Suites: []*api.TestSuite{
346
+					{
347
+						Name:       "package",
348
+						NumTests:   6,
349
+						NumFailed:  1,
350
+						NumSkipped: 1,
351
+						Duration:   0.4,
352
+						Children: []*api.TestSuite{
353
+							{
354
+								Name:       "package/name",
355
+								NumTests:   4,
356
+								NumFailed:  1,
357
+								NumSkipped: 1,
358
+								Duration:   0.1,
359
+								TestCases: []*api.TestCase{
360
+									{
361
+										Name:     "TestOne",
362
+										Duration: 0.02,
363
+									},
364
+									{
365
+										Name:     "TestTwo",
366
+										Duration: 0.03,
367
+									},
368
+								},
369
+								Children: []*api.TestSuite{
370
+
371
+									{
372
+										Name:       "package/name/nested",
373
+										NumTests:   2,
374
+										NumFailed:  1,
375
+										NumSkipped: 1,
376
+										Duration:   0.05,
377
+										TestCases: []*api.TestCase{
378
+											{
379
+												Name:     "TestOne",
380
+												Duration: 0.02,
381
+												FailureOutput: &api.FailureOutput{
382
+													Output: `=== RUN   TestOne
383
+--- FAIL: TestOne (0.02 seconds)
384
+	file_test.go:11: Error message
385
+	file_test.go:11: Longer
386
+		error
387
+		message.`,
388
+												},
389
+											},
390
+											{
391
+												Name:     "TestTwo",
392
+												Duration: 0.03,
393
+												SkipMessage: &api.SkipMessage{
394
+													Message: `=== RUN   TestTwo
395
+--- SKIP: TestTwo (0.03 seconds)
396
+	file_test.go:11: Skip message
397
+PASS`, // we include this line greedily even though it does not belong to the test
398
+												},
399
+											},
400
+										},
401
+									},
402
+								},
403
+							},
404
+							{
405
+								Name:     "package/other",
406
+								NumTests: 2,
407
+								Duration: 0.3,
408
+								Children: []*api.TestSuite{
409
+
410
+									{
411
+										Name:     "package/other/nested",
412
+										NumTests: 2,
413
+										Duration: 0.3,
414
+										TestCases: []*api.TestCase{
415
+											{
416
+												Name:     "TestOne",
417
+												Duration: 0.1,
418
+											},
419
+											{
420
+												Name:     "TestTwo",
421
+												Duration: 0.2,
422
+											},
423
+										},
424
+									},
425
+								},
426
+							},
427
+						},
428
+					},
429
+				},
430
+			},
431
+		},
432
+		{
433
+			name:     "test case timing doesn't add to test suite timing",
434
+			testFile: "10.txt",
435
+			expectedSuites: &api.TestSuites{
436
+				Suites: []*api.TestSuite{
437
+					{
438
+						Name:     "package",
439
+						NumTests: 2,
440
+						Duration: 2.16,
441
+						Children: []*api.TestSuite{
442
+							{
443
+								Name:     "package/name",
444
+								NumTests: 2,
445
+								Duration: 2.16,
446
+								TestCases: []*api.TestCase{
447
+									{
448
+										Name:     "TestOne",
449
+										Duration: 0.06,
450
+									},
451
+									{
452
+										Name:     "TestTwo",
453
+										Duration: 0.1,
454
+									},
455
+								},
456
+							},
457
+						},
458
+					},
459
+				},
460
+			},
461
+		},
462
+	}
463
+
464
+	for _, testCase := range testCases {
465
+		parser := NewParser(nested.NewTestSuitesBuilder(testCase.rootSuiteNames))
466
+
467
+		testFile := "./../../../test/gotest/testdata/" + testCase.testFile
468
+
469
+		reader, err := os.Open(testFile)
470
+		if err != nil {
471
+			t.Errorf("%s: unexpected error opening file %q: %v", testCase.name, testFile, err)
472
+			continue
473
+		}
474
+		testSuites, err := parser.Parse(bufio.NewScanner(reader))
475
+		if err != nil {
476
+			t.Errorf("%s: unexpected error parsing file: %v", testCase.name, err)
477
+			continue
478
+		}
479
+
480
+		if !reflect.DeepEqual(testSuites, testCase.expectedSuites) {
481
+			fmt.Println(util.ObjectGoPrintDiff(testSuites, testCase.expectedSuites))
482
+			t.Errorf("%s: did not produce the correct test suites from file:\n\texpected:\n\t%v,\n\tgot\n\t%v", testCase.name, testCase.expectedSuites, testSuites)
483
+		}
484
+	}
485
+}
0 486
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package parser
1
+
2
+import (
3
+	"bufio"
4
+
5
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
6
+)
7
+
8
+// TestOutputParser knows how to parse test output to create a collection of test suites
9
+type TestOutputParser interface {
10
+	Parse(input *bufio.Scanner) (*api.TestSuites, error)
11
+}
0 12
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+package stack
1
+
2
+import "github.com/openshift/origin/tools/junitreport/pkg/api"
3
+
4
+// TestDataParser knows how to take raw test data and extract the useful information from it
5
+type TestDataParser interface {
6
+	// MarksBeginning determines if the line marks the begining of a test case
7
+	MarksBeginning(line string) bool
8
+
9
+	// ExtractName extracts the name of the test case from test output lines
10
+	ExtractName(line string) (name string, succeeded bool)
11
+
12
+	// ExtractResult extracts the test result from a test output line
13
+	ExtractResult(line string) (result api.TestResult, succeeded bool)
14
+
15
+	// ExtractDuration extracts the test duration from a test output line
16
+	ExtractDuration(line string) (duration string, succeeded bool)
17
+
18
+	// ExtractMessage extracts a message (e.g. for signalling why a failure or skip occured) from a test output line
19
+	ExtractMessage(line string) (message string, succeeded bool)
20
+
21
+	// MarksCompletion determines if the line marks the completion of a test case
22
+	MarksCompletion(line string) bool
23
+}
24
+
25
+// TestSuiteDataParser knows how to take raw test suite data and extract the useful information from it
26
+type TestSuiteDataParser interface {
27
+	// MarksBeginning determines if the line marks the begining of a test suite
28
+	MarksBeginning(line string) bool
29
+
30
+	// ExtractName extracts the name of the test suite from a test output line
31
+	ExtractName(line string) (name string, succeeded bool)
32
+
33
+	// ExtractProperties extracts any metadata properties of the test suite from a test output line
34
+	ExtractProperties(line string) (properties map[string]string, succeeded bool)
35
+
36
+	// MarksCompletion determines if the line marks the completion of a test suite
37
+	MarksCompletion(line string) bool
38
+}
0 39
new file mode 100644
... ...
@@ -0,0 +1,125 @@
0
+package stack
1
+
2
+import (
3
+	"bufio"
4
+	"strings"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+	"github.com/openshift/origin/tools/junitreport/pkg/builder"
8
+	"github.com/openshift/origin/tools/junitreport/pkg/parser"
9
+)
10
+
11
+// NewParser returns a new parser that's capable of parsing Go unit test output
12
+func NewParser(builder builder.TestSuitesBuilder, testParser TestDataParser, suiteParser TestSuiteDataParser) parser.TestOutputParser {
13
+	return &testOutputParser{
14
+		builder:     builder,
15
+		testParser:  testParser,
16
+		suiteParser: suiteParser,
17
+	}
18
+}
19
+
20
+// testOutputParser uses a stack to parse test output. Critical assumptions that this parser makes are:
21
+//   1 - packages may be nested but tests may not
22
+//   2 - no package declarations will occur within the boundaries of a test
23
+//   3 - all tests and packages are fully bounded by a start and result line
24
+//   4 - if a package or test declaration occurs after the start of a package but before it's result,
25
+//       the sub-package's or member test's result line will occur before that of the parent package
26
+//       i.e. any test or package overlap will necessarily mean that one package's lines are a superset
27
+//       of any lines of tests or other packages overlapping with it
28
+//   5 - any text in the input file that doesn't match the parser regex is necessarily the output of the
29
+//       current test being built
30
+type testOutputParser struct {
31
+	builder builder.TestSuitesBuilder
32
+
33
+	testParser  TestDataParser
34
+	suiteParser TestSuiteDataParser
35
+}
36
+
37
+// Parse parses output syntax of a specific class, the assumptions of which are outlined in the struct definition.
38
+// The specific boundary markers and metadata encodings are free to vary as long as regex can be build to extract them
39
+// from test output.
40
+func (p *testOutputParser) Parse(input *bufio.Scanner) (*api.TestSuites, error) {
41
+	inProgress := NewTestSuiteStack()
42
+
43
+	var currentTest *api.TestCase
44
+	var currentResult api.TestResult
45
+	var currentOutput []string
46
+	var currentMessage string
47
+
48
+	for input.Scan() {
49
+		line := input.Text()
50
+		isTestOutput := true
51
+
52
+		if p.testParser.MarksBeginning(line) {
53
+			currentTest = &api.TestCase{}
54
+			currentResult = api.TestResultFail
55
+			currentOutput = []string{}
56
+			currentMessage = ""
57
+		}
58
+
59
+		if name, contained := p.testParser.ExtractName(line); contained {
60
+			currentTest.Name = name
61
+		}
62
+
63
+		if result, contained := p.testParser.ExtractResult(line); contained {
64
+			currentResult = result
65
+		}
66
+
67
+		if duration, contained := p.testParser.ExtractDuration(line); contained {
68
+			if err := currentTest.SetDuration(duration); err != nil {
69
+				return nil, err
70
+			}
71
+		}
72
+
73
+		if message, contained := p.testParser.ExtractMessage(line); contained {
74
+			currentMessage = message
75
+		}
76
+
77
+		if p.testParser.MarksCompletion(line) {
78
+			// if we have finished the current test case, we finalize our current test, add it to the package
79
+			// at the head of our in progress package stack, and clear our current test record.
80
+			output := strings.Join(currentOutput, "\n")
81
+
82
+			switch currentResult {
83
+			case api.TestResultSkip:
84
+				currentTest.MarkSkipped(currentMessage)
85
+			case api.TestResultFail:
86
+				currentTest.MarkFailed(currentMessage, output)
87
+			}
88
+
89
+			inProgress.Peek().AddTestCase(currentTest)
90
+			currentTest = &api.TestCase{}
91
+		}
92
+
93
+		if p.suiteParser.MarksBeginning(line) {
94
+			// if we encounter the beginning of a suite, we create a new suite to be considered and
95
+			// add it to the head of our in progress package stack
96
+			inProgress.Push(&api.TestSuite{})
97
+			isTestOutput = false
98
+		}
99
+
100
+		if name, contained := p.suiteParser.ExtractName(line); contained {
101
+			inProgress.Peek().Name = name
102
+			isTestOutput = false
103
+		}
104
+
105
+		if properties, contained := p.suiteParser.ExtractProperties(line); contained {
106
+			for propertyName := range properties {
107
+				inProgress.Peek().AddProperty(propertyName, properties[propertyName])
108
+			}
109
+			isTestOutput = false
110
+		}
111
+
112
+		if p.suiteParser.MarksCompletion(line) {
113
+			// if we encounter the end of a suite, we remove the suite at the head of the in progress stack
114
+			p.builder.AddSuite(inProgress.Pop())
115
+			isTestOutput = false
116
+		}
117
+
118
+		// we want to associate every line other than those directly involved with test suites as output of a test case
119
+		if isTestOutput {
120
+			currentOutput = append(currentOutput, line)
121
+		}
122
+	}
123
+	return p.builder.Build(), nil
124
+}
0 125
new file mode 100644
... ...
@@ -0,0 +1,73 @@
0
+package stack
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
6
+)
7
+
8
+// TestSuiteStack is a data structure that holds api.TestSuite objects in a LIFO
9
+type TestSuiteStack interface {
10
+	// Push adds the testSuite to the top of the LIFO
11
+	Push(pkg *api.TestSuite)
12
+	// Pop removes the head of the LIFO and returns it
13
+	Pop() *api.TestSuite
14
+	// Peek returns a reference to the head of the LIFO without removing it
15
+	Peek() *api.TestSuite
16
+	// IsEmpty determines if the stack has any members
17
+	IsEmpty() bool
18
+}
19
+
20
+// NewTestSuiteStack returns a new TestSuiteStack
21
+func NewTestSuiteStack() TestSuiteStack {
22
+	return &testSuiteStack{
23
+		head: nil,
24
+	}
25
+}
26
+
27
+// testSuiteStack is an implementation of a TestSuiteStack using a linked list
28
+type testSuiteStack struct {
29
+	head *testSuiteNode
30
+}
31
+
32
+// Push adds the testSuite to the top of the LIFO
33
+func (s *testSuiteStack) Push(data *api.TestSuite) {
34
+	newNode := &testSuiteNode{
35
+		Member: data,
36
+		Next:   s.head,
37
+	}
38
+	s.head = newNode
39
+}
40
+
41
+// Pop removes the head of the LIFO and returns it
42
+func (s *testSuiteStack) Pop() *api.TestSuite {
43
+	if s.IsEmpty() {
44
+		return nil
45
+	}
46
+	oldNode := s.head
47
+	s.head = s.head.Next
48
+	return oldNode.Member
49
+}
50
+
51
+// Peek returns a reference to the head of the LIFO without removing it
52
+func (s *testSuiteStack) Peek() *api.TestSuite {
53
+	if s.IsEmpty() {
54
+		return nil
55
+	}
56
+	return s.head.Member
57
+}
58
+
59
+// IsEmpty determines if the stack has any members
60
+func (s *testSuiteStack) IsEmpty() bool {
61
+	return s.head == nil
62
+}
63
+
64
+// testSuiteNode is a node in a singly-linked list
65
+type testSuiteNode struct {
66
+	Member *api.TestSuite
67
+	Next   *testSuiteNode
68
+}
69
+
70
+func (n *testSuiteNode) String() string {
71
+	return fmt.Sprintf("{Member: %s, Next: %s}", n.Member, n.Next.String())
72
+}
0 73
new file mode 100644
... ...
@@ -0,0 +1,179 @@
0
+package stack
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
7
+)
8
+
9
+func TestPush(t *testing.T) {
10
+	var testCases = []struct {
11
+		name            string
12
+		stackSeed       *testSuiteNode
13
+		testSuiteToPush *api.TestSuite
14
+		expectedStack   *testSuiteNode
15
+	}{
16
+		{
17
+			name:            "push on empty stack",
18
+			stackSeed:       nil,
19
+			testSuiteToPush: newTestSuite("test"),
20
+			expectedStack:   newTestSuiteNode(newTestSuite("test"), nil),
21
+		},
22
+		{
23
+			name:            "push on existing stack",
24
+			stackSeed:       newTestSuiteNode(newTestSuite("test"), nil),
25
+			testSuiteToPush: newTestSuite("test2"),
26
+			expectedStack:   newTestSuiteNode(newTestSuite("test2"), newTestSuiteNode(newTestSuite("test"), nil)),
27
+		},
28
+		{
29
+			name:            "push on deep stack",
30
+			stackSeed:       newTestSuiteNode(newTestSuite("test2"), newTestSuiteNode(newTestSuite("test3"), nil)),
31
+			testSuiteToPush: newTestSuite("test1"),
32
+			expectedStack:   newTestSuiteNode(newTestSuite("test1"), newTestSuiteNode(newTestSuite("test2"), newTestSuiteNode(newTestSuite("test3"), nil))),
33
+		},
34
+	}
35
+
36
+	for _, testCase := range testCases {
37
+		testStack := &testSuiteStack{
38
+			head: testCase.stackSeed,
39
+		}
40
+		testStack.Push(testCase.testSuiteToPush)
41
+
42
+		if !reflect.DeepEqual(testStack.head, testCase.expectedStack) {
43
+			t.Errorf("%s: did not get correct stack state after push:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedStack, testStack.head)
44
+		}
45
+	}
46
+}
47
+
48
+func TestPop(t *testing.T) {
49
+	var testCases = []struct {
50
+		name              string
51
+		stackSeed         *testSuiteNode
52
+		expectedTestSuite *api.TestSuite
53
+		expectedStack     *testSuiteNode
54
+	}{
55
+		{
56
+			name:              "pop on empty stack",
57
+			stackSeed:         nil,
58
+			expectedTestSuite: nil,
59
+			expectedStack:     nil,
60
+		},
61
+		{
62
+			name:              "pop on existing stack",
63
+			stackSeed:         newTestSuiteNode(newTestSuite("test"), nil),
64
+			expectedTestSuite: newTestSuite("test"),
65
+			expectedStack:     nil,
66
+		},
67
+		{
68
+			name:              "pop on deep stack",
69
+			stackSeed:         newTestSuiteNode(newTestSuite("test"), newTestSuiteNode(newTestSuite("test2"), nil)),
70
+			expectedTestSuite: newTestSuite("test"),
71
+			expectedStack:     newTestSuiteNode(newTestSuite("test2"), nil),
72
+		},
73
+	}
74
+
75
+	for _, testCase := range testCases {
76
+		testStack := &testSuiteStack{
77
+			head: testCase.stackSeed,
78
+		}
79
+		testSuite := testStack.Pop()
80
+		if !reflect.DeepEqual(testSuite, testCase.expectedTestSuite) {
81
+			t.Errorf("%s: did not get correct package from pop:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedTestSuite, testSuite)
82
+		}
83
+		if !reflect.DeepEqual(testStack.head, testCase.expectedStack) {
84
+			t.Errorf("%s: did not get correct stack state after pop:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedStack, testStack.head)
85
+		}
86
+	}
87
+}
88
+
89
+func TestPeek(t *testing.T) {
90
+	var testCases = []struct {
91
+		name              string
92
+		stackSeed         *testSuiteNode
93
+		expectedTestSuite *api.TestSuite
94
+		expectedStack     *testSuiteNode
95
+	}{
96
+		{
97
+			name:              "peek on empty stack",
98
+			stackSeed:         nil,
99
+			expectedTestSuite: nil,
100
+			expectedStack:     nil,
101
+		},
102
+		{
103
+			name:              "peek on existing stack",
104
+			stackSeed:         newTestSuiteNode(newTestSuite("test"), nil),
105
+			expectedTestSuite: newTestSuite("test"),
106
+			expectedStack:     newTestSuiteNode(newTestSuite("test"), nil),
107
+		},
108
+		{
109
+			name:              "peek on deep stack",
110
+			stackSeed:         newTestSuiteNode(newTestSuite("test"), newTestSuiteNode(newTestSuite("test2"), nil)),
111
+			expectedTestSuite: newTestSuite("test"),
112
+			expectedStack:     newTestSuiteNode(newTestSuite("test"), newTestSuiteNode(newTestSuite("test2"), nil)),
113
+		},
114
+	}
115
+
116
+	for _, testCase := range testCases {
117
+		testStack := &testSuiteStack{
118
+			head: testCase.stackSeed,
119
+		}
120
+		testSuite := testStack.Peek()
121
+		if !reflect.DeepEqual(testSuite, testCase.expectedTestSuite) {
122
+			t.Errorf("%s: did not get correct package from pop:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedTestSuite, testSuite)
123
+		}
124
+		if !reflect.DeepEqual(testStack.head, testCase.expectedStack) {
125
+			t.Errorf("%s: did not get correct stack state after pop:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedStack, testStack.head)
126
+		}
127
+	}
128
+}
129
+
130
+func TestIsEmpty(t *testing.T) {
131
+	var testCases = []struct {
132
+		name          string
133
+		stackSeed     *testSuiteNode
134
+		expectedState bool
135
+		expectedStack *testSuiteNode
136
+	}{
137
+		{
138
+			name:          "isempty on empty stack",
139
+			stackSeed:     nil,
140
+			expectedState: true,
141
+			expectedStack: nil,
142
+		},
143
+		{
144
+			name:          "isempty on existing stack",
145
+			stackSeed:     newTestSuiteNode(newTestSuite("test"), nil),
146
+			expectedState: false,
147
+			expectedStack: newTestSuiteNode(newTestSuite("test"), nil),
148
+		},
149
+	}
150
+
151
+	for _, testCase := range testCases {
152
+		testStack := &testSuiteStack{
153
+			head: testCase.stackSeed,
154
+		}
155
+		state := testStack.IsEmpty()
156
+
157
+		if state != testCase.expectedState {
158
+			t.Errorf("%s: did not get correct stack emptiness after push: expected: %t got: %t\n", testCase.name, testCase.expectedState, state)
159
+		}
160
+
161
+		if !reflect.DeepEqual(testStack.head, testCase.expectedStack) {
162
+			t.Errorf("%s: did not get correct stack state after push:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", testCase.name, testCase.expectedStack, testStack.head)
163
+		}
164
+	}
165
+}
166
+
167
+func newTestSuite(name string) *api.TestSuite {
168
+	return &api.TestSuite{
169
+		Name: name,
170
+	}
171
+}
172
+
173
+func newTestSuiteNode(testSuite *api.TestSuite, next *testSuiteNode) *testSuiteNode {
174
+	return &testSuiteNode{
175
+		Member: testSuite,
176
+		Next:   next,
177
+	}
178
+}
0 179
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="2.16">
3
+		<testcase name="TestOne" time="0.06"></testcase>
4
+		<testcase name="TestTwo" time="0.1"></testcase>
5
+	</testsuite>
6
+</testsuites>
0 7
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="2.16">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="2.16">
4
+			<testcase name="TestOne" time="0.06"></testcase>
5
+			<testcase name="TestTwo" time="0.1"></testcase>
6
+		</testsuite>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testcase name="TestOne" time="0.06"></testcase>
4
+		<testcase name="TestTwo" time="0.1"></testcase>
5
+	</testsuite>
6
+</testsuites>
0 7
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
4
+			<testcase name="TestOne" time="0.06"></testcase>
5
+			<testcase name="TestTwo" time="0.1"></testcase>
6
+		</testsuite>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testcase name="TestOne" time="0.06"></testcase>
4
+		<testcase name="TestTwo" time="0.1"></testcase>
5
+	</testsuite>
6
+</testsuites>
0 7
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="1" time="0.15">
3
+		<testcase name="TestOne" time="0.02">
4
+			<failure message="">=== RUN TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
5
+		</testcase>
6
+		<testcase name="TestTwo" time="0.13"></testcase>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="1" time="0.15">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="1" time="0.15">
4
+			<testcase name="TestOne" time="0.02">
5
+				<failure message="">=== RUN TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
6
+			</testcase>
7
+			<testcase name="TestTwo" time="0.13"></testcase>
8
+		</testsuite>
9
+	</testsuite>
10
+</testsuites>
0 11
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="1" failures="0" time="0.15">
3
+		<testcase name="TestOne" time="0.02">
4
+			<skipped message="=== RUN TestOne&#xA;--- SKIP: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Skip message"></skipped>
5
+		</testcase>
6
+		<testcase name="TestTwo" time="0.13"></testcase>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="1" failures="0" time="0.15">
3
+		<testsuite name="package/name" tests="2" skipped="1" failures="0" time="0.15">
4
+			<testcase name="TestOne" time="0.02">
5
+				<skipped message="=== RUN TestOne&#xA;--- SKIP: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Skip message"></skipped>
6
+			</testcase>
7
+			<testcase name="TestTwo" time="0.13"></testcase>
8
+		</testsuite>
9
+	</testsuite>
10
+</testsuites>
0 11
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testcase name="TestOne" time="0.06"></testcase>
4
+		<testcase name="TestTwo" time="0.1"></testcase>
5
+	</testsuite>
6
+</testsuites>
0 7
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
4
+			<testcase name="TestOne" time="0.06"></testcase>
5
+			<testcase name="TestTwo" time="0.1"></testcase>
6
+		</testsuite>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name1" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testcase name="TestOne" time="0.06"></testcase>
4
+		<testcase name="TestTwo" time="0.1"></testcase>
5
+	</testsuite>
6
+	<testsuite name="package/name2" tests="2" skipped="0" failures="1" time="0.15">
7
+		<testcase name="TestOne" time="0.02">
8
+			<failure message="">=== RUN TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
9
+		</testcase>
10
+		<testcase name="TestTwo" time="0.13"></testcase>
11
+	</testsuite>
12
+</testsuites>
0 13
new file mode 100644
... ...
@@ -0,0 +1,15 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="4" skipped="0" failures="1" time="0.31">
3
+		<testsuite name="package/name1" tests="2" skipped="0" failures="0" time="0.16">
4
+			<testcase name="TestOne" time="0.06"></testcase>
5
+			<testcase name="TestTwo" time="0.1"></testcase>
6
+		</testsuite>
7
+		<testsuite name="package/name2" tests="2" skipped="0" failures="1" time="0.15">
8
+			<testcase name="TestOne" time="0.02">
9
+				<failure message="">=== RUN TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
10
+			</testcase>
11
+			<testcase name="TestTwo" time="0.13"></testcase>
12
+		</testsuite>
13
+	</testsuite>
14
+</testsuites>
0 15
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
3
+		<propery name="coverage.statements.pct" value="13.37"></propery>
4
+		<testcase name="TestOne" time="0.06"></testcase>
5
+		<testcase name="TestTwo" time="0.1"></testcase>
6
+	</testsuite>
7
+</testsuites>
0 8
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
4
+			<propery name="coverage.statements.pct" value="13.37"></propery>
5
+			<testcase name="TestOne" time="0.06"></testcase>
6
+			<testcase name="TestTwo" time="0.1"></testcase>
7
+		</testsuite>
8
+	</testsuite>
9
+</testsuites>
0 10
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
3
+		<propery name="coverage.statements.pct" value="10.0"></propery>
4
+		<testcase name="TestOne" time="0.06"></testcase>
5
+		<testcase name="TestTwo" time="0.1"></testcase>
6
+	</testsuite>
7
+</testsuites>
0 8
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="0.16">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.16">
4
+			<propery name="coverage.statements.pct" value="10.0"></propery>
5
+			<testcase name="TestOne" time="0.06"></testcase>
6
+			<testcase name="TestTwo" time="0.1"></testcase>
7
+		</testsuite>
8
+	</testsuite>
9
+</testsuites>
0 10
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.05">
3
+		<testcase name="TestOne" time="0.02"></testcase>
4
+		<testcase name="TestTwo" time="0.03"></testcase>
5
+	</testsuite>
6
+</testsuites>
0 7
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="2" skipped="0" failures="0" time="0.05">
3
+		<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.05">
4
+			<testcase name="TestOne" time="0.02"></testcase>
5
+			<testcase name="TestTwo" time="0.03"></testcase>
6
+		</testsuite>
7
+	</testsuite>
8
+</testsuites>
0 9
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="2" skipped="0" failures="0" time="0.05">
3
+		<testcase name="TestOne" time="0.02"></testcase>
4
+		<testcase name="TestTwo" time="0.03"></testcase>
5
+	</testsuite>
6
+	<testsuite name="package/name/nested" tests="2" skipped="1" failures="1" time="0.05">
7
+		<testcase name="TestOne" time="0.02">
8
+			<failure message="">=== RUN   TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
9
+		</testcase>
10
+		<testcase name="TestTwo" time="0.03">
11
+			<skipped message="=== RUN   TestTwo&#xA;--- SKIP: TestTwo (0.03 seconds)&#xA;&#x9;file_test.go:11: Skip message&#xA;PASS"></skipped>
12
+		</testcase>
13
+	</testsuite>
14
+	<testsuite name="package/other/nested" tests="2" skipped="0" failures="0" time="0.3">
15
+		<testcase name="TestOne" time="0.1"></testcase>
16
+		<testcase name="TestTwo" time="0.2"></testcase>
17
+	</testsuite>
18
+</testsuites>
0 19
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package" tests="6" skipped="1" failures="1" time="0.4">
3
+		<testsuite name="package/name" tests="4" skipped="1" failures="1" time="0.1">
4
+			<testcase name="TestOne" time="0.02"></testcase>
5
+			<testcase name="TestTwo" time="0.03"></testcase>
6
+			<testsuite name="package/name/nested" tests="2" skipped="1" failures="1" time="0.05">
7
+				<testcase name="TestOne" time="0.02">
8
+					<failure message="">=== RUN   TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
9
+				</testcase>
10
+				<testcase name="TestTwo" time="0.03">
11
+					<skipped message="=== RUN   TestTwo&#xA;--- SKIP: TestTwo (0.03 seconds)&#xA;&#x9;file_test.go:11: Skip message&#xA;PASS"></skipped>
12
+				</testcase>
13
+			</testsuite>
14
+		</testsuite>
15
+		<testsuite name="package/other" tests="2" skipped="0" failures="0" time="0.3">
16
+			<testsuite name="package/other/nested" tests="2" skipped="0" failures="0" time="0.3">
17
+				<testcase name="TestOne" time="0.1"></testcase>
18
+				<testcase name="TestTwo" time="0.2"></testcase>
19
+			</testsuite>
20
+		</testsuite>
21
+	</testsuite>
22
+</testsuites>
0 23
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+<?xml version="1.0" encoding="UTF-8"?>
1
+<testsuites>
2
+	<testsuite name="package/name" tests="4" skipped="1" failures="1" time="0.1">
3
+		<testcase name="TestOne" time="0.02"></testcase>
4
+		<testcase name="TestTwo" time="0.03"></testcase>
5
+		<testsuite name="package/name/nested" tests="2" skipped="1" failures="1" time="0.05">
6
+			<testcase name="TestOne" time="0.02">
7
+				<failure message="">=== RUN   TestOne&#xA;--- FAIL: TestOne (0.02 seconds)&#xA;&#x9;file_test.go:11: Error message&#xA;&#x9;file_test.go:11: Longer&#xA;&#x9;&#x9;error&#xA;&#x9;&#x9;message.</failure>
8
+			</testcase>
9
+			<testcase name="TestTwo" time="0.03">
10
+				<skipped message="=== RUN   TestTwo&#xA;--- SKIP: TestTwo (0.03 seconds)&#xA;&#x9;file_test.go:11: Skip message&#xA;PASS"></skipped>
11
+			</testcase>
12
+		</testsuite>
13
+	</testsuite>
14
+	<testsuite name="package/other" tests="2" skipped="0" failures="0" time="0.3">
15
+		<testsuite name="package/other/nested" tests="2" skipped="0" failures="0" time="0.3">
16
+			<testcase name="TestOne" time="0.1"></testcase>
17
+			<testcase name="TestTwo" time="0.2"></testcase>
18
+		</testsuite>
19
+	</testsuite>
20
+</testsuites>
0 21
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 2.160s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 0.160s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+Of 2 tests executed in 0.150s, 1 succeeded, 1 failed, and 0 were skipped.
1
+
2
+In suite "package/name", test case "TestOne" failed:
3
+=== RUN TestOne
4
+--- FAIL: TestOne (0.02 seconds)
5
+	file_test.go:11: Error message
6
+	file_test.go:11: Longer
7
+		error
8
+		message.
9
+
0 10
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+Of 2 tests executed in 0.150s, 1 succeeded, 0 failed, and 1 was skipped.
1
+
2
+In suite "package/name", test case "TestOne" was skipped:
3
+=== RUN TestOne
4
+--- SKIP: TestOne (0.02 seconds)
5
+	file_test.go:11: Skip message
6
+
0 7
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 0.160s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+Of 4 tests executed in 0.310s, 3 succeeded, 1 failed, and 0 were skipped.
1
+
2
+In suite "package/name2", test case "TestOne" failed:
3
+=== RUN TestOne
4
+--- FAIL: TestOne (0.02 seconds)
5
+	file_test.go:11: Error message
6
+	file_test.go:11: Longer
7
+		error
8
+		message.
9
+
0 10
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 0.160s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 0.160s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+Of 2 tests executed in 0.050s, 2 succeeded, 0 failed, and 0 were skipped.
1
+
0 2
new file mode 100644
... ...
@@ -0,0 +1,16 @@
0
+Of 6 tests executed in 0.400s, 4 succeeded, 1 failed, and 1 was skipped.
1
+
2
+In suite "package/name/nested", test case "TestOne" failed:
3
+=== RUN   TestOne
4
+--- FAIL: TestOne (0.02 seconds)
5
+	file_test.go:11: Error message
6
+	file_test.go:11: Longer
7
+		error
8
+		message.
9
+
10
+In suite "package/name/nested", test case "TestTwo" was skipped:
11
+=== RUN   TestTwo
12
+--- SKIP: TestTwo (0.03 seconds)
13
+	file_test.go:11: Skip message
14
+PASS
15
+
0 16
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06 seconds)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10 seconds)
4
+PASS
5
+ok  	package/name 0.160s
0 6
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06 seconds)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10 seconds)
4
+PASS
5
+ok  	package/name 2.160s
0 6
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+=== RUN TestOne
1
+--- FAIL: TestOne (0.02 seconds)
2
+	file_test.go:11: Error message
3
+	file_test.go:11: Longer
4
+		error
5
+		message.
6
+=== RUN TestTwo
7
+--- PASS: TestTwo (0.13 seconds)
8
+FAIL
9
+exit status 1
10
+FAIL	package/name 0.150s
0 11
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+=== RUN TestOne
1
+--- SKIP: TestOne (0.02 seconds)
2
+	file_test.go:11: Skip message
3
+=== RUN TestTwo
4
+--- PASS: TestTwo (0.13 seconds)
5
+PASS
6
+ok	package/name 0.150s
0 7
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06s)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10s)
4
+PASS
5
+ok  	package/name	0.160s
0 6
new file mode 100644
... ...
@@ -0,0 +1,17 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06 seconds)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10 seconds)
4
+PASS
5
+ok  	package/name1 0.160s
6
+=== RUN TestOne
7
+--- FAIL: TestOne (0.02 seconds)
8
+	file_test.go:11: Error message
9
+	file_test.go:11: Longer
10
+		error
11
+		message.
12
+=== RUN TestTwo
13
+--- PASS: TestTwo (0.13 seconds)
14
+FAIL
15
+exit status 1
16
+FAIL	package/name2 0.150s
0 17
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06 seconds)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10 seconds)
4
+PASS
5
+coverage: 13.37% of statements
6
+ok  	package/name 0.160s
0 7
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+=== RUN TestOne
1
+--- PASS: TestOne (0.06 seconds)
2
+=== RUN TestTwo
3
+--- PASS: TestTwo (0.10 seconds)
4
+PASS
5
+ok  	package/name 0.160s  coverage: 10.0% of statements
0 6
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+=== RUN   TestOne
1
+--- PASS: TestOne (0.02s)
2
+=== RUN   TestTwo
3
+--- PASS: TestTwo (0.03s)
4
+PASS
5
+ok  	package/name	0.050s
0 6
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+=== RUN   TestOne
1
+--- PASS: TestOne (0.02s)
2
+=== RUN   TestTwo
3
+--- PASS: TestTwo (0.03s)
4
+PASS
5
+ok  	package/name	0.050s
6
+=== RUN   TestOne
7
+--- FAIL: TestOne (0.02 seconds)
8
+	file_test.go:11: Error message
9
+	file_test.go:11: Longer
10
+		error
11
+		message.
12
+=== RUN   TestTwo
13
+--- SKIP: TestTwo (0.03 seconds)
14
+	file_test.go:11: Skip message
15
+PASS
16
+ok  	package/name/nested	0.050s
17
+=== RUN   TestOne
18
+--- PASS: TestOne (0.1s)
19
+=== RUN   TestTwo
20
+--- PASS: TestTwo (0.2s)
21
+PASS
22
+ok  	package/other/nested	0.300s
0 23
\ No newline at end of file
1 24
new file mode 100755
... ...
@@ -0,0 +1,71 @@
0
+#!/bin/bash
1
+
2
+# This file runs the integration tests for the `junitreport` binary to ensure that correct jUnit XML is produced.
3
+
4
+set -o errexit
5
+set -o nounset
6
+set -o pipefail
7
+
8
+JUNITREPORT_ROOT=$(dirname "${BASH_SOURCE}")/..
9
+pushd "${JUNITREPORT_ROOT}" > /dev/null
10
+
11
+TMPDIR="/tmp/junitreport/test/integration"
12
+mkdir -p "${TMPDIR}"
13
+
14
+echo "[INFO] Building junitreport binary for testing..."
15
+go build .
16
+
17
+for suite in test/*/; do
18
+	suite_name=$( basename ${suite} )
19
+	echo "[INFO] Testing suite ${suite_name}..."
20
+
21
+	WORKINGDIR="${TMPDIR}/${suite_name}"
22
+	mkdir -p "${WORKINGDIR}"
23
+
24
+	# test every case with flat and nested suites
25
+	for test in ${suite}/testdata/*.txt; do
26
+		test_name=$( basename ${test} | grep -Po ".+(?=\.txt)" )
27
+
28
+		cat "${test}" | ./junitreport -type "${suite_name}" -suites flat > "${WORKINGDIR}/${test_name}_flat.xml"
29
+		if ! diff "${suite}/reports/${test_name}_flat.xml" "${WORKINGDIR}/${test_name}_flat.xml"; then
30
+			echo "[FAIL] Test '${test_name}' in suite '${suite_name}' failed for flat suite builder."
31
+			exit 1
32
+		fi
33
+
34
+		cat "${test}" | ./junitreport -type "${suite_name}" -suites nested > "${WORKINGDIR}/${test_name}_nested.xml"
35
+		if ! diff "${suite}/reports/${test_name}_nested.xml" "${WORKINGDIR}/${test_name}_nested.xml"; then
36
+			echo "[FAIL] Test '${test_name}' in suite '${suite_name}' failed for nested suite builder."
37
+			exit 1
38
+		fi
39
+
40
+		cat "${WORKINGDIR}/${test_name}_flat.xml" | ./junitreport summarize > "${WORKINGDIR}/${test_name}_summary.txt"
41
+		if ! diff "${suite}/summaries/${test_name}_summary.txt" "${WORKINGDIR}/${test_name}_summary.txt"; then
42
+			echo "[FAIL] Test '${test_name}' in suite '${suite_name}' failed to summarize flat XML."
43
+		fi
44
+
45
+		cat "${WORKINGDIR}/${test_name}_nested.xml" | ./junitreport summarize > "${WORKINGDIR}/${test_name}_summary.txt"
46
+		if ! diff "${suite}/summaries/${test_name}_summary.txt" "${WORKINGDIR}/${test_name}_summary.txt"; then
47
+			echo "[FAIL] Test '${test_name}' in suite '${suite_name}' failed to summarize nested XML."
48
+		fi
49
+	done
50
+
51
+	echo "[PASS] Test output type passed: ${suite_name}"
52
+done
53
+
54
+echo "[INFO] Testing restricted roots with nested suites..."
55
+# test some cases with nested suites and given roots
56
+cat "test/gotest/testdata/1.txt" | ./junitreport -type gotest -suites nested -roots package/name > "${TMPDIR}/gotest/1_nested_restricted.xml"
57
+if ! diff "test/gotest/reports/1_nested_restricted.xml" "${TMPDIR}/gotest/1_nested_restricted.xml"; then
58
+	echo "[FAIL] Test '1' in suite 'gotest' failed for nested suite builder with restricted roots: 'package/name'."
59
+	exit 1
60
+fi
61
+
62
+cat "test/gotest/testdata/9.txt" | ./junitreport -type gotest -suites nested -roots package/name,package/other > "${TMPDIR}/gotest/9_nested_restricted.xml"
63
+if ! diff "test/gotest/reports/9_nested_restricted.xml" "${TMPDIR}/gotest/9_nested_restricted.xml"; then
64
+	echo "[FAIL] Test '9' in suite 'gotest' failed for nested suite builder with restricted roots: 'package/name,package/other'."
65
+	exit 1
66
+fi
67
+echo "[PASS] Suite passed: restricted roots"
68
+
69
+echo "[PASS] junitreport testing successful"
70
+popd > /dev/null