github.com/GoogleCloudPlatform/testgrid@v0.0.174/metadata/junit/junit.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package junit describes the test-infra definition of "junit", and provides 18 // utilities to parse it. 19 package junit 20 21 import ( 22 "bytes" 23 "encoding/xml" 24 "fmt" 25 "io" 26 "strings" 27 "unicode/utf8" 28 ) 29 30 type suiteOrSuites struct { 31 suites Suites 32 } 33 34 func (s *suiteOrSuites) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 35 switch start.Name.Local { 36 case "testsuites": 37 d.DecodeElement(&s.suites, &start) 38 case "testsuite": 39 var suite Suite 40 d.DecodeElement(&suite, &start) 41 s.suites.Suites = append(s.suites.Suites, suite) 42 default: 43 return fmt.Errorf("bad element name: %q", start.Name) 44 } 45 s.suites.Truncate(10000) 46 return nil 47 } 48 49 // Suites holds a <testsuites/> list of Suite results 50 type Suites struct { 51 XMLName xml.Name `xml:"testsuites"` 52 Suites []Suite `xml:"testsuite"` 53 } 54 55 // Truncate ensures that strings do not exceed the specified length. 56 func (s *Suites) Truncate(max int) { 57 for i := range s.Suites { 58 s.Suites[i].Truncate(max) 59 } 60 } 61 62 // Suite holds <testsuite/> results 63 type Suite struct { 64 XMLName xml.Name `xml:"testsuite"` 65 Suites []Suite `xml:"testsuite"` 66 Name string `xml:"name,attr"` 67 Properties *Properties `xml:"properties,omitempty"` 68 Time float64 `xml:"time,attr"` // Seconds 69 TimeStamp string `xml:"timestamp,attr"` 70 Failures int `xml:"failures,attr"` 71 Tests int `xml:"tests,attr"` 72 Disabled int `xml:"disabled,attr"` 73 Skipped int `xml:"skipped,attr"` 74 Errors int `xml:"errors,attr"` 75 Results []Result `xml:"testcase"` 76 } 77 78 // Truncate ensures that strings do not exceed the specified length. 79 func (s *Suite) Truncate(max int) { 80 for i := range s.Suites { 81 s.Suites[i].Truncate(max) 82 } 83 for i := range s.Results { 84 s.Results[i].Truncate(max) 85 } 86 } 87 88 // Property defines the xml element that stores additional metrics about each benchmark. 89 type Property struct { 90 Name string `xml:"name,attr"` 91 Value string `xml:"value,attr"` 92 } 93 94 // Properties defines the xml element that stores the list of properties that are associated with one benchmark. 95 type Properties struct { 96 PropertyList []Property `xml:"property"` 97 } 98 99 // Result holds <testcase/> results 100 type Result struct { 101 Name string `xml:"name,attr"` 102 Time float64 `xml:"time,attr"` 103 ClassName string `xml:"classname,attr"` 104 Output *string `xml:"system-out,omitempty"` 105 Error *string `xml:"system-err,omitempty"` 106 Errored *Errored `xml:"error,omitempty"` 107 Failure *Failure `xml:"failure,omitempty"` 108 Skipped *Skipped `xml:"skipped,omitempty"` 109 Status string `xml:"status,attr"` 110 Properties *Properties `xml:"properties,omitempty"` 111 } 112 113 // Errored holds <error/> elements. 114 type Errored struct { 115 Message string `xml:"message,attr"` 116 Type string `xml:"type,attr"` 117 Value string `xml:",chardata"` 118 } 119 120 // Failure holds <failure/> elements. 121 type Failure struct { 122 Message string `xml:"message,attr"` 123 Type string `xml:"type,attr"` 124 Value string `xml:",chardata"` 125 } 126 127 // Skipped holds <skipped/> elements. 128 type Skipped struct { 129 Message string `xml:"message,attr"` 130 Value string `xml:",chardata"` 131 } 132 133 // SetProperty adds the specified property to the Result or replaces the 134 // existing value if a property with that name already exists. 135 func (r *Result) SetProperty(name, value string) { 136 if r.Properties == nil { 137 r.Properties = &Properties{} 138 } 139 for i, existing := range r.Properties.PropertyList { 140 if existing.Name == name { 141 r.Properties.PropertyList[i].Value = value 142 return 143 } 144 } 145 // Didn't find an existing property. Add a new one. 146 r.Properties.PropertyList = append( 147 r.Properties.PropertyList, 148 Property{ 149 Name: name, 150 Value: value, 151 }, 152 ) 153 } 154 155 // Message extracts the message for the junit test case. 156 // 157 // Will use the first non-empty <error/>, <failure/>, <skipped/>, <system-err/>, <system-out/> value. 158 func (r Result) Message(max int) string { 159 var msg string 160 switch { 161 case r.Errored != nil && (r.Errored.Message != "" || r.Errored.Value != ""): 162 msg = composeMessage(r.Errored.Message, r.Errored.Value) 163 case r.Failure != nil && (r.Failure.Message != "" || r.Failure.Value != ""): 164 msg = composeMessage(r.Failure.Message, r.Failure.Value) 165 case r.Skipped != nil && (r.Skipped.Message != "" || r.Skipped.Value != ""): 166 msg = composeMessage(r.Skipped.Message, r.Skipped.Value) 167 case r.Error != nil && *r.Error != "": 168 msg = *r.Error 169 case r.Output != nil && *r.Output != "": 170 msg = *r.Output 171 } 172 msg = truncate(msg, max) 173 if utf8.ValidString(msg) { 174 return msg 175 } 176 return fmt.Sprintf("invalid utf8: %s", strings.ToValidUTF8(msg, "?")) 177 } 178 179 func composeMessage(messages ...string) string { 180 nonEmptyMessages := []string{} 181 for _, m := range messages { 182 if m != "" { 183 nonEmptyMessages = append(nonEmptyMessages, m) 184 } 185 } 186 messageBuilder := strings.Builder{} 187 for i, m := range nonEmptyMessages { 188 messageBuilder.WriteString(m) 189 if i+1 < len(nonEmptyMessages) { 190 messageBuilder.WriteRune('\n') 191 } 192 } 193 return messageBuilder.String() 194 } 195 196 func truncate(s string, max int) string { 197 if max <= 0 { 198 return s 199 } 200 l := len(s) 201 if l < max { 202 return s 203 } 204 h := max / 2 205 return s[:h] + "..." + s[l-h:] 206 } 207 208 func truncatePointer(str *string, max int) { 209 if str == nil { 210 return 211 } 212 s := truncate(*str, max) 213 str = &s 214 } 215 216 // Truncate ensures that strings do not exceed the specified length. 217 func (r Result) Truncate(max int) { 218 var errorVal, failureVal, skippedVal string 219 if r.Errored != nil { 220 errorVal = r.Errored.Value 221 } 222 if r.Failure != nil { 223 failureVal = r.Failure.Value 224 } 225 if r.Skipped != nil { 226 skippedVal = r.Skipped.Value 227 } 228 for _, s := range []*string{&errorVal, &failureVal, &skippedVal, r.Error, r.Output} { 229 truncatePointer(s, max) 230 } 231 } 232 233 func unmarshalXML(reader io.Reader, i interface{}) error { 234 dec := xml.NewDecoder(reader) 235 dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { 236 switch charset { 237 case "UTF-8", "utf8", "": 238 // utf8 is not recognized by golang, but our coalesce.py writes a utf8 doc, which python accepts. 239 return input, nil 240 default: 241 return nil, fmt.Errorf("unknown charset: %s", charset) 242 } 243 } 244 return dec.Decode(i) 245 } 246 247 // Parse returns the Suites representation of these XML bytes. 248 func Parse(buf []byte) (*Suites, error) { 249 if len(buf) == 0 { 250 return &Suites{}, nil 251 } 252 reader := bytes.NewReader(buf) 253 return ParseStream(reader) 254 } 255 256 // ParseStream reads bytes into a Suites object. 257 func ParseStream(reader io.Reader) (*Suites, error) { 258 // Try to parse it as a <testsuites/> object 259 var s suiteOrSuites 260 err := unmarshalXML(reader, &s) 261 if err != nil && err != io.EOF { 262 return nil, err 263 } 264 return &s.suites, nil 265 }