github.com/jaylevin/jenkins-library@v1.230.4/cmd/abapEnvironmentRunAUnitTest.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "encoding/xml" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "net/http/cookiejar" 11 "reflect" 12 "strings" 13 "time" 14 15 "github.com/SAP/jenkins-library/pkg/abaputils" 16 "github.com/SAP/jenkins-library/pkg/command" 17 piperhttp "github.com/SAP/jenkins-library/pkg/http" 18 "github.com/SAP/jenkins-library/pkg/log" 19 "github.com/SAP/jenkins-library/pkg/piperutils" 20 "github.com/SAP/jenkins-library/pkg/telemetry" 21 "github.com/pkg/errors" 22 ) 23 24 func abapEnvironmentRunAUnitTest(config abapEnvironmentRunAUnitTestOptions, telemetryData *telemetry.CustomData) { 25 26 // for command execution use Command 27 c := command.Command{} 28 // reroute command output to logging framework 29 c.Stdout(log.Writer()) 30 c.Stderr(log.Writer()) 31 32 var autils = abaputils.AbapUtils{ 33 Exec: &c, 34 } 35 36 client := piperhttp.Client{} 37 38 // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end 39 err := runAbapEnvironmentRunAUnitTest(&config, telemetryData, &autils, &client) 40 if err != nil { 41 log.Entry().WithError(err).Fatal("step execution failed") 42 } 43 } 44 45 func runAbapEnvironmentRunAUnitTest(config *abapEnvironmentRunAUnitTestOptions, telemetryData *telemetry.CustomData, com abaputils.Communication, client piperhttp.Sender) error { 46 var details abaputils.ConnectionDetailsHTTP 47 subOptions := convertAUnitOptions(config) 48 details, err := com.GetAbapCommunicationArrangementInfo(subOptions, "") 49 var resp *http.Response 50 cookieJar, _ := cookiejar.New(nil) 51 //Fetch Xcrsf-Token 52 if err == nil { 53 credentialsOptions := piperhttp.ClientOptions{ 54 Username: details.User, 55 Password: details.Password, 56 CookieJar: cookieJar, 57 } 58 client.SetOptions(credentialsOptions) 59 details.XCsrfToken, err = fetchAUnitXcsrfToken("GET", details, nil, client) 60 } 61 if err == nil { 62 resp, err = triggerAUnitrun(*config, details, client) 63 } 64 if err == nil { 65 err = fetchAndPersistAUnitResults(resp, details, client, config.AUnitResultsFileName, config.GenerateHTML) 66 } 67 if err != nil { 68 log.Entry().WithError(err).Fatal("step execution failed") 69 } 70 log.Entry().Info("AUnit test run completed successfully. If there are any results from the respective run they will be listed in the logs above as well as being saved in the output .xml file") 71 return nil 72 } 73 74 func triggerAUnitrun(config abapEnvironmentRunAUnitTestOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (*http.Response, error) { 75 76 abapEndpoint := details.URL 77 bodyString, err := buildAUnitRequestBody(config) 78 if err != nil { 79 return nil, err 80 } 81 82 //Trigger AUnit run 83 var resp *http.Response 84 85 var body = []byte(bodyString) 86 log.Entry().Debugf("Request Body: %s", bodyString) 87 details.URL = abapEndpoint + "/sap/bc/adt/api/abapunit/runs" 88 resp, err = runAUnit("POST", details, body, client) 89 return resp, err 90 } 91 92 func resolveAUnitConfiguration(config abapEnvironmentRunAUnitTestOptions) (aUnitConfig AUnitConfig, err error) { 93 94 if config.AUnitConfig != "" { 95 // Configuration defaults to AUnitConfig 96 log.Entry().Infof("AUnit Configuration: %s", config.AUnitConfig) 97 result, err := abaputils.ReadConfigFile(config.AUnitConfig) 98 if err != nil { 99 return aUnitConfig, err 100 } 101 err = json.Unmarshal(result, &aUnitConfig) 102 return aUnitConfig, err 103 104 } else if config.Repositories != "" { 105 // Fallback / EasyMode is the Repositories configuration 106 log.Entry().Infof("AUnit Configuration derived from: %s", config.Repositories) 107 repos, err := abaputils.GetRepositories((&abaputils.RepositoriesConfig{Repositories: config.Repositories})) 108 if err != nil { 109 return aUnitConfig, err 110 } 111 for _, repo := range repos { 112 aUnitConfig.ObjectSet.SoftwareComponents = append(aUnitConfig.ObjectSet.SoftwareComponents, abaputils.SoftwareComponents{Name: repo.Name}) 113 } 114 aUnitConfig.Title = "AUnit Test Run" 115 return aUnitConfig, nil 116 } else { 117 // Fail if no configuration is provided 118 return aUnitConfig, errors.New("No configuration provided - please provide either an AUnit configuration file or a repository configuration file") 119 } 120 } 121 122 func convertAUnitOptions(options *abapEnvironmentRunAUnitTestOptions) abaputils.AbapEnvironmentOptions { 123 subOptions := abaputils.AbapEnvironmentOptions{} 124 125 subOptions.CfAPIEndpoint = options.CfAPIEndpoint 126 subOptions.CfServiceInstance = options.CfServiceInstance 127 subOptions.CfServiceKeyName = options.CfServiceKeyName 128 subOptions.CfOrg = options.CfOrg 129 subOptions.CfSpace = options.CfSpace 130 subOptions.Host = options.Host 131 subOptions.Password = options.Password 132 subOptions.Username = options.Username 133 134 return subOptions 135 } 136 137 func fetchAndPersistAUnitResults(resp *http.Response, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, aunitResultFileName string, generateHTML bool) error { 138 var err error 139 var abapEndpoint string 140 abapEndpoint = details.URL 141 location := resp.Header.Get("Location") 142 details.URL = abapEndpoint + location 143 location, err = pollAUnitRun(details, nil, client) 144 if err == nil { 145 details.URL = abapEndpoint + location 146 resp, err = getAUnitResults("GET", details, nil, client) 147 } 148 //Parse response 149 var body []byte 150 if err == nil { 151 body, err = ioutil.ReadAll(resp.Body) 152 } 153 if err == nil { 154 defer resp.Body.Close() 155 err = persistAUnitResult(body, aunitResultFileName, generateHTML) 156 } 157 if err != nil { 158 return fmt.Errorf("Handling AUnit result failed: %w", err) 159 } 160 return nil 161 } 162 163 func buildAUnitRequestBody(config abapEnvironmentRunAUnitTestOptions) (bodyString string, err error) { 164 165 bodyString = "" 166 AUnitConfig, err := resolveAUnitConfiguration(config) 167 if err != nil { 168 return bodyString, err 169 } 170 171 //Checks before building the XML body 172 if AUnitConfig.Title == "" { 173 return bodyString, fmt.Errorf("Error while parsing AUnit test run config. No title for the AUnit run has been provided. Please configure an appropriate title for the respective test run") 174 } 175 if AUnitConfig.Context == "" { 176 AUnitConfig.Context = "ABAP Environment Pipeline" 177 } 178 if reflect.DeepEqual(abaputils.ObjectSet{}, AUnitConfig.ObjectSet) { 179 return bodyString, fmt.Errorf("Error while parsing AUnit test run object set config. No object set has been provided. Please configure the objects you want to be checked for the respective test run") 180 } 181 182 //Build Options 183 optionsString := buildAUnitOptionsString(AUnitConfig) 184 //Build metadata string 185 metadataString := `<aunit:run title="` + AUnitConfig.Title + `" context="` + AUnitConfig.Context + `" xmlns:aunit="http://www.sap.com/adt/api/aunit">` 186 //Build Object Set 187 objectSetString := abaputils.BuildOSLString(AUnitConfig.ObjectSet) 188 189 bodyString += `<?xml version="1.0" encoding="UTF-8"?>` + metadataString + optionsString + objectSetString + `</aunit:run>` 190 191 return bodyString, nil 192 } 193 194 func runAUnit(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 195 196 log.Entry().WithField("ABAP endpoint: ", details.URL).Info("Triggering AUnit run") 197 198 header := make(map[string][]string) 199 header["X-Csrf-Token"] = []string{details.XCsrfToken} 200 header["Content-Type"] = []string{"application/vnd.sap.adt.api.abapunit.run.v1+xml; charset=utf-8;"} 201 202 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 203 if err != nil { 204 return req, fmt.Errorf("Triggering AUnit run failed: %w", err) 205 } 206 defer req.Body.Close() 207 return req, err 208 } 209 210 func buildAUnitOptionsString(AUnitConfig AUnitConfig) (optionsString string) { 211 212 optionsString += `<aunit:options>` 213 if AUnitConfig.Options.Measurements != "" { 214 optionsString += `<aunit:measurements type="` + AUnitConfig.Options.Measurements + `"/>` 215 } else { 216 optionsString += `<aunit:measurements type="none"/>` 217 } 218 //We assume there must be one scope configured 219 optionsString += `<aunit:scope` 220 if AUnitConfig.Options.Scope.OwnTests != nil { 221 optionsString += ` ownTests="` + fmt.Sprintf("%v", *AUnitConfig.Options.Scope.OwnTests) + `"` 222 } else { 223 optionsString += ` ownTests="true"` 224 } 225 if AUnitConfig.Options.Scope.ForeignTests != nil { 226 optionsString += ` foreignTests="` + fmt.Sprintf("%v", *AUnitConfig.Options.Scope.ForeignTests) + `"` 227 } else { 228 optionsString += ` foreignTests="true"` 229 } 230 //We assume there must be one riskLevel configured 231 optionsString += `/><aunit:riskLevel` 232 if AUnitConfig.Options.RiskLevel.Harmless != nil { 233 optionsString += ` harmless="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Harmless) + `"` 234 } else { 235 optionsString += ` harmless="true"` 236 } 237 if AUnitConfig.Options.RiskLevel.Dangerous != nil { 238 optionsString += ` dangerous="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Dangerous) + `"` 239 } else { 240 optionsString += ` dangerous="true"` 241 } 242 if AUnitConfig.Options.RiskLevel.Critical != nil { 243 optionsString += ` critical="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Critical) + `"` 244 } else { 245 optionsString += ` critical="true"` 246 } 247 //We assume there must be one duration time configured 248 optionsString += `/><aunit:duration` 249 if AUnitConfig.Options.Duration.Short != nil { 250 optionsString += ` short="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Short) + `"` 251 } else { 252 optionsString += ` short="true"` 253 } 254 if AUnitConfig.Options.Duration.Medium != nil { 255 optionsString += ` medium="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Medium) + `"` 256 } else { 257 optionsString += ` medium="true"` 258 } 259 if AUnitConfig.Options.Duration.Long != nil { 260 optionsString += ` long="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Long) + `"` 261 } else { 262 optionsString += ` long="true"` 263 } 264 optionsString += `/></aunit:options>` 265 return optionsString 266 } 267 268 func fetchAUnitXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 269 270 log.Entry().WithField("ABAP Endpoint: ", details.URL).Debug("Fetching Xcrsf-Token") 271 272 details.URL += "/sap/bc/adt/api/abapunit/runs/00000000000000000000000000000000" 273 details.XCsrfToken = "fetch" 274 header := make(map[string][]string) 275 header["X-Csrf-Token"] = []string{details.XCsrfToken} 276 header["Accept"] = []string{"application/vnd.sap.adt.api.abapunit.run-status.v1+xml"} 277 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 278 if err != nil { 279 return "", fmt.Errorf("Fetching Xcsrf-Token failed: %w", err) 280 } 281 defer req.Body.Close() 282 283 token := req.Header.Get("X-Csrf-Token") 284 return token, err 285 } 286 287 func pollAUnitRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 288 289 log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling AUnit run status") 290 291 for { 292 resp, err := getHTTPResponseAUnitRun("GET", details, nil, client) 293 if err != nil { 294 return "", fmt.Errorf("Getting HTTP response failed: %w", err) 295 } 296 bodyText, err := ioutil.ReadAll(resp.Body) 297 if err != nil { 298 return "", fmt.Errorf("Reading response body failed: %w", err) 299 } 300 x := new(AUnitRun) 301 xml.Unmarshal(bodyText, &x) 302 303 log.Entry().Infof("Current polling status: %s", x.Progress.Status) 304 if x.Progress.Status == "Not Created" { 305 return "", err 306 } 307 if x.Progress.Status == "Completed" || x.Progress.Status == "FINISHED" { 308 return x.Link.Href, err 309 } 310 if x.Progress.Status == "" { 311 return "", fmt.Errorf("Could not get any response from AUnit poll: %w", errors.New("Status from AUnit run is empty. Either it's not an ABAP system or AUnit run hasn't started")) 312 } 313 time.Sleep(10 * time.Second) 314 } 315 } 316 317 func getHTTPResponseAUnitRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 318 319 log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Polling AUnit run status") 320 321 header := make(map[string][]string) 322 header["Accept"] = []string{"application/vnd.sap.adt.api.abapunit.run-status.v1+xml"} 323 324 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 325 if err != nil { 326 return req, fmt.Errorf("Getting AUnit run status failed: %w", err) 327 } 328 return req, err 329 } 330 331 func getAUnitResults(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 332 333 log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting AUnit results") 334 335 header := make(map[string][]string) 336 header["x-csrf-token"] = []string{details.XCsrfToken} 337 header["Accept"] = []string{"application/vnd.sap.adt.api.junit.run-result.v1+xml"} 338 339 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 340 if err != nil { 341 return req, fmt.Errorf("Getting AUnit run results failed: %w", err) 342 } 343 return req, err 344 } 345 346 func persistAUnitResult(body []byte, aunitResultFileName string, generateHTML bool) (err error) { 347 if len(body) == 0 { 348 return fmt.Errorf("Parsing AUnit result failed: %w", errors.New("Body is empty, can't parse empty body")) 349 } 350 351 responseBody := string(body) 352 log.Entry().Debugf("Response body: %s", responseBody) 353 354 //Optional checks before writing the Results 355 parsedXML := new(AUnitResult) 356 xml.Unmarshal([]byte(body), &parsedXML) 357 358 //Write Results 359 err = ioutil.WriteFile(aunitResultFileName, body, 0644) 360 if err != nil { 361 return fmt.Errorf("Writing results failed: %w", err) 362 } 363 log.Entry().Infof("Writing %s file was successful.", aunitResultFileName) 364 var reports []piperutils.Path 365 //Return before processing empty AUnit results --> XML can still be written with response body 366 if len(parsedXML.Testsuite.Testcase) == 0 { 367 log.Entry().Infof("There were no AUnit findings from this run. The response has been saved in the %s file", aunitResultFileName) 368 } else { 369 log.Entry().Infof("Please find the results from the respective AUnit run in the %s file or in below logs", aunitResultFileName) 370 //Logging of AUnit findings 371 log.Entry().Infof(`Here are the results for the AUnit test run '%s' executed by User %s on System %s in Client %s at %s. The AUnit run took %s seconds and contains %s tests with %s failures, %s errors, %s skipped and %s assert findings`, parsedXML.Title, parsedXML.System, parsedXML.ExecutedBy, parsedXML.Client, parsedXML.Timestamp, parsedXML.Time, parsedXML.Tests, parsedXML.Failures, parsedXML.Errors, parsedXML.Skipped, parsedXML.Asserts) 372 for _, s := range parsedXML.Testsuite.Testcase { 373 //Log Infos for testcase 374 //HTML Procesing can be done here 375 for _, failure := range s.Failure { 376 log.Entry().Debugf("%s, %s: %s found by %s", failure.Type, failure.Message, failure.Message, s.Classname) 377 } 378 for _, skipped := range s.Skipped { 379 log.Entry().Debugf("The following test has been skipped: %s: %s", skipped.Message, skipped.Text) 380 } 381 } 382 if generateHTML == true { 383 htmlString := generateHTMLDocumentAUnit(parsedXML) 384 htmlStringByte := []byte(htmlString) 385 aUnitResultHTMLFileName := strings.Trim(aunitResultFileName, ".xml") + ".html" 386 err = ioutil.WriteFile(aUnitResultHTMLFileName, htmlStringByte, 0644) 387 if err != nil { 388 return fmt.Errorf("Writing HTML document failed: %w", err) 389 } 390 log.Entry().Info("Writing " + aUnitResultHTMLFileName + " file was successful") 391 reports = append(reports, piperutils.Path{Target: aUnitResultHTMLFileName, Name: "ATC Results HTML file", Mandatory: true}) 392 } 393 } 394 //Persist findings afterwards 395 reports = append(reports, piperutils.Path{Target: aunitResultFileName, Name: "AUnit Results", Mandatory: true}) 396 piperutils.PersistReportsAndLinks("abapEnvironmentRunAUnitTest", "", reports, nil) 397 return nil 398 } 399 400 func generateHTMLDocumentAUnit(parsedXML *AUnitResult) (htmlDocumentString string) { 401 htmlDocumentString = `<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"><head><title>AUnit Results</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><style>table,th,td {border-collapse:collapse;}th,td{padding: 5px;text-align:left;font-size:medium;}</style></head><body><h1 style="text-align:left;font-size:large">AUnit Results</h1><table><tr><th>Run title</th><td style="padding-right: 20px">` + parsedXML.Title + `</td><th>System</th><td style="padding-right: 20px">` + parsedXML.System + `</td><th>Client</th><td style="padding-right: 20px">` + parsedXML.Client + `</td><th>ExecutedBy</th><td style="padding-right: 20px">` + parsedXML.ExecutedBy + `</td><th>Duration</th><td style="padding-right: 20px">` + parsedXML.Time + `s</td><th>Timestamp</th><td style="padding-right: 20px">` + parsedXML.Timestamp + `</td></tr><tr><th>Failures</th><td style="padding-right: 20px">` + parsedXML.Failures + `</td><th>Errors</th><td style="padding-right: 20px">` + parsedXML.Errors + `</td><th>Skipped</th><td style="padding-right: 20px">` + parsedXML.Skipped + `</td><th>Asserts</th><td style="padding-right: 20px">` + parsedXML.Asserts + `</td><th>Tests</th><td style="padding-right: 20px">` + parsedXML.Tests + `</td></tr></table><br><table style="width:100%; border: 1px solid black""><tr style="border: 1px solid black"><th style="border: 1px solid black">Severity</th><th style="border: 1px solid black">File</th><th style="border: 1px solid black">Message</th><th style="border: 1px solid black">Type</th><th style="border: 1px solid black">Text</th></tr>` 402 403 var htmlDocumentStringError, htmlDocumentStringWarning, htmlDocumentStringInfo, htmlDocumentStringDefault string 404 for _, s := range parsedXML.Testsuite.Testcase { 405 //Add coloring of lines inside of the respective severities, e.g. failures in red 406 trBackgroundColorTestcase := "grey" 407 trBackgroundColorError := "rgba(227,85,0)" 408 trBackgroundColorFailure := "rgba(227,85,0)" 409 trBackgroundColorSkipped := "rgba(255,175,0, 0.2)" 410 if (len(s.Error) != 0) || (len(s.Failure) != 0) || (len(s.Skipped) != 0) { 411 htmlDocumentString += `<tr style="background-color: ` + trBackgroundColorTestcase + `"><td colspan="5"><b>Testcase: ` + s.Name + ` for class ` + s.Classname + `</b></td></tr>` 412 } 413 for _, t := range s.Error { 414 htmlDocumentString += `<tr style="background-color: ` + trBackgroundColorError + `"><td style="border: 1px solid black">Failure</td><td style="border: 1px solid black">` + s.Classname + `</td><td style="border: 1px solid black">` + t.Message + `</td><td style="border: 1px solid black">` + t.Type + `</td><td style="border: 1px solid black">` + t.Text + `</td></tr>` 415 } 416 for _, t := range s.Failure { 417 htmlDocumentString += `<tr style="background-color: ` + trBackgroundColorFailure + `"><td style="border: 1px solid black">Failure</td><td style="border: 1px solid black">` + s.Classname + `</td><td style="border: 1px solid black">` + t.Message + `</td><td style="border: 1px solid black">` + t.Type + `</td><td style="border: 1px solid black">` + t.Text + `</td></tr>` 418 } 419 for _, t := range s.Skipped { 420 htmlDocumentString += `<tr style="background-color: ` + trBackgroundColorSkipped + `"><td style="border: 1px solid black">Failure</td><td style="border: 1px solid black">` + s.Classname + `</td><td style="border: 1px solid black">` + t.Message + `</td><td style="border: 1px solid black">-</td><td style="border: 1px solid black">` + t.Text + `</td></tr>` 421 } 422 } 423 if len(parsedXML.Testsuite.Testcase) == 0 { 424 htmlDocumentString += `<tr><td colspan="5"><b>There are no AUnit findings to be displayed</b></td></tr>` 425 } 426 htmlDocumentString += htmlDocumentStringError + htmlDocumentStringWarning + htmlDocumentStringInfo + htmlDocumentStringDefault + `</table></body></html>` 427 428 return htmlDocumentString 429 } 430 431 // 432 // Object Set Structure 433 // 434 435 //AUnitConfig object for parsing yaml config of software components and packages 436 type AUnitConfig struct { 437 Title string `json:"title,omitempty"` 438 Context string `json:"context,omitempty"` 439 Options AUnitOptions `json:"options,omitempty"` 440 ObjectSet abaputils.ObjectSet `json:"objectset,omitempty"` 441 } 442 443 //AUnitOptions in form of packages and software components to be checked 444 type AUnitOptions struct { 445 Measurements string `json:"measurements,omitempty"` 446 Scope Scope `json:"scope,omitempty"` 447 RiskLevel RiskLevel `json:"risklevel,omitempty"` 448 Duration Duration `json:"duration,omitempty"` 449 } 450 451 //Scope in form of packages and software components to be checked 452 type Scope struct { 453 OwnTests *bool `json:"owntests,omitempty"` 454 ForeignTests *bool `json:"foreigntests,omitempty"` 455 } 456 457 //RiskLevel in form of packages and software components to be checked 458 type RiskLevel struct { 459 Harmless *bool `json:"harmless,omitempty"` 460 Dangerous *bool `json:"dangerous,omitempty"` 461 Critical *bool `json:"critical,omitempty"` 462 } 463 464 //Duration in form of packages and software components to be checked 465 type Duration struct { 466 Short *bool `json:"short,omitempty"` 467 Medium *bool `json:"medium,omitempty"` 468 Long *bool `json:"long,omitempty"` 469 } 470 471 // 472 // AUnit Run Structure 473 // 474 475 //AUnitRun Object for parsing XML 476 type AUnitRun struct { 477 XMLName xml.Name `xml:"run"` 478 Title string `xml:"title,attr"` 479 Context string `xml:"context,attr"` 480 Progress Progress `xml:"progress"` 481 ExecutedBy ExecutedBy `xml:"executedBy"` 482 Time Time `xml:"time"` 483 Link AUnitLink `xml:"link"` 484 } 485 486 //Progress of AUnit run 487 type Progress struct { 488 Status string `xml:"status,attr"` 489 Percentage string `xml:"percentage,attr"` 490 } 491 492 //ExecutedBy User 493 type ExecutedBy struct { 494 User string `xml:"user,attr"` 495 } 496 497 //Time run was started and finished 498 type Time struct { 499 Started string `xml:"started,attr"` 500 Ended string `xml:"ended,attr"` 501 } 502 503 //AUnitLink containing result locations 504 type AUnitLink struct { 505 Href string `xml:"href,attr"` 506 Rel string `xml:"rel,attr"` 507 Type string `xml:"type,attr"` 508 } 509 510 // 511 // AUnit Result Structure 512 // 513 514 type AUnitResult struct { 515 XMLName xml.Name `xml:"testsuites"` 516 Title string `xml:"title,attr"` 517 System string `xml:"system,attr"` 518 Client string `xml:"client,attr"` 519 ExecutedBy string `xml:"executedBy,attr"` 520 Time string `xml:"time,attr"` 521 Timestamp string `xml:"timestamp,attr"` 522 Failures string `xml:"failures,attr"` 523 Errors string `xml:"errors,attr"` 524 Skipped string `xml:"skipped,attr"` 525 Asserts string `xml:"asserts,attr"` 526 Tests string `xml:"tests,attr"` 527 Testsuite struct { 528 Tests string `xml:"tests,attr"` 529 Asserts string `xml:"asserts,attr"` 530 Skipped string `xml:"skipped,attr"` 531 Errors string `xml:"errors,attr"` 532 Failures string `xml:"failures,attr"` 533 Timestamp string `xml:"timestamp,attr"` 534 Time string `xml:"time,attr"` 535 Hostname string `xml:"hostname,attr"` 536 Package string `xml:"package,attr"` 537 Name string `xml:"name,attr"` 538 Testcase []struct { 539 Asserts string `xml:"asserts,attr"` 540 Time string `xml:"time,attr"` 541 Name string `xml:"name,attr"` 542 Classname string `xml:"classname,attr"` 543 Error []struct { 544 Text string `xml:",chardata"` 545 Type string `xml:"type,attr"` 546 Message string `xml:"message,attr"` 547 } `xml:"error"` 548 Failure []struct { 549 Text string `xml:",chardata"` 550 Type string `xml:"type,attr"` 551 Message string `xml:"message,attr"` 552 } `xml:"failure"` 553 Skipped []struct { 554 Text string `xml:",chardata"` 555 Message string `xml:"message,attr"` 556 } `xml:"skipped"` 557 } `xml:"testcase"` 558 } `xml:"testsuite"` 559 }