github.com/xgoffin/jenkins-library@v1.154.0/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  type abapEnvironmentRunAUnitTestUtils interface {
    25  	command.ExecRunner
    26  
    27  	FileExists(filename string) (bool, error)
    28  
    29  	// Add more methods here, or embed additional interfaces, or remove/replace as required.
    30  	// The abapEnvironmentRunAUnitTestUtils interface should be descriptive of your runtime dependencies,
    31  	// i.e. include everything you need to be able to mock in tests.
    32  	// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
    33  }
    34  
    35  type abapEnvironmentRunAUnitTestUtilsBundle struct {
    36  	*command.Command
    37  	*piperutils.Files
    38  
    39  	// Embed more structs as necessary to implement methods or interfaces you add to abapEnvironmentRunAUnitTestUtils.
    40  	// Structs embedded in this way must each have a unique set of methods attached.
    41  	// If there is no struct which implements the method you need, attach the method to
    42  	// abapEnvironmentRunAUnitTestUtilsBundle and forward to the implementation of the dependency.
    43  }
    44  
    45  func newAbapEnvironmentRunAUnitTestUtils() abapEnvironmentRunAUnitTestUtils {
    46  	utils := abapEnvironmentRunAUnitTestUtilsBundle{
    47  		Command: &command.Command{},
    48  		Files:   &piperutils.Files{},
    49  	}
    50  	// Reroute command output to logging framework
    51  	utils.Stdout(log.Writer())
    52  	utils.Stderr(log.Writer())
    53  	return &utils
    54  }
    55  
    56  func abapEnvironmentRunAUnitTest(config abapEnvironmentRunAUnitTestOptions, telemetryData *telemetry.CustomData) {
    57  
    58  	// for command execution use Command
    59  	c := command.Command{}
    60  	// reroute command output to logging framework
    61  	c.Stdout(log.Writer())
    62  	c.Stderr(log.Writer())
    63  
    64  	var autils = abaputils.AbapUtils{
    65  		Exec: &c,
    66  	}
    67  
    68  	client := piperhttp.Client{}
    69  
    70  	// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
    71  	err := runAbapEnvironmentRunAUnitTest(&config, telemetryData, &autils, &client)
    72  	if err != nil {
    73  		log.Entry().WithError(err).Fatal("step execution failed")
    74  	}
    75  }
    76  
    77  func runAbapEnvironmentRunAUnitTest(config *abapEnvironmentRunAUnitTestOptions, telemetryData *telemetry.CustomData, com abaputils.Communication, client piperhttp.Sender) error {
    78  	var details abaputils.ConnectionDetailsHTTP
    79  	subOptions := convertAUnitOptions(config)
    80  	details, err := com.GetAbapCommunicationArrangementInfo(subOptions, "")
    81  	var resp *http.Response
    82  	cookieJar, _ := cookiejar.New(nil)
    83  	//Fetch Xcrsf-Token
    84  	if err == nil {
    85  		credentialsOptions := piperhttp.ClientOptions{
    86  			Username:  details.User,
    87  			Password:  details.Password,
    88  			CookieJar: cookieJar,
    89  		}
    90  		client.SetOptions(credentialsOptions)
    91  		details.XCsrfToken, err = fetchAUnitXcsrfToken("GET", details, nil, client)
    92  	}
    93  	if err == nil {
    94  		resp, err = triggerAUnitrun(*config, details, client)
    95  	}
    96  	if err == nil {
    97  		err = fetchAndPersistAUnitResults(resp, details, client, config.AUnitResultsFileName, config.GenerateHTML)
    98  	}
    99  	if err != nil {
   100  		log.Entry().WithError(err).Fatal("step execution failed")
   101  	}
   102  	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")
   103  	return nil
   104  }
   105  
   106  func triggerAUnitrun(config abapEnvironmentRunAUnitTestOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (*http.Response, error) {
   107  
   108  	abapEndpoint := details.URL
   109  	bodyString, err := buildAUnitRequestBody(config)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	//Trigger AUnit run
   115  	var resp *http.Response
   116  
   117  	var body = []byte(bodyString)
   118  	log.Entry().Debugf("Request Body: %s", bodyString)
   119  	details.URL = abapEndpoint + "/sap/bc/adt/api/abapunit/runs"
   120  	resp, err = runAUnit("POST", details, body, client)
   121  	return resp, err
   122  }
   123  
   124  func resolveAUnitConfiguration(config abapEnvironmentRunAUnitTestOptions) (aUnitConfig AUnitConfig, err error) {
   125  
   126  	if config.AUnitConfig != "" {
   127  		// Configuration defaults to AUnitConfig
   128  		log.Entry().Infof("AUnit Configuration: %s", config.AUnitConfig)
   129  		result, err := abaputils.ReadConfigFile(config.AUnitConfig)
   130  		if err != nil {
   131  			return aUnitConfig, err
   132  		}
   133  		err = json.Unmarshal(result, &aUnitConfig)
   134  		return aUnitConfig, err
   135  
   136  	} else if config.Repositories != "" {
   137  		// Fallback / EasyMode is the Repositories configuration
   138  		log.Entry().Infof("AUnit Configuration derived from: %s", config.Repositories)
   139  		repos, err := abaputils.GetRepositories((&abaputils.RepositoriesConfig{Repositories: config.Repositories}))
   140  		if err != nil {
   141  			return aUnitConfig, err
   142  		}
   143  		for _, repo := range repos {
   144  			aUnitConfig.ObjectSet.SoftwareComponents = append(aUnitConfig.ObjectSet.SoftwareComponents, SoftwareComponents{Name: repo.Name})
   145  		}
   146  		aUnitConfig.Title = "AUnit Test Run"
   147  		return aUnitConfig, nil
   148  	} else {
   149  		// Fail if no configuration is provided
   150  		return aUnitConfig, errors.New("No configuration provided - please provide either an AUnit configuration file or a repository configuration file")
   151  	}
   152  }
   153  
   154  func convertAUnitOptions(options *abapEnvironmentRunAUnitTestOptions) abaputils.AbapEnvironmentOptions {
   155  	subOptions := abaputils.AbapEnvironmentOptions{}
   156  
   157  	subOptions.CfAPIEndpoint = options.CfAPIEndpoint
   158  	subOptions.CfServiceInstance = options.CfServiceInstance
   159  	subOptions.CfServiceKeyName = options.CfServiceKeyName
   160  	subOptions.CfOrg = options.CfOrg
   161  	subOptions.CfSpace = options.CfSpace
   162  	subOptions.Host = options.Host
   163  	subOptions.Password = options.Password
   164  	subOptions.Username = options.Username
   165  
   166  	return subOptions
   167  }
   168  
   169  func fetchAndPersistAUnitResults(resp *http.Response, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, aunitResultFileName string, generateHTML bool) error {
   170  	var err error
   171  	var abapEndpoint string
   172  	abapEndpoint = details.URL
   173  	location := resp.Header.Get("Location")
   174  	details.URL = abapEndpoint + location
   175  	location, err = pollAUnitRun(details, nil, client)
   176  	if err == nil {
   177  		details.URL = abapEndpoint + location
   178  		resp, err = getAUnitResults("GET", details, nil, client)
   179  	}
   180  	//Parse response
   181  	var body []byte
   182  	if err == nil {
   183  		body, err = ioutil.ReadAll(resp.Body)
   184  	}
   185  	if err == nil {
   186  		defer resp.Body.Close()
   187  		err = persistAUnitResult(body, aunitResultFileName, generateHTML)
   188  	}
   189  	if err != nil {
   190  		return fmt.Errorf("Handling AUnit result failed: %w", err)
   191  	}
   192  	return nil
   193  }
   194  
   195  func buildAUnitRequestBody(config abapEnvironmentRunAUnitTestOptions) (bodyString string, err error) {
   196  
   197  	bodyString = ""
   198  	AUnitConfig, err := resolveAUnitConfiguration(config)
   199  	if err != nil {
   200  		return bodyString, err
   201  	}
   202  
   203  	//Checks before building the XML body
   204  	if AUnitConfig.Title == "" {
   205  		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")
   206  	}
   207  	if AUnitConfig.Context == "" {
   208  		AUnitConfig.Context = "ABAP Environment Pipeline"
   209  	}
   210  	if reflect.DeepEqual(ObjectSet{}, AUnitConfig.ObjectSet) {
   211  		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")
   212  	}
   213  
   214  	//Build Options
   215  	optionsString := buildAUnitOptionsString(AUnitConfig)
   216  	//Build metadata string
   217  	metadataString := `<aunit:run title="` + AUnitConfig.Title + `" context="` + AUnitConfig.Context + `" xmlns:aunit="http://www.sap.com/adt/api/aunit">`
   218  	//Build Object Set
   219  	objectSetString := buildAUnitObjectSetString(AUnitConfig)
   220  
   221  	bodyString += `<?xml version="1.0" encoding="UTF-8"?>` + metadataString + optionsString + objectSetString + `</aunit:run>`
   222  
   223  	return bodyString, nil
   224  }
   225  
   226  func runAUnit(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {
   227  
   228  	log.Entry().WithField("ABAP endpoint: ", details.URL).Info("Triggering AUnit run")
   229  
   230  	header := make(map[string][]string)
   231  	header["X-Csrf-Token"] = []string{details.XCsrfToken}
   232  	header["Content-Type"] = []string{"application/vnd.sap.adt.api.abapunit.run.v1+xml; charset=utf-8;"}
   233  
   234  	req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil)
   235  	if err != nil {
   236  		return req, fmt.Errorf("Triggering AUnit run failed: %w", err)
   237  	}
   238  	defer req.Body.Close()
   239  	return req, err
   240  }
   241  
   242  func buildAUnitOptionsString(AUnitConfig AUnitConfig) (optionsString string) {
   243  
   244  	optionsString += `<aunit:options>`
   245  	if AUnitConfig.Options.Measurements != "" {
   246  		optionsString += `<aunit:measurements type="` + AUnitConfig.Options.Measurements + `"/>`
   247  	} else {
   248  		optionsString += `<aunit:measurements type="none"/>`
   249  	}
   250  	//We assume there must be one scope configured
   251  	optionsString += `<aunit:scope`
   252  	if AUnitConfig.Options.Scope.OwnTests != nil {
   253  		optionsString += ` ownTests="` + fmt.Sprintf("%v", *AUnitConfig.Options.Scope.OwnTests) + `"`
   254  	} else {
   255  		optionsString += ` ownTests="true"`
   256  	}
   257  	if AUnitConfig.Options.Scope.ForeignTests != nil {
   258  		optionsString += ` foreignTests="` + fmt.Sprintf("%v", *AUnitConfig.Options.Scope.ForeignTests) + `"`
   259  	} else {
   260  		optionsString += ` foreignTests="true"`
   261  	}
   262  	//We assume there must be one riskLevel configured
   263  	optionsString += `/><aunit:riskLevel`
   264  	if AUnitConfig.Options.RiskLevel.Harmless != nil {
   265  		optionsString += ` harmless="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Harmless) + `"`
   266  	} else {
   267  		optionsString += ` harmless="true"`
   268  	}
   269  	if AUnitConfig.Options.RiskLevel.Dangerous != nil {
   270  		optionsString += ` dangerous="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Dangerous) + `"`
   271  	} else {
   272  		optionsString += ` dangerous="true"`
   273  	}
   274  	if AUnitConfig.Options.RiskLevel.Critical != nil {
   275  		optionsString += ` critical="` + fmt.Sprintf("%v", *AUnitConfig.Options.RiskLevel.Critical) + `"`
   276  	} else {
   277  		optionsString += ` critical="true"`
   278  	}
   279  	//We assume there must be one duration time configured
   280  	optionsString += `/><aunit:duration`
   281  	if AUnitConfig.Options.Duration.Short != nil {
   282  		optionsString += ` short="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Short) + `"`
   283  	} else {
   284  		optionsString += ` short="true"`
   285  	}
   286  	if AUnitConfig.Options.Duration.Medium != nil {
   287  		optionsString += ` medium="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Medium) + `"`
   288  	} else {
   289  		optionsString += ` medium="true"`
   290  	}
   291  	if AUnitConfig.Options.Duration.Long != nil {
   292  		optionsString += ` long="` + fmt.Sprintf("%v", *AUnitConfig.Options.Duration.Long) + `"`
   293  	} else {
   294  		optionsString += ` long="true"`
   295  	}
   296  	optionsString += `/></aunit:options>`
   297  	return optionsString
   298  }
   299  
   300  func writeObjectSetProperties(set MultiPropertySet) (objectSetString string) {
   301  	for _, packages := range set.PackageNames {
   302  		objectSetString += `<osl:package name="` + packages.Name + `"/>`
   303  	}
   304  	for _, objectTypeGroup := range set.ObjectTypeGroups {
   305  		objectSetString += `<osl:objectTypeGroup name="` + objectTypeGroup.Name + `"/>`
   306  	}
   307  	for _, objectType := range set.ObjectTypes {
   308  		objectSetString += `<osl:objectType name="` + objectType.Name + `"/>`
   309  	}
   310  	for _, owner := range set.Owners {
   311  		objectSetString += `<osl:owner name="` + owner.Name + `"/>`
   312  	}
   313  	for _, releaseState := range set.ReleaseStates {
   314  		objectSetString += `<osl:releaseState value="` + releaseState.Value + `"/>`
   315  	}
   316  	for _, version := range set.Versions {
   317  		objectSetString += `<osl:version value="` + version.Value + `"/>`
   318  	}
   319  	for _, applicationComponent := range set.ApplicationComponents {
   320  		objectSetString += `<osl:applicationComponent name="` + applicationComponent.Name + `"/>`
   321  	}
   322  	for _, component := range set.SoftwareComponents {
   323  		objectSetString += `<osl:softwareComponent name="` + component.Name + `"/>`
   324  	}
   325  	for _, transportLayer := range set.TransportLayers {
   326  		objectSetString += `<osl:transportLayer name="` + transportLayer.Name + `"/>`
   327  	}
   328  	for _, language := range set.Languages {
   329  		objectSetString += `<osl:language value="` + language.Value + `"/>`
   330  	}
   331  	for _, sourceSystem := range set.SourceSystems {
   332  		objectSetString += `<osl:sourceSystem name="` + sourceSystem.Name + `"/>`
   333  	}
   334  	return objectSetString
   335  }
   336  
   337  func buildAUnitObjectSetString(AUnitConfig AUnitConfig) (objectSetString string) {
   338  
   339  	//Build ObjectSets
   340  	s := AUnitConfig.ObjectSet
   341  	if s.Type == "" {
   342  		s.Type = "multiPropertySet"
   343  	}
   344  	if s.Type != "multiPropertySet" {
   345  		log.Entry().Infof("Wrong configuration has been detected: %s has been used. This is currently not supported and this set will not be included in this run. Please check the step documentation for more information", s.Type)
   346  	} else {
   347  		objectSetString += `<osl:objectSet xsi:type="` + s.Type + `" xmlns:osl="http://www.sap.com/api/osl" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">`
   348  
   349  		if !(reflect.DeepEqual(s.PackageNames, AUnitPackage{})) || !(reflect.DeepEqual(s.SoftwareComponents, SoftwareComponents{})) {
   350  			//To ensure Scomps and packages can be assigned on this level
   351  			mps := MultiPropertySet{
   352  				PackageNames:       s.PackageNames,
   353  				SoftwareComponents: s.SoftwareComponents,
   354  			}
   355  			objectSetString += writeObjectSetProperties(mps)
   356  		}
   357  
   358  		objectSetString += writeObjectSetProperties(s.MultiPropertySet)
   359  
   360  		if !(reflect.DeepEqual(s.MultiPropertySet, MultiPropertySet{})) {
   361  			log.Entry().Info("Wrong configuration has been detected: MultiPropertySet has been used. Please note that there is no official documentation for this usage. Please check the step documentation for more information")
   362  		}
   363  
   364  		for _, t := range s.Set {
   365  			log.Entry().Infof("Wrong configuration has been detected: %s has been used. This is currently not supported and this set will not be included in this run. Please check the step documentation for more information", t.Type)
   366  		}
   367  		objectSetString += `</osl:objectSet>`
   368  	}
   369  	return objectSetString
   370  }
   371  
   372  func fetchAUnitXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) {
   373  
   374  	log.Entry().WithField("ABAP Endpoint: ", details.URL).Debug("Fetching Xcrsf-Token")
   375  
   376  	details.URL += "/sap/bc/adt/api/abapunit/runs/00000000000000000000000000000000"
   377  	details.XCsrfToken = "fetch"
   378  	header := make(map[string][]string)
   379  	header["X-Csrf-Token"] = []string{details.XCsrfToken}
   380  	header["Accept"] = []string{"application/vnd.sap.adt.api.abapunit.run-status.v1+xml"}
   381  	req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil)
   382  	if err != nil {
   383  		return "", fmt.Errorf("Fetching Xcsrf-Token failed: %w", err)
   384  	}
   385  	defer req.Body.Close()
   386  
   387  	token := req.Header.Get("X-Csrf-Token")
   388  	return token, err
   389  }
   390  
   391  func pollAUnitRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) {
   392  
   393  	log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling AUnit run status")
   394  
   395  	for {
   396  		resp, err := getHTTPResponseAUnitRun("GET", details, nil, client)
   397  		if err != nil {
   398  			return "", fmt.Errorf("Getting HTTP response failed: %w", err)
   399  		}
   400  		bodyText, err := ioutil.ReadAll(resp.Body)
   401  		if err != nil {
   402  			return "", fmt.Errorf("Reading response body failed: %w", err)
   403  		}
   404  		x := new(AUnitRun)
   405  		xml.Unmarshal(bodyText, &x)
   406  
   407  		log.Entry().Infof("Current polling status: %s", x.Progress.Status)
   408  		if x.Progress.Status == "Not Created" {
   409  			return "", err
   410  		}
   411  		if x.Progress.Status == "Completed" || x.Progress.Status == "FINISHED" {
   412  			return x.Link.Href, err
   413  		}
   414  		if x.Progress.Status == "" {
   415  			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"))
   416  		}
   417  		time.Sleep(10 * time.Second)
   418  	}
   419  }
   420  
   421  func getHTTPResponseAUnitRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {
   422  
   423  	log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Polling AUnit run status")
   424  
   425  	header := make(map[string][]string)
   426  	header["Accept"] = []string{"application/vnd.sap.adt.api.abapunit.run-status.v1+xml"}
   427  
   428  	req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil)
   429  	if err != nil {
   430  		return req, fmt.Errorf("Getting AUnit run status failed: %w", err)
   431  	}
   432  	return req, err
   433  }
   434  
   435  func getAUnitResults(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {
   436  
   437  	log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting AUnit results")
   438  
   439  	header := make(map[string][]string)
   440  	header["x-csrf-token"] = []string{details.XCsrfToken}
   441  	header["Accept"] = []string{"application/vnd.sap.adt.api.junit.run-result.v1+xml"}
   442  
   443  	req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil)
   444  	if err != nil {
   445  		return req, fmt.Errorf("Getting AUnit run results failed: %w", err)
   446  	}
   447  	return req, err
   448  }
   449  
   450  func persistAUnitResult(body []byte, aunitResultFileName string, generateHTML bool) (err error) {
   451  	if len(body) == 0 {
   452  		return fmt.Errorf("Parsing AUnit result failed: %w", errors.New("Body is empty, can't parse empty body"))
   453  	}
   454  
   455  	responseBody := string(body)
   456  	log.Entry().Debugf("Response body: %s", responseBody)
   457  
   458  	//Optional checks before writing the Results
   459  	parsedXML := new(AUnitResult)
   460  	xml.Unmarshal([]byte(body), &parsedXML)
   461  
   462  	//Write Results
   463  	err = ioutil.WriteFile(aunitResultFileName, body, 0644)
   464  	if err != nil {
   465  		return fmt.Errorf("Writing results failed: %w", err)
   466  	}
   467  	log.Entry().Infof("Writing %s file was successful.", aunitResultFileName)
   468  	var reports []piperutils.Path
   469  	//Return before processing empty AUnit results --> XML can still be written with response body
   470  	if len(parsedXML.Testsuite.Testcase) == 0 {
   471  		log.Entry().Infof("There were no AUnit findings from this run. The response has been saved in the %s file", aunitResultFileName)
   472  	} else {
   473  		log.Entry().Infof("Please find the results from the respective AUnit run in the %s file or in below logs", aunitResultFileName)
   474  		//Logging of AUnit findings
   475  		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)
   476  		for _, s := range parsedXML.Testsuite.Testcase {
   477  			//Log Infos for testcase
   478  			//HTML Procesing can be done here
   479  			for _, failure := range s.Failure {
   480  				log.Entry().Debugf("%s, %s: %s found by %s", failure.Type, failure.Message, failure.Message, s.Classname)
   481  			}
   482  			for _, skipped := range s.Skipped {
   483  				log.Entry().Debugf("The following test has been skipped: %s: %s", skipped.Message, skipped.Text)
   484  			}
   485  		}
   486  		if generateHTML == true {
   487  			htmlString := generateHTMLDocumentAUnit(parsedXML)
   488  			htmlStringByte := []byte(htmlString)
   489  			aUnitResultHTMLFileName := strings.Trim(aunitResultFileName, ".xml") + ".html"
   490  			err = ioutil.WriteFile(aUnitResultHTMLFileName, htmlStringByte, 0644)
   491  			if err != nil {
   492  				return fmt.Errorf("Writing HTML document failed: %w", err)
   493  			}
   494  			log.Entry().Info("Writing " + aUnitResultHTMLFileName + " file was successful")
   495  			reports = append(reports, piperutils.Path{Target: aUnitResultHTMLFileName, Name: "ATC Results HTML file", Mandatory: true})
   496  		}
   497  	}
   498  	//Persist findings afterwards
   499  	reports = append(reports, piperutils.Path{Target: aunitResultFileName, Name: "AUnit Results", Mandatory: true})
   500  	piperutils.PersistReportsAndLinks("abapEnvironmentRunAUnitTest", "", reports, nil)
   501  	return nil
   502  }
   503  
   504  func generateHTMLDocumentAUnit(parsedXML *AUnitResult) (htmlDocumentString string) {
   505  	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>`
   506  
   507  	var htmlDocumentStringError, htmlDocumentStringWarning, htmlDocumentStringInfo, htmlDocumentStringDefault string
   508  	for _, s := range parsedXML.Testsuite.Testcase {
   509  		//Add coloring of lines inside of the respective severities, e.g. failures in red
   510  		trBackgroundColorTestcase := "grey"
   511  		trBackgroundColorError := "rgba(227,85,0)"
   512  		trBackgroundColorFailure := "rgba(227,85,0)"
   513  		trBackgroundColorSkipped := "rgba(255,175,0, 0.2)"
   514  		if (len(s.Error) != 0) || (len(s.Failure) != 0) || (len(s.Skipped) != 0) {
   515  			htmlDocumentString += `<tr style="background-color: ` + trBackgroundColorTestcase + `"><td colspan="5"><b>Testcase: ` + s.Name + ` for class ` + s.Classname + `</b></td></tr>`
   516  		}
   517  		for _, t := range s.Error {
   518  			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>`
   519  		}
   520  		for _, t := range s.Failure {
   521  			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>`
   522  		}
   523  		for _, t := range s.Skipped {
   524  			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>`
   525  		}
   526  	}
   527  	if len(parsedXML.Testsuite.Testcase) == 0 {
   528  		htmlDocumentString += `<tr><td colspan="5"><b>There are no AUnit findings to be displayed</b></td></tr>`
   529  	}
   530  	htmlDocumentString += htmlDocumentStringError + htmlDocumentStringWarning + htmlDocumentStringInfo + htmlDocumentStringDefault + `</table></body></html>`
   531  
   532  	return htmlDocumentString
   533  }
   534  
   535  //
   536  //	Object Set Structure
   537  //
   538  
   539  //AUnitConfig object for parsing yaml config of software components and packages
   540  type AUnitConfig struct {
   541  	Title     string       `json:"title,omitempty"`
   542  	Context   string       `json:"context,omitempty"`
   543  	Options   AUnitOptions `json:"options,omitempty"`
   544  	ObjectSet ObjectSet    `json:"objectset,omitempty"`
   545  }
   546  
   547  //AUnitOptions in form of packages and software components to be checked
   548  type AUnitOptions struct {
   549  	Measurements string    `json:"measurements,omitempty"`
   550  	Scope        Scope     `json:"scope,omitempty"`
   551  	RiskLevel    RiskLevel `json:"risklevel,omitempty"`
   552  	Duration     Duration  `json:"duration,omitempty"`
   553  }
   554  
   555  //Scope in form of packages and software components to be checked
   556  type Scope struct {
   557  	OwnTests     *bool `json:"owntests,omitempty"`
   558  	ForeignTests *bool `json:"foreigntests,omitempty"`
   559  }
   560  
   561  //RiskLevel in form of packages and software components to be checked
   562  type RiskLevel struct {
   563  	Harmless  *bool `json:"harmless,omitempty"`
   564  	Dangerous *bool `json:"dangerous,omitempty"`
   565  	Critical  *bool `json:"critical,omitempty"`
   566  }
   567  
   568  //Duration in form of packages and software components to be checked
   569  type Duration struct {
   570  	Short  *bool `json:"short,omitempty"`
   571  	Medium *bool `json:"medium,omitempty"`
   572  	Long   *bool `json:"long,omitempty"`
   573  }
   574  
   575  //ObjectSet in form of packages and software components to be checked
   576  type ObjectSet struct {
   577  	PackageNames       []AUnitPackage       `json:"packages,omitempty"`
   578  	SoftwareComponents []SoftwareComponents `json:"softwarecomponents,omitempty"`
   579  	Type               string               `json:"type,omitempty"`
   580  	MultiPropertySet   MultiPropertySet     `json:"multipropertyset,omitempty"`
   581  	Set                []Set                `json:"set,omitempty"`
   582  }
   583  
   584  //MultiPropertySet that can possibly contain any subsets/object of the OSL
   585  type MultiPropertySet struct {
   586  	Type                  string                 `json:"type,omitempty"`
   587  	PackageNames          []AUnitPackage         `json:"packages,omitempty"`
   588  	ObjectTypeGroups      []ObjectTypeGroup      `json:"objecttypegroups,omitempty"`
   589  	ObjectTypes           []ObjectType           `json:"objecttypes,omitempty"`
   590  	Owners                []Owner                `json:"owners,omitempty"`
   591  	ReleaseStates         []ReleaseState         `json:"releasestates,omitempty"`
   592  	Versions              []Version              `json:"versions,omitempty"`
   593  	ApplicationComponents []ApplicationComponent `json:"applicationcomponents,omitempty"`
   594  	SoftwareComponents    []SoftwareComponents   `json:"softwarecomponents,omitempty"`
   595  	TransportLayers       []TransportLayer       `json:"transportlayers,omitempty"`
   596  	Languages             []Language             `json:"languages,omitempty"`
   597  	SourceSystems         []SourceSystem         `json:"sourcesystems,omitempty"`
   598  }
   599  
   600  //Set
   601  type Set struct {
   602  	Type          string               `json:"type,omitempty"`
   603  	Set           []Set                `json:"set,omitempty"`
   604  	PackageSet    []AUnitPackageSet    `json:"package,omitempty"`
   605  	FlatObjectSet []AUnitFlatObjectSet `json:"object,omitempty"`
   606  	ComponentSet  []AUnitComponentSet  `json:"component,omitempty"`
   607  	TransportSet  []AUnitTransportSet  `json:"transport,omitempty"`
   608  	ObjectTypeSet []AUnitObjectTypeSet `json:"objecttype,omitempty"`
   609  }
   610  
   611  //AUnitPackageSet in form of packages to be checked
   612  type AUnitPackageSet struct {
   613  	Name               string `json:"name,omitempty"`
   614  	IncludeSubpackages *bool  `json:"includesubpackages,omitempty"`
   615  }
   616  
   617  //AUnitFlatObjectSet
   618  type AUnitFlatObjectSet struct {
   619  	Name string `json:"name,omitempty"`
   620  	Type string `json:"type,omitempty"`
   621  }
   622  
   623  //AUnitComponentSet in form of software components to be checked
   624  type AUnitComponentSet struct {
   625  	Name string `json:"name,omitempty"`
   626  }
   627  
   628  //AUnitTransportSet in form of transports to be checked
   629  type AUnitTransportSet struct {
   630  	Number string `json:"number,omitempty"`
   631  }
   632  
   633  //AUnitObjectTypeSet
   634  type AUnitObjectTypeSet struct {
   635  	Name string `json:"name,omitempty"`
   636  }
   637  
   638  //AUnitPackage for MPS
   639  type AUnitPackage struct {
   640  	Name string `json:"name,omitempty"`
   641  }
   642  
   643  //ObjectTypeGroup
   644  type ObjectTypeGroup struct {
   645  	Name string `json:"name,omitempty"`
   646  }
   647  
   648  //ObjectType
   649  type ObjectType struct {
   650  	Name string `json:"name,omitempty"`
   651  }
   652  
   653  //Owner
   654  type Owner struct {
   655  	Name string `json:"name,omitempty"`
   656  }
   657  
   658  //ReleaseState
   659  type ReleaseState struct {
   660  	Value string `json:"value,omitempty"`
   661  }
   662  
   663  //Version
   664  type Version struct {
   665  	Value string `json:"value,omitempty"`
   666  }
   667  
   668  //ApplicationComponent
   669  type ApplicationComponent struct {
   670  	Name string `json:"name,omitempty"`
   671  }
   672  
   673  //SoftwareComponents
   674  type SoftwareComponents struct {
   675  	Name string `json:"name,omitempty"`
   676  }
   677  
   678  //TransportLayer
   679  type TransportLayer struct {
   680  	Name string `json:"name,omitempty"`
   681  }
   682  
   683  //Language
   684  type Language struct {
   685  	Value string `json:"value,omitempty"`
   686  }
   687  
   688  //SourceSystem
   689  type SourceSystem struct {
   690  	Name string `json:"name,omitempty"`
   691  }
   692  
   693  //
   694  //	AUnit Run Structure
   695  //
   696  
   697  //AUnitRun Object for parsing XML
   698  type AUnitRun struct {
   699  	XMLName    xml.Name   `xml:"run"`
   700  	Title      string     `xml:"title,attr"`
   701  	Context    string     `xml:"context,attr"`
   702  	Progress   Progress   `xml:"progress"`
   703  	ExecutedBy ExecutedBy `xml:"executedBy"`
   704  	Time       Time       `xml:"time"`
   705  	Link       AUnitLink  `xml:"link"`
   706  }
   707  
   708  //Progress of AUnit run
   709  type Progress struct {
   710  	Status     string `xml:"status,attr"`
   711  	Percentage string `xml:"percentage,attr"`
   712  }
   713  
   714  //ExecutedBy User
   715  type ExecutedBy struct {
   716  	User string `xml:"user,attr"`
   717  }
   718  
   719  //Time run was started and finished
   720  type Time struct {
   721  	Started string `xml:"started,attr"`
   722  	Ended   string `xml:"ended,attr"`
   723  }
   724  
   725  //AUnitLink containing result locations
   726  type AUnitLink struct {
   727  	Href string `xml:"href,attr"`
   728  	Rel  string `xml:"rel,attr"`
   729  	Type string `xml:"type,attr"`
   730  }
   731  
   732  //
   733  //	AUnit Result Structure
   734  //
   735  
   736  type AUnitResult struct {
   737  	XMLName    xml.Name `xml:"testsuites"`
   738  	Title      string   `xml:"title,attr"`
   739  	System     string   `xml:"system,attr"`
   740  	Client     string   `xml:"client,attr"`
   741  	ExecutedBy string   `xml:"executedBy,attr"`
   742  	Time       string   `xml:"time,attr"`
   743  	Timestamp  string   `xml:"timestamp,attr"`
   744  	Failures   string   `xml:"failures,attr"`
   745  	Errors     string   `xml:"errors,attr"`
   746  	Skipped    string   `xml:"skipped,attr"`
   747  	Asserts    string   `xml:"asserts,attr"`
   748  	Tests      string   `xml:"tests,attr"`
   749  	Testsuite  struct {
   750  		Tests     string `xml:"tests,attr"`
   751  		Asserts   string `xml:"asserts,attr"`
   752  		Skipped   string `xml:"skipped,attr"`
   753  		Errors    string `xml:"errors,attr"`
   754  		Failures  string `xml:"failures,attr"`
   755  		Timestamp string `xml:"timestamp,attr"`
   756  		Time      string `xml:"time,attr"`
   757  		Hostname  string `xml:"hostname,attr"`
   758  		Package   string `xml:"package,attr"`
   759  		Name      string `xml:"name,attr"`
   760  		Testcase  []struct {
   761  			Asserts   string `xml:"asserts,attr"`
   762  			Time      string `xml:"time,attr"`
   763  			Name      string `xml:"name,attr"`
   764  			Classname string `xml:"classname,attr"`
   765  			Error     []struct {
   766  				Text    string `xml:",chardata"`
   767  				Type    string `xml:"type,attr"`
   768  				Message string `xml:"message,attr"`
   769  			} `xml:"error"`
   770  			Failure []struct {
   771  				Text    string `xml:",chardata"`
   772  				Type    string `xml:"type,attr"`
   773  				Message string `xml:"message,attr"`
   774  			} `xml:"failure"`
   775  			Skipped []struct {
   776  				Text    string `xml:",chardata"`
   777  				Message string `xml:"message,attr"`
   778  			} `xml:"skipped"`
   779  		} `xml:"testcase"`
   780  	} `xml:"testsuite"`
   781  }