xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/bzltestutil/xml.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1// Copyright 2020 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package bzltestutil
16
17import (
18	"encoding/json"
19	"encoding/xml"
20	"fmt"
21	"io"
22	"path"
23	"sort"
24	"strings"
25	"time"
26)
27
28type xmlTestSuites struct {
29	XMLName xml.Name       `xml:"testsuites"`
30	Suites  []xmlTestSuite `xml:"testsuite"`
31}
32
33type xmlTestSuite struct {
34	XMLName   xml.Name      `xml:"testsuite"`
35	TestCases []xmlTestCase `xml:"testcase"`
36	Errors    int           `xml:"errors,attr"`
37	Failures  int           `xml:"failures,attr"`
38	Skipped   int           `xml:"skipped,attr"`
39	Tests     int           `xml:"tests,attr"`
40	Time      string        `xml:"time,attr"`
41	Name      string        `xml:"name,attr"`
42}
43
44type xmlTestCase struct {
45	XMLName   xml.Name    `xml:"testcase"`
46	Classname string      `xml:"classname,attr"`
47	Name      string      `xml:"name,attr"`
48	Time      string      `xml:"time,attr"`
49	Failure   *xmlMessage `xml:"failure,omitempty"`
50	Error     *xmlMessage `xml:"error,omitempty"`
51	Skipped   *xmlMessage `xml:"skipped,omitempty"`
52}
53
54type xmlMessage struct {
55	Message  string `xml:"message,attr"`
56	Type     string `xml:"type,attr"`
57	Contents string `xml:",chardata"`
58}
59
60// jsonEvent as encoded by the test2json package.
61type jsonEvent struct {
62	Time    *time.Time
63	Action  string
64	Package string
65	Test    string
66	Elapsed *float64
67	Output  string
68}
69
70type testCase struct {
71	state    string
72	output   strings.Builder
73	duration *float64
74}
75
76// json2xml converts test2json's output into an xml output readable by Bazel.
77// http://windyroad.com.au/dl/Open%20Source/JUnit.xsd
78func json2xml(r io.Reader, pkgName string) ([]byte, error) {
79	var pkgDuration *float64
80	testcases := make(map[string]*testCase)
81	testCaseByName := func(name string) *testCase {
82		if name == "" {
83			return nil
84		}
85		if _, ok := testcases[name]; !ok {
86			testcases[name] = &testCase{}
87		}
88		return testcases[name]
89	}
90
91	dec := json.NewDecoder(r)
92	for {
93		var e jsonEvent
94		if err := dec.Decode(&e); err == io.EOF {
95			break
96		} else if err != nil {
97			return nil, fmt.Errorf("error decoding test2json output: %s", err)
98		}
99		switch s := e.Action; s {
100		case "run":
101			if c := testCaseByName(e.Test); c != nil {
102				c.state = s
103			}
104		case "output":
105			if c := testCaseByName(e.Test); c != nil {
106				c.output.WriteString(e.Output)
107			}
108		case "skip":
109			if c := testCaseByName(e.Test); c != nil {
110				c.output.WriteString(e.Output)
111				c.state = s
112				c.duration = e.Elapsed
113			}
114		case "fail":
115			if c := testCaseByName(e.Test); c != nil {
116				c.state = s
117				c.duration = e.Elapsed
118			} else {
119				pkgDuration = e.Elapsed
120			}
121		case "pass":
122			if c := testCaseByName(e.Test); c != nil {
123				c.duration = e.Elapsed
124				c.state = s
125			} else {
126				pkgDuration = e.Elapsed
127			}
128		}
129	}
130
131	return xml.MarshalIndent(toXML(pkgName, pkgDuration, testcases), "", "\t")
132}
133
134func toXML(pkgName string, pkgDuration *float64, testcases map[string]*testCase) *xmlTestSuites {
135	cases := make([]string, 0, len(testcases))
136	for k := range testcases {
137		cases = append(cases, k)
138	}
139	sort.Strings(cases)
140	suite := xmlTestSuite{
141		Name: pkgName,
142	}
143	if pkgDuration != nil {
144		suite.Time = fmt.Sprintf("%.3f", *pkgDuration)
145	}
146	for _, name := range cases {
147		c := testcases[name]
148		suite.Tests++
149		newCase := xmlTestCase{
150			Name:      name,
151			Classname: path.Base(pkgName),
152		}
153		if c.duration != nil {
154			newCase.Time = fmt.Sprintf("%.3f", *c.duration)
155		}
156		switch c.state {
157		case "skip":
158			suite.Skipped++
159			newCase.Skipped = &xmlMessage{
160				Message:  "Skipped",
161				Contents: c.output.String(),
162			}
163		case "fail":
164			suite.Failures++
165			newCase.Failure = &xmlMessage{
166				Message:  "Failed",
167				Contents: c.output.String(),
168			}
169		case "pass":
170			break
171		default:
172			suite.Errors++
173			newCase.Error = &xmlMessage{
174				Message:  "No pass/skip/fail event found for test",
175				Contents: c.output.String(),
176			}
177		}
178		suite.TestCases = append(suite.TestCases, newCase)
179	}
180	return &xmlTestSuites{Suites: []xmlTestSuite{suite}}
181}
182