Browse code

Merge pull request #12357 from smarterclayton/merge_junit

Merged by openshift-bot

OpenShift Bot authored on 2016/12/29 13:54:33
Showing 5 changed files
... ...
@@ -29,4 +29,6 @@ TEST_PARALLEL="${PARALLEL_NODES:-5}" FOCUS="${pf}" SKIP="${ps}" TEST_REPORT_FILE
29 29
 os::log::info "Running serial tests"
30 30
 FOCUS="${sf}" SKIP="${ss}" TEST_REPORT_FILE_NAME=conformance_serial os::test::extended::run -- -ginkgo.noColor -ginkgo.v -test.timeout 2h ${TEST_EXTENDED_ARGS-} || exitstatus=$?
31 31
 
32
+os::test::extended::merge_junit
33
+
32 34
 exit $exitstatus
... ...
@@ -34,4 +34,6 @@ os::log::info ""
34 34
 os::log::info "Running serial tests"
35 35
 FOCUS="${sf}" SKIP="${ss}" TEST_REPORT_FILE_NAME=core_serial os::test::extended::run -- -ginkgo.noColor -ginkgo.v -test.timeout 2h ${TEST_EXTENDED_ARGS-} || exitstatus=$?
36 36
 
37
+os::test::extended::merge_junit
38
+
37 39
 exit $exitstatus
... ...
@@ -3,7 +3,7 @@
3 3
 # This abstracts starting up an extended server.
4 4
 
5 5
 # If invoked with arguments, executes the test directly.
6
-function os::test::extended::focus {
6
+function os::test::extended::focus () {
7 7
 	if [[ $# -ne 0 ]]; then
8 8
 		os::log::info "Running custom: $*"
9 9
 		os::test::extended::test_list "$@"
... ...
@@ -27,6 +27,7 @@ function os::test::extended::setup () {
27 27
 	os::util::ensure::built_binary_exists 'openshift'
28 28
 	os::util::ensure::built_binary_exists 'oadm'
29 29
 	os::util::ensure::built_binary_exists 'oc'
30
+	os::util::ensure::built_binary_exists 'junitmerge' 'tools/junitmerge'
30 31
 
31 32
 	# ensure proper relative directories are set
32 33
 	export EXTENDED_TEST_PATH="${OS_ROOT}/test/extended"
... ...
@@ -244,6 +245,18 @@ function os::test::extended::test_list () {
244 244
 }
245 245
 readonly -f os::test::extended::test_list
246 246
 
247
+# Merge all of the JUnit output files in the TEST_REPORT_DIR into a single file.
248
+# This works around a gap in Jenkins JUnit reporter output that double counts skipped
249
+# files until https://github.com/jenkinsci/junit-plugin/pull/54 is merged.
250
+function os::test::extended::merge_junit () {
251
+	local output
252
+	output="$( mktemp )"
253
+	"$( os::util::find::built_binary junitmerge )" "${TEST_REPORT_DIR}"/*.xml > "${output}"
254
+	rm "${TEST_REPORT_DIR}"/*.xml
255
+	mv "${output}" "${TEST_REPORT_DIR}/junit.xml"
256
+}
257
+readonly -f os::test::extended::merge_junit
258
+
247 259
 # Not run by any suite
248 260
 readonly EXCLUDED_TESTS=(
249 261
 	"\[Skipped\]"
250 262
new file mode 100644
... ...
@@ -0,0 +1,155 @@
0
+package main
1
+
2
+import (
3
+	"encoding/xml"
4
+	"log"
5
+	"os"
6
+
7
+	"fmt"
8
+	"github.com/openshift/origin/tools/junitreport/pkg/api"
9
+	"sort"
10
+)
11
+
12
+type uniqueSuites map[string]*suiteRuns
13
+
14
+func (s uniqueSuites) Merge(namePrefix string, suite *api.TestSuite) {
15
+	name := suite.Name
16
+	if len(namePrefix) > 0 {
17
+		name = namePrefix + "/"
18
+	}
19
+	existing, ok := s[name]
20
+	if !ok {
21
+		existing = newSuiteRuns(suite)
22
+		s[name] = existing
23
+	}
24
+
25
+	existing.Merge(suite.TestCases)
26
+
27
+	for _, suite := range suite.Children {
28
+		s.Merge(name, suite)
29
+	}
30
+}
31
+
32
+type suiteRuns struct {
33
+	suite *api.TestSuite
34
+	runs  map[string]*api.TestCase
35
+}
36
+
37
+func newSuiteRuns(suite *api.TestSuite) *suiteRuns {
38
+	return &suiteRuns{
39
+		suite: suite,
40
+		runs:  make(map[string]*api.TestCase),
41
+	}
42
+}
43
+
44
+func (r *suiteRuns) Merge(testCases []*api.TestCase) {
45
+	for _, testCase := range testCases {
46
+		existing, ok := r.runs[testCase.Name]
47
+		if !ok {
48
+			r.runs[testCase.Name] = testCase
49
+			continue
50
+		}
51
+		switch {
52
+		case testCase.SkipMessage != nil:
53
+			// if the new test is a skip, ignore it
54
+		case existing.SkipMessage != nil && testCase.SkipMessage == nil:
55
+			// always replace a skip with a non-skip
56
+			r.runs[testCase.Name] = testCase
57
+		case existing.FailureOutput == nil && testCase.FailureOutput != nil:
58
+			// replace a passing test with a failing test
59
+			r.runs[testCase.Name] = testCase
60
+		}
61
+	}
62
+}
63
+
64
+func main() {
65
+	log.SetFlags(0)
66
+	suites := make(uniqueSuites)
67
+
68
+	for _, arg := range os.Args[1:] {
69
+		f, err := os.Open(arg)
70
+		if err != nil {
71
+			log.Fatal(err)
72
+		}
73
+		defer f.Close()
74
+		d := xml.NewDecoder(f)
75
+
76
+		for {
77
+			t, err := d.Token()
78
+			if err != nil {
79
+				log.Fatal(err)
80
+			}
81
+			if t == nil {
82
+				log.Fatalf("input file %s does not appear to be a JUnit XML file", arg)
83
+			}
84
+			// Inspect the top level DOM element and perform the appropriate action
85
+			switch se := t.(type) {
86
+			case xml.StartElement:
87
+				switch se.Name.Local {
88
+				case "testsuites":
89
+					input := &api.TestSuites{}
90
+					if err := d.DecodeElement(input, &se); err != nil {
91
+						log.Fatal(err)
92
+					}
93
+					for _, suite := range input.Suites {
94
+						suites.Merge("", suite)
95
+					}
96
+				case "testsuite":
97
+					input := &api.TestSuite{}
98
+					if err := d.DecodeElement(input, &se); err != nil {
99
+						log.Fatal(err)
100
+					}
101
+					suites.Merge("", input)
102
+				default:
103
+					log.Fatal(fmt.Errorf("unexpected top level element in %s: %s", arg, se.Name.Local))
104
+				}
105
+			default:
106
+				continue
107
+			}
108
+			break
109
+		}
110
+	}
111
+
112
+	var suiteNames []string
113
+	for k := range suites {
114
+		suiteNames = append(suiteNames, k)
115
+	}
116
+	sort.Sort(sort.StringSlice(suiteNames))
117
+	output := &api.TestSuites{}
118
+
119
+	for _, name := range suiteNames {
120
+		suite := suites[name]
121
+
122
+		out := &api.TestSuite{
123
+			Name:     name,
124
+			NumTests: uint(len(suite.runs)),
125
+		}
126
+
127
+		var keys []string
128
+		for k := range suite.runs {
129
+			keys = append(keys, k)
130
+		}
131
+		sort.Sort(sort.StringSlice(keys))
132
+
133
+		for _, k := range keys {
134
+			testCase := suite.runs[k]
135
+			out.TestCases = append(out.TestCases, testCase)
136
+			switch {
137
+			case testCase.SkipMessage != nil:
138
+				out.NumSkipped++
139
+			case testCase.FailureOutput != nil:
140
+				out.NumFailed++
141
+			}
142
+			out.Duration += testCase.Duration
143
+		}
144
+		output.Suites = append(output.Suites, out)
145
+	}
146
+
147
+	e := xml.NewEncoder(os.Stdout)
148
+	e.Indent("", "\t")
149
+	if err := e.Encode(output); err != nil {
150
+		log.Fatal(err)
151
+	}
152
+	e.Flush()
153
+	fmt.Fprintln(os.Stdout)
154
+}
... ...
@@ -60,6 +60,9 @@ type TestCase struct {
60 60
 	// Name is the name of the test case
61 61
 	Name string `xml:"name,attr"`
62 62
 
63
+	// Classname is an attribute set by the package type and is required
64
+	Classname string `xml:"classname,attr,omitempty"`
65
+
63 66
 	// Duration is the time taken in seconds to run the test
64 67
 	Duration float64 `xml:"time,attr"`
65 68
 
... ...
@@ -68,6 +71,12 @@ type TestCase struct {
68 68
 
69 69
 	// FailureOutput holds the output from a failing test
70 70
 	FailureOutput *FailureOutput `xml:"failure"`
71
+
72
+	// SystemOut is output written to stdout during the execution of this test case
73
+	SystemOut string `xml:"system-out,omitempty"`
74
+
75
+	// SystemErr is output written to stderr during the execution of this test case
76
+	SystemErr string `xml:"system-err,omitempty"`
71 77
 }
72 78
 
73 79
 // SkipMessage holds a message explaining why a test was skipped
... ...
@@ -75,7 +84,7 @@ type SkipMessage struct {
75 75
 	XMLName xml.Name `xml:"skipped"`
76 76
 
77 77
 	// Message explains why the test was skipped
78
-	Message string `xml:"message,attr"`
78
+	Message string `xml:"message,attr,omitempty"`
79 79
 }
80 80
 
81 81
 // FailureOutput holds the output from a failing test