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