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