From 278c47b808a3f3d3c1cc770ab7e29a9935a4fd08 Mon Sep 17 00:00:00 2001 From: Rinita Asani Date: Wed, 15 Dec 2021 10:55:53 +0100 Subject: [PATCH] Refactor of gctsExecuteABAPUnitTests (#3338) * Update gctsExecuteABAPUnitTests.yaml * Update gctsExecuteABAPUnitTests.go * Update gctsExecuteABAPUnitTests.go * Update gctsExecuteABAPUnitTests.yaml * Add commit parameter * Update gctsExecuteABAPUnitTests.yaml * Update gctsExecuteABAPUnitTests.go * Update gctsExecuteABAPUnitTests.go * Update gctsExecuteABAPUnitTests.yaml * Update gctsExecuteABAPUnitTests.go * Change the commit parameter in Yaml file * Adding scope paramter to gCTSExecuteABAPUnitTest * Adding logs * Adapting checkstyle for success case * write ATC file * add log * time duration * add log * Improve log * Improve unit test * severity error * Fix the xml file for Unit Tests * Editing UnitTest Results for better parsing * Change file name * Add folder * Add path to file * Add src * Add src and object type * Add filename * Add path * Handling success cases * complete refactoring * fix for file path * filname lower case * Add log entry * Delete ATC Results * Change ATC name * Functiongroups * When ATC errors Pipeline fails * Reading a file into chuncks * escape string * Fix for a bug * Unesscape URL * Change from fatal error to error * When unit test fails pipeline fails too * Severity errors * Severity for Unit Test * Fix for successful unit test * Code review * Telemetry was missing * yaml file * generate yaml file * add logs * fix location * do not close body * checkstyle severity * checkstyle * checkstyle2 * atc method * line calculation * global file * method * line * fix * add logs * add log * add logg * unit test error * Code checked and finalized * change commit paramter to optional * Add logs for current local and remote commit * Add logs for object type and name * parse ATC checks error * add log of http response * add log * remove end of function findline * log cleaning * log extra cleaning * cleaning log for unit test * more cleaning * double cleanser for your code * more cleaning * docu checks * file name fix for function group * handle programs-reports * fix a bug * fix regex * fix regex for test class * fix regexos * fix dots * fix a error message in unit tests * calculate dynamically file extension * add log for path * add method name * add method name- * fix repo scope * include table type * Change paramter commitId to commit only * change log output * give more detailed examples * Table Name in get file name * change logs * improve log entries * improve log entries again * final improvements * Delete UnitTestResults * Review * Change text and comments * fix a type in commit paramter * change repository method name * Fix typos in yaml file * Remove empty lines in yaml file * Remove blank lines in the end * Add new line * go generate * Remove tabs Co-authored-by: Martin Bredy <58846699+martin-bredy@users.noreply.github.com> Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> --- cmd/gctsExecuteABAPUnitTests.go | 1764 +++++++++++++++-- cmd/gctsExecuteABAPUnitTests_generated.go | 145 +- cmd/gctsExecuteABAPUnitTests_test.go | 1609 ++++++++++++++- .../docs/steps/gctsExecuteABAPUnitTests.md | 90 +- go.sum | 2 +- .../metadata/gctsExecuteABAPUnitTests.yaml | 107 +- 6 files changed, 3453 insertions(+), 264 deletions(-) diff --git a/cmd/gctsExecuteABAPUnitTests.go b/cmd/gctsExecuteABAPUnitTests.go index 259359428..d24e10eb5 100644 --- a/cmd/gctsExecuteABAPUnitTests.go +++ b/cmd/gctsExecuteABAPUnitTests.go @@ -3,75 +3,442 @@ package cmd import ( "bytes" "encoding/xml" + "fmt" + "html" + "io/ioutil" "net/http" "net/http/cookiejar" + "net/url" + "regexp" + "strconv" + "strings" + "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/pkg/errors" ) +var atcFailure, aUnitFailure bool + func gctsExecuteABAPUnitTests(config gctsExecuteABAPUnitTestsOptions, telemetryData *telemetry.CustomData) { - // for http calls import piperhttp "github.com/SAP/jenkins-library/pkg/http" - // and use a &piperhttp.Client{} in a custom system - // Example: step checkmarxExecuteScan.go - httpClient := &piperhttp.Client{} + // for command execution use Command + c := command.Command{} + // reroute command output to logging framework + c.Stdout(log.Writer()) + c.Stderr(log.Writer()) + httpClient := &piperhttp.Client{} // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end - err := runUnitTestsForAllRepoPackages(&config, httpClient) + err := runGctsExecuteABAPUnitTests(&config, httpClient) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } + + if aUnitFailure || atcFailure { + + log.Entry().Fatal("step execution failed") + + } + } -func runUnitTestsForAllRepoPackages(config *gctsExecuteABAPUnitTestsOptions, httpClient piperhttp.Sender) error { +func runGctsExecuteABAPUnitTests(config *gctsExecuteABAPUnitTestsOptions, httpClient piperhttp.Sender) error { + + const localChangedObjects = "localchangedobjects" + const remoteChangedObjects = "remotechangedobjects" + const localChangedPackages = "localchangedpackages" + const remoteChangedPackages = "remotechangedpackages" + const repository = "repository" + const packages = "packages" cookieJar, cookieErr := cookiejar.New(nil) if cookieErr != nil { - return errors.Wrap(cookieErr, "execution of unit tests failed") + return errors.Wrap(cookieErr, "creating a cookie jar failed") } + clientOptions := piperhttp.ClientOptions{ CookieJar: cookieJar, Username: config.Username, Password: config.Password, } + httpClient.SetOptions(clientOptions) - repoObjects, getPackageErr := getPackageList(config, httpClient) + log.Entry().Infof("start of gCTSExecuteABAPUnitTests step with configuration values: %v", config) - if getPackageErr != nil { - return errors.Wrap(getPackageErr, "execution of unit tests failed") + var objects []repoObject + var err error + + log.Entry().Info("scope:", config.Scope) + + switch strings.ToLower(config.Scope) { + case localChangedObjects: + objects, err = getLocalObjects(config, httpClient) + case remoteChangedObjects: + objects, err = getRemoteObjects(config, httpClient) + case localChangedPackages: + objects, err = getLocalPackages(config, httpClient) + case remoteChangedPackages: + objects, err = getRemotePackages(config, httpClient) + case repository: + objects, err = getRepositoryObjects(config, httpClient) + case packages: + objects, err = getPackages(config, httpClient) + default: + log.Entry().Info("the specified scope does not exists, the default one will be used:" + repository) + objects, err = getRepositoryObjects(config, httpClient) } - discHeader, discError := discoverServer(config, httpClient) - - if discError != nil { - return errors.Wrap(discError, "execution of unit tests failed") + if err != nil { + log.Entry().WithError(err).Fatal("failure in get objects") } - if discHeader.Get("X-Csrf-Token") == "" { - return errors.Errorf("could not retrieve x-csrf-token from server") + if objects == nil { + log.Entry().Warning("no object delta was found, therefore the step execution will stop") + return nil + } - header := make(http.Header) - header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) - header.Add("Accept", "application/xml") - header.Add("Content-Type", "application/vnd.sap.adt.abapunit.testruns.result.v1+xml") + log.Entry().Infof("objects to be checked:") + for _, object := range objects { + log.Entry().Info(object.Type, " ", object.Object) + } - for _, object := range repoObjects { - executeTestsErr := executeTestsForPackage(config, httpClient, header, object) + if config.AUnitTest { + + // wrapper for execution of AUnit Test + err := executeAUnitTest(config, httpClient, objects) + + if err != nil { + log.Entry().WithError(err) + + } + + if aUnitFailure { + + log.Entry().Error("unit test(s) has/have failed! Check " + config.AUnitResultsFileName + " for more information! If you have enabled Warnings-Next-Generation Plugin, you can see the issues there!") + + } else { + + log.Entry().Info("AUnit test run completed successfully. If there are any results from the run, the results are saved in " + config.AUnitResultsFileName) - if executeTestsErr != nil { - return errors.Wrap(executeTestsErr, "execution of unit tests failed") } } - log.Entry(). - WithField("repository", config.Repository). - Info("all unit tests were successful") + if config.AtcCheck { + + // wrapper for execution of ATCChecks + err = executeATCCheck(config, httpClient, objects) + + if err != nil { + log.Entry().WithError(err).Fatal("execute ATC Check failed") + } + + if atcFailure { + + log.Entry().Error(" ATC issue(s) found! Check " + config.AtcResultsFileName + " for more information! If you have enabled Warnings-Next-Generation Plugin, you can see the issues there!") + + } else { + + log.Entry().Info("ATCCheck test run completed successfully. If there are any results from the run, the results are saved in " + config.AtcResultsFileName) + + } + + } + return nil + +} + +func getLocalObjects(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + var localObjects []repoObject + var localObject repoObject + + log.Entry().Info("get local changed objects started") + + if config.Commit == "" { + + return []repoObject{}, errors.Errorf("For scope: localChangedObjects you need to specify a commit") + + } + + repository, err := getRepo(config, client) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get local changed objects failed") + } + + currentLocalCommit := repository.Result.CurrentCommit + log.Entry().Info("current commit in the local repository: ", currentLocalCommit) + + // object delta between the commit that triggered the pipeline and the current commit in the local repository + resp, err := getObjectDifference(config, currentLocalCommit, config.Commit, client) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get local changed objects failed") + } + + for _, object := range resp.Objects { + localObject.Object = object.Name + localObject.Type = object.Type + localObjects = append(localObjects, localObject) + } + + log.Entry().Info("get local changed objects finished") + + return localObjects, nil +} + +func getRemoteObjects(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + var remoteObjects []repoObject + var remoteObject repoObject + var currentRemoteCommit string + + log.Entry().Info("get remote changed objects started") + + if config.Commit == "" { + + return []repoObject{}, errors.Errorf("For scope: remoteChangedObjects you need to specify a commit") + + } + + commitList, err := getCommitList(config, client) + + if err != nil { + return []repoObject{}, errors.Wrap(err, "get remote changed objects failed") + } + + for i, commit := range commitList.Commits { + if commit.ID == config.Commit { + currentRemoteCommit = commitList.Commits[i+1].ID + break + } + } + if currentRemoteCommit == "" { + return []repoObject{}, errors.New("current remote commit was not found") + + } + log.Entry().Info("current commit in the remote repository: ", currentRemoteCommit) + // object delta between the commit that triggered the pipeline and the current commit in the remote repository + resp, err := getObjectDifference(config, currentRemoteCommit, config.Commit, client) + + if err != nil { + return []repoObject{}, errors.Wrap(err, "get remote changed objects failed") + } + + for _, object := range resp.Objects { + remoteObject.Object = object.Name + remoteObject.Type = object.Type + remoteObjects = append(remoteObjects, remoteObject) + } + + log.Entry().Info("get remote changed objects finished") + + return remoteObjects, nil +} + +func getLocalPackages(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + var localPackages []repoObject + var localPackage repoObject + + log.Entry().Info("get local changed packages started") + + if config.Commit == "" { + + return []repoObject{}, errors.Errorf("For scope: localChangedPackages you need to specify a commit") + + } + + repository, err := getRepo(config, client) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get local changed packages failed") + } + + currentLocalCommit := repository.Result.CurrentCommit + log.Entry().Info("current commit in the local repository: ", currentLocalCommit) + + //object delta between the commit that triggered the pipeline and the current commit in the local repository + resp, err := getObjectDifference(config, currentLocalCommit, config.Commit, client) + + if err != nil { + return []repoObject{}, errors.Wrap(err, "get local changed packages failed") + + } + + myPackages := map[string]bool{} + + // objects are resolved into packages(DEVC) + for _, object := range resp.Objects { + objInfo, err := getObjectInfo(config, client, object.Name, object.Type) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get local changed packages failed") + } + if myPackages[objInfo.Devclass] { + + } else { + myPackages[objInfo.Devclass] = true + localPackage.Object = objInfo.Devclass + localPackage.Type = "DEVC" + localPackages = append(localPackages, localPackage) + } + + } + + log.Entry().Info("get local changed packages finished") + return localPackages, nil +} + +func getRemotePackages(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + var remotePackages []repoObject + var remotePackage repoObject + var currentRemoteCommit string + + log.Entry().Info("get remote changed packages started") + + if config.Commit == "" { + + return []repoObject{}, errors.Errorf("For scope: remoteChangedPackages you need to specify a commit") + + } + + commitList, err := getCommitList(config, client) + + if err != nil { + return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") + } + + for i, commit := range commitList.Commits { + if commit.ID == config.Commit { + currentRemoteCommit = commitList.Commits[i+1].ID + break + } + } + + if currentRemoteCommit == "" { + return []repoObject{}, errors.Wrap(err, "current remote commit was not found") + + } + log.Entry().Info("current commit in the remote repository: ", currentRemoteCommit) + //object delta between the commit that triggered the pipeline and the current commit in the remote repository + resp, err := getObjectDifference(config, currentRemoteCommit, config.Commit, client) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") + } + + myPackages := map[string]bool{} + // objects are resolved into packages(DEVC) + for _, object := range resp.Objects { + objInfo, err := getObjectInfo(config, client, object.Name, object.Type) + if err != nil { + return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") + } + if myPackages[objInfo.Devclass] { + + } else { + myPackages[objInfo.Devclass] = true + remotePackage.Object = objInfo.Devclass + remotePackage.Type = "DEVC" + remotePackages = append(remotePackages, remotePackage) + } + + } + log.Entry().Info("get remote changed packages finished") + return remotePackages, nil +} + +func getRepositoryObjects(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + log.Entry().Info("get repository objects started") + + var repoResp repoObjectResponse + + url := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/objects?sap-client=" + config.Client + + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return []repoObject{}, errors.Wrap(httpErr, "could not get repository objects") + } else if resp == nil { + return []repoObject{}, errors.New("could not get repository objects: did not retrieve a HTTP response") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoResp) + if parsingErr != nil { + return []repoObject{}, errors.Errorf("%v", parsingErr) + } + + var repositoryObjects []repoObject + + // remove object type DEVC, because it is already included in scope packages + // also if you run ATC Checks for DEVC together with other object types, ATC checks will run only for DEVC + for _, object := range repoResp.Objects { + + if object.Type != "DEVC" { + repositoryObjects = append(repositoryObjects, object) + } + + } + + log.Entry().Info("get repository objects finished") + + // all objects that are part of the local repository + return repositoryObjects, nil +} + +func getPackages(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]repoObject, error) { + + var packages []repoObject + + log.Entry().Info("get packages started") + + var repoResp repoObjectResponse + + url := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/objects?sap-client=" + config.Client + + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return []repoObject{}, errors.Wrap(httpErr, "get packages failed: could not get repository objects") + } else if resp == nil { + return []repoObject{}, errors.New("get packages failed: could not get repository objects: did not retrieve a HTTP response") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoResp) + if parsingErr != nil { + return []repoObject{}, errors.Errorf("%v", parsingErr) + } + // chose only DEVC from repository objects + for _, object := range repoResp.Objects { + + if object.Type == "DEVC" { + packages = append(packages, object) + } + + } + + log.Entry().Info("get packages finished") + return packages, nil } func discoverServer(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (*http.Header, error) { @@ -101,34 +468,340 @@ func discoverServer(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Se return &disc.Header, nil } -func executeTestsForPackage(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, header http.Header, packageName string) error { +func executeAUnitTest(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, objects []repoObject) error { + + log.Entry().Info("execute ABAP Unit Test started") + + var innerXml string + var result runResult + + for _, object := range objects { + + switch object.Type { + case "CLAS": + innerXml = innerXml + `` + case "DEVC": + innerXml = innerXml + `` + + } + + } var xmlBody = []byte(` - + - + - - - - + + + + + - - - - - - + + + ` + + innerXml + + ` + - `) + `) + resp, err := runAUnitTest(config, client, xmlBody) + if err != nil { + return errors.Wrap(err, "execute of Aunit test has failed") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyXML(resp, &result) + if parsingErr != nil { + log.Entry().Warning(parsingErr) + return nil + } + + parsedRes, err := parseUnitResult(config, client, &result) + + if err != nil { + log.Entry().Warning(err) + return nil + } + + log.Entry().Info("execute ABAP Unit Test finished.", parsedRes.Text) + + return nil +} + +func runAUnitTest(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, xml []byte) (response *http.Response, err error) { + + log.Entry().Info("run ABAP Unit Test started") url := config.Host + "/sap/bc/adt/abapunit/testruns?sap-client=" + config.Client - resp, httpErr := client.SendRequest("POST", url, bytes.NewBuffer(xmlBody), header, nil) + discHeader, discError := discoverServer(config, client) + + if discError != nil { + return response, errors.Wrap(discError, "run of unit tests failed") + } + + if discHeader.Get("X-Csrf-Token") == "" { + + return response, errors.Errorf("could not retrieve x-csrf-token from server") + } + + header := make(http.Header) + header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) + header.Add("Accept", "application/xml") + header.Add("Content-Type", "application/vnd.sap.adt.abapunit.testruns.result.v1+xml") + + response, httpErr := client.SendRequest("POST", url, bytes.NewBuffer(xml), header, nil) + + if httpErr != nil { + return response, errors.Wrap(httpErr, "run of unit tests failed") + } else if response == nil { + return response, errors.New("run of unit tests failed: did not retrieve a HTTP response") + } + + log.Entry().Info("run ABAP Unit Test finished") + return response, nil +} + +func parseUnitResult(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, aUnitRunResult *runResult) (parsedResult checkstyle, err error) { + + log.Entry().Info("parse ABAP Unit Result started") + + var fileName string + var aUnitFile file + var aUnitError checkstyleError + + parsedResult.Version = "1.0" + + for _, program := range aUnitRunResult.Program { + + objectType := program.Type[0:4] + objectName := program.Name + + //syntax error in unit test or class + if program.Alerts.Alert.HasSyntaxErrors == "true" { + + aUnitFailure = true + aUnitError.Source = objectName + aUnitError.Severity = "error" + log.Entry().Info("severity: ", aUnitError.Severity) + aUnitError.Message = html.UnescapeString(program.Alerts.Alert.Title + " " + program.Alerts.Alert.Details.Detail.AttrText) + log.Entry().Info("message: ", aUnitError.Message) + aUnitError.Line, err = findLine(config, client, program.Alerts.Alert.Stack.StackEntry.URI, objectName, objectType) + log.Entry().Error("line: ", aUnitError.Line) + if err != nil { + return parsedResult, errors.Wrap(err, "parse AUnit Result failed") + + } + fileName, err = getFileName(config, client, program.Alerts.Alert.Stack.StackEntry.URI, objectName) + log.Entry().Error("file path: ", aUnitError.Line) + if err != nil { + return parsedResult, errors.Wrap(err, "parse AUnit Result failed") + + } + + aUnitFile.Error = append(aUnitFile.Error, aUnitError) + aUnitError = checkstyleError{} + log.Entry().Error("there is a syntax error", aUnitFile) + } + + for _, testClass := range program.TestClasses.TestClass { + + for _, testMethod := range testClass.TestMethods.TestMethod { + + aUnitError.Source = testClass.Name + "/" + testMethod.Name + + // unit test failure + if len(testMethod.Alerts.Alert) > 0 { + + for _, testalert := range testMethod.Alerts.Alert { + + switch testalert.Severity { + case "fatal": + log.Entry().Error("unit test " + aUnitError.Source + " has failed with severity fatal") + aUnitFailure = true + aUnitError.Severity = "error" + case "critical": + log.Entry().Error("unit test " + aUnitError.Source + " has failed with severity critical") + aUnitFailure = true + aUnitError.Severity = "error" + case "tolerable": + log.Entry().Warning("unit test " + aUnitError.Source + " has failed with severity warning") + aUnitError.Severity = "warning" + default: + aUnitError.Severity = "info" + + } + + //unit test message is spread in different elements + for _, detail := range testalert.Details.Detail { + aUnitError.Message = aUnitError.Message + " " + detail.AttrText + for _, subdetail := range detail.Details.Detail { + + aUnitError.Message = html.UnescapeString(aUnitError.Message + " " + subdetail.AttrText) + log.Entry().Info("message: ", aUnitError.Message) + } + + } + + aUnitError.Line, err = findLine(config, client, testalert.Stack.StackEntry.URI, objectName, objectType) + log.Entry().Info("line: ", aUnitError.Line) + if err != nil { + + log.Entry().Warning(err) + + } + + } + + aUnitFile.Error = append(aUnitFile.Error, aUnitError) + aUnitError = checkstyleError{} + + } else { + + log.Entry().Info("unit test:", aUnitError.Source, "- was successful") + + } + + } + + fileName, err = getFileName(config, client, testClass.URI, objectName) + if err != nil { + return parsedResult, errors.Wrap(err, "parse AUnit Result failed") + + } + } + + aUnitFile.Name, err = constructPath(config, client, fileName, objectName, objectType) + log.Entry().Error("file path: ", aUnitFile.Name) + if err != nil { + + return parsedResult, errors.Wrap(err, "parse AUnit Result failed") + } + parsedResult.File = append(parsedResult.File, aUnitFile) + aUnitFile = file{} + + } + + body, _ := xml.Marshal(parsedResult) + + writeErr := ioutil.WriteFile(config.AUnitResultsFileName, body, 0644) + + if writeErr != nil { + log.Entry().Error("file AUnitResults.xml could not be created") + return parsedResult, fmt.Errorf("handling unit test results failed: %w", writeErr) + } + + log.Entry().Info("parse ABAP Unit Result finished") + return parsedResult, nil + +} + +func executeATCCheck(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, objects []repoObject) (error error) { + + log.Entry().Info("execute ATC Check started") + + var innerXml string + var result worklist + + for _, object := range objects { + + switch object.Type { + + case "CLAS": + innerXml = innerXml + `` + case "INTF": + innerXml = innerXml + `` + case "DEVC": + innerXml = innerXml + `` + case "FUGR": + innerXml = innerXml + `` + case "TABL": + innerXml = innerXml + `` + case "DTEL": + innerXml = innerXml + `` + case "DOMA": + innerXml = innerXml + `` + case "MSAG": + innerXml = innerXml + `` + case "PROG": + innerXml = innerXml + `` + default: + log.Entry().Warning("object Type " + object.Type + " is not supported!") + + } + + } + + var xmlBody = []byte(` + + + + ` + innerXml + + ` + + + `) + + worklist, err := getWorklist(config, client) + if err != nil { + return errors.Wrap(err, "execution of ATC Checks failed") + } + + err = startATCRun(config, client, xmlBody, worklist) + + if err != nil { + return errors.Wrap(err, "execution of ATC Checks failed") + } + + resp, err := getATCRun(config, client, worklist) + + if err != nil { + return errors.Wrap(err, "execution of ATC Checks failed") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyXML(resp, &result) + if parsingErr != nil { + log.Entry().Warning(parsingErr) + return nil + } + + atcRes, err := parseATCCheckResult(config, client, &result) + + if err != nil { + log.Entry().Error(err) + return errors.Wrap(err, "execution of ATC Checks failed") + } + + log.Entry().Info("execute ATC Checks finished.", atcRes.Text) + + return nil + +} +func startATCRun(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, xml []byte, worklistID string) (err error) { + + log.Entry().Info("ATC Run started") + + discHeader, discError := discoverServer(config, client) + if discError != nil { + return errors.Wrap(discError, "start of ATC run failed") + } + + if discHeader.Get("X-Csrf-Token") == "" { + return errors.Errorf("could not retrieve x-csrf-token from server") + } + + header := make(http.Header) + header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) + header.Add("Accept", "application/xml") + + url := config.Host + + "/sap/bc/adt/atc/runs?worklistId=" + worklistID + "&sap-client=" + config.Client + + resp, httpErr := client.SendRequest("POST", url, bytes.NewBuffer(xml), header, nil) defer func() { if resp != nil && resp.Body != nil { @@ -137,69 +810,625 @@ func executeTestsForPackage(config *gctsExecuteABAPUnitTestsOptions, client pipe }() if httpErr != nil { - return errors.Wrap(httpErr, "execution of unit tests failed") + return errors.Wrap(httpErr, "start of ATC run failed") } else if resp == nil { - return errors.New("execution of unit tests failed: did not retrieve a HTTP response") + return errors.New("start of ATC run failed: did not retrieve a HTTP response") } - var response runResult - parsingErr := piperhttp.ParseHTTPResponseBodyXML(resp, &response) - if parsingErr != nil { - log.Entry().Warning(parsingErr) - } - - aunitError := parseAUnitResponse(&response) - if aunitError != nil { - return aunitError - } + log.Entry().Info("ATC Run finished") return nil + } -func parseAUnitResponse(response *runResult) error { - var node string - aunitError := false +func getATCRun(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, worklistID string) (response *http.Response, err error) { - for _, program := range response.Program { - log.Entry().Infof("testing class %v", program.Name) - for _, testClass := range program.TestClasses.TestClass { - log.Entry().Infof("using test class %v", testClass.Name) - for _, testMethod := range testClass.TestMethods.TestMethod { - node = testMethod.Name - if len(testMethod.Alerts.Alert) > 0 { - log.Entry().Errorf("%v - error", node) - aunitError = true - } else { - log.Entry().Infof("%v - ok", node) - } - } - } - } - if aunitError { - return errors.Errorf("some unit tests failed") - } - return nil -} + log.Entry().Info("get ATC Run Results started") -func getPackageList(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) ([]string, error) { - - type object struct { - Pgmid string `json:"pgmid"` - Object string `json:"object"` - Type string `json:"type"` - Description string `json:"description"` - } - - type objectsResponseBody struct { - Objects []object `json:"objects"` - Log []gctsLogs `json:"log"` - Exception gctsException `json:"exception"` - ErrorLogs []gctsLogs `json:"errorLog"` - } + header := make(http.Header) + url := config.Host + + "/sap/bc/adt/atc/worklists/" + worklistID + "?sap-client=" + config.Client + + header.Add("Accept", "application/atc.worklist.v1+xml") + + resp, httpErr := client.SendRequest("GET", url, nil, header, nil) + + if httpErr != nil { + return response, errors.Wrap(httpErr, "get ATC run failed") + } else if resp == nil { + return response, errors.New("get ATC run failed: did not retrieve a HTTP response") + } + log.Entry().Info("get ATC Run Results finished") + return resp, nil + +} + +func getWorklist(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (worklistID string, error error) { + + url := config.Host + + "/sap/bc/adt/atc/worklists?checkVariant=" + config.AtcVariant + "&sap-client=" + config.Client + discHeader, discError := discoverServer(config, client) + + if discError != nil { + return worklistID, errors.Wrap(discError, "get worklist failed") + } + + if discHeader.Get("X-Csrf-Token") == "" { + return worklistID, errors.Errorf("could not retrieve x-csrf-token from server") + } + + header := make(http.Header) + header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) + header.Add("Accept", "*/*") + + resp, httpErr := client.SendRequest("POST", url, nil, header, nil) + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return worklistID, errors.Wrap(httpErr, "get worklist failed") + } else if resp == nil { + return worklistID, errors.New("get worklist failed: did not retrieve a HTTP response") + } + location := resp.Header["Location"][0] + locationSlice := strings.Split(location, "/") + worklistID = locationSlice[len(locationSlice)-1] + log.Entry().Info("worklist id for ATC check: ", worklistID) + + return worklistID, nil +} + +func parseATCCheckResult(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, response *worklist) (atcResults checkstyle, error error) { + + log.Entry().Info("parse ATC Check Result started") + + var atcFile file + var subObject string + var aTCUnitError checkstyleError + + atcResults.Version = "1.0" + + for _, object := range response.Objects.Object { + + objectType := object.Type + objectName := object.Name + + for _, atcworklist := range object.Findings.Finding { + + log.Entry().Info("there is atc finding for object type: ", objectType+" object name: "+objectName) + + path, err := url.PathUnescape(atcworklist.Location) + + if err != nil { + return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") + + } + + if len(atcworklist.Atcfinding) > 0 { + + priority, err := strconv.Atoi(atcworklist.Priority) + + if err != nil { + return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") + + } + + switch priority { + case 1: + atcFailure = true + aTCUnitError.Severity = "error" + log.Entry().Error("atc issue with priority: 1 ") + case 2: + atcFailure = true + aTCUnitError.Severity = "error" + log.Entry().Error("atc issue with priority: 2 ") + case 3: + aTCUnitError.Severity = "warning" + log.Entry().Warning("atc issue with priority: 3 ") + default: + aTCUnitError.Severity = "info" + log.Entry().Info("atc issue with low priority ") + } + + log.Entry().Error("severity: ", aTCUnitError.Severity) + + if aTCUnitError.Line == "" { + + aTCUnitError.Line, err = findLine(config, client, path, objectName, objectType) + log.Entry().Info("line: ", aTCUnitError.Line) + + if err != nil { + log.Entry().Info(path) + log.Entry().Warning(err) + + } + + } + + if subObject != "" { + aTCUnitError.Source = objectName + "/" + strings.ToUpper(subObject) + } else { + aTCUnitError.Source = objectName + } + + aTCUnitError.Message = html.UnescapeString(atcworklist.CheckTitle + " " + atcworklist.MessageTitle) + log.Entry().Info("message: ", aTCUnitError.Message) + atcFile.Error = append(atcFile.Error, aTCUnitError) + aTCUnitError = checkstyleError{} + } + + if atcFile.Error[0].Message != "" { + + fileName, err := getFileName(config, client, path, objectName) + + if err != nil { + return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") + } + + atcFile.Name, err = constructPath(config, client, fileName, objectName, objectType) + log.Entry().Info("file path: ", atcFile.Name) + if err != nil { + return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") + } + atcResults.File = append(atcResults.File, atcFile) + atcFile = file{} + + } + + } + } + + atcBody, _ := xml.Marshal(atcResults) + + writeErr := ioutil.WriteFile(config.AtcResultsFileName, atcBody, 0644) + + if writeErr != nil { + log.Entry().Error("ATCResults.xml could not be created") + return atcResults, fmt.Errorf("handling atc results failed: %w", writeErr) + } + log.Entry().Info("parsing ATC check results to CheckStyle has finished.") + return atcResults, writeErr +} + +func constructPath(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, fileName string, objectName string, objectType string) (filePath string, error error) { + + targetDir, err := getTargetDir(config, client) + if err != nil { + return filePath, errors.Wrap(err, "path could not be constructed") + + } + + filePath = config.Workspace + "/" + targetDir + "/objects/" + strings.ToUpper(objectType) + "/" + strings.ToUpper(objectName) + "/" + fileName + return filePath, nil + +} + +func findLine(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, path string, objectName string, objectType string) (line string, error error) { + + regexLine := regexp.MustCompile(`.start=\d*`) + regexMethod := regexp.MustCompile(`.name=[a-zA-Z0-9_-]*;`) + + readableSource, err := checkReadableSource(config, client) + + if err != nil { + + return line, errors.Wrap(err, "could not find line in source code") + + } + + fileName, err := getFileName(config, client, path, objectName) + + if err != nil { + + return line, err + + } + + filePath, err := constructPath(config, client, fileName, objectName, objectType) + if err != nil { + return line, errors.Wrap(err, objectType+"/"+objectName+"could not find line in source code") + + } + + var absLine int + if readableSource { + + // the error line that we get from UnitTest Run or ATC Check is not aligned for the readable source, we need to calculated it + rawfile, err := ioutil.ReadFile(filePath) + + if err != nil { + + return line, errors.Wrapf(err, "could not find object in the workspace of your CI/CD tool ") + } + + file := string(rawfile) + + splittedfile := strings.Split(file, "\n") + + // CLAS/OSO - is unique identifier for protection section in CLAS + if strings.Contains(path, "CLAS/OSO") { + + for l, line := range splittedfile { + + if strings.Contains(line, "protected section.") { + absLine = l + break + } + + } + + // CLAS/OM - is unique identifier for method section in CLAS + } else if strings.Contains(path, "CLAS/OM") { + + methodName := regexMethod.FindString(path) + + if methodName != "" { + methodName = methodName[len(`.name=`) : len(methodName)-1] + + } + + for line, linecontent := range splittedfile { + + if strings.Contains(linecontent, "method"+" "+methodName) { + absLine = line + break + } + + } + + // CLAS/OSI - is unique identifier for private section in CLAS + } else if strings.Contains(path, "CLAS/OSI") { + + for line, linecontent := range splittedfile { + + if strings.Contains(linecontent, "private section.") { + absLine = line + break + } + + } + + } + + errLine := regexLine.FindString(path) + + if errLine != "" { + + errLine, err := strconv.Atoi(errLine[len(`.start=`):]) + if err == nil { + line = strconv.Itoa(absLine + errLine) + + } + + } + + } else { + // classic format + errLine := regexLine.FindString(path) + if errLine != "" { + line = errLine[len(`.start=`):] + + } + + } + + return line, nil +} +func getFileName(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, path string, objName string) (fileName string, error error) { + + readableSource, err := checkReadableSource(config, client) + if err != nil { + return fileName, errors.Wrap(err, "get file name has failed") + + } + + path, err = url.PathUnescape(path) + + var fileExtension string + fileExtensionLength := 30 - len(objName) + for i := 0; i < fileExtensionLength; i++ { + fileExtension += "=" + } + + if err != nil { + return fileName, errors.Wrap(err, "get file name has failed") + + } + + // INTERFACES + regexInterface := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/interfaces\/\w*`) + intf := regexInterface.FindString(path) + if intf != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".intf.abap" + } else { + fileName = "REPS " + strings.ToUpper(objName) + fileExtension + "IU.abap" + } + + } + // CLASSES DEFINITIONS + regexClasDef := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/definitions\/`) + clasDef := regexClasDef.FindString(path) + if clasDef != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.definitions.abap" + } else { + fileName = "CINC " + objName + fileExtension + "CCDEF.abap" + } + + } + + // CLASSES IMPLEMENTATIONS + regexClasImpl := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/implementations\/`) + clasImpl := regexClasImpl.FindString(path) + if clasImpl != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.implementations.abap" + } else { + fileName = "CINC " + objName + fileExtension + "CCIMP.abap" + } + + } + + // CLASSES MACROS + regexClasMacro := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/macros\/`) + clasMacro := regexClasMacro.FindString(path) + if clasMacro != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.macros.abap" + } else { + fileName = "CINC " + objName + fileExtension + "CCMAC.abap" + } + + } + + // TEST CLASSES + regexTestClass := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*#?\/?\w*\/?testclass`) + testClass := regexTestClass.FindString(path) + if testClass != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.testclasses.abap" + } else { + fileName = "CINC " + objName + fileExtension + "CCAU.abap" + } + + } + + // CLASS PROTECTED + regexClasProtected := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OSO`) + classProtected := regexClasProtected.FindString(path) + if classProtected != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.abap" + } else { + fileName = "CPRO " + objName + ".abap" + } + + } + + // CLASS PRIVATE + regexClasPrivate := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OSI`) + classPrivate := regexClasPrivate.FindString(path) + if classPrivate != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.abap" + } else { + fileName = "CPRI " + objName + ".abap" + } + + } + + // CLASS METHOD + regexClasMethod := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OM`) + classMethod := regexClasMethod.FindString(path) + if classMethod != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.abap" + } else { + + regexmethodName := regexp.MustCompile(`name=\w*`) + methodName := regexmethodName.FindString(path) + + fileName = "METH " + methodName[len(`name=`):] + ".abap" + } + + } + + // CLASS PUBLIC + regexClasPublic := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#start`) + classPublic := regexClasPublic.FindString(path) + if classPublic != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.abap" + } else { + fileName = "CPUB " + objName + ".abap" + } + + } + + // FUNCTION INCLUDE + regexFuncIncl := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/includes/\w*`) + + funcIncl := regexFuncIncl.FindString(path) + if funcIncl != "" && fileName == "" { + + regexSubObj := regexp.MustCompile(`includes\/\w*`) + subObject := regexSubObj.FindString(path) + subObject = subObject[len(`includes/`):] + + if readableSource { + + fileName = strings.ToLower(objName) + ".fugr." + strings.ToLower(subObject) + ".reps.abap" + } else { + fileName = "REPS " + strings.ToUpper(subObject) + ".abap" + } + + } + + // FUNCTION GROUP + regexFuncGr := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/source\/main`) + + funcGr := regexFuncGr.FindString(path) + if funcGr != "" && fileName == "" { + + if readableSource { + + fileName = strings.ToLower(objName) + ".fugr.sapl" + strings.ToLower(objName) + ".reps.abap" + } else { + fileName = "REPS SAPL" + objName + ".abap" + } + + } + + // FUNCTION MODULE + regexFuncMod := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/fmodules/\w*`) + funcMod := regexFuncMod.FindString(path) + if funcMod != "" && fileName == "" { + + regexSubObj := regexp.MustCompile(`includes\/\w*`) + subObject := regexSubObj.FindString(path) + subObject = subObject[len(`includes/`):] + + if readableSource { + + fileName = strings.ToLower(subObject) + ".func.abap" + } else { + fileName = "FUNC " + subObject + ".abap" + } + + } + // CLAS + regexClas := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/` + strings.ToLower(objName)) + clas := regexClas.FindString(path) + if clas != "" && fileName == "" { + if readableSource { + + fileName = strings.ToLower(objName) + ".clas.abap" + } else { + + fileName = "CPUB " + objName + ".abap" + } + + } + + // PROGRAM + regexProg := regexp.MustCompile(`\/sap\/bc\/adt\/programs\/programs\/` + strings.ToLower(objName)) + prog := regexProg.FindString(path) + if prog != "" && fileName == "" { + + fileName = "REPS " + objName + ".abap" + + } + + // TABLES + regexTab := regexp.MustCompile(`\/sap\/bc\/adt\/ddic\/tables\/` + strings.ToLower(objName)) + tab := regexTab.FindString(path) + if tab != "" && fileName == "" { + + fileName = "TABL " + objName + ".asx.json" + + } + + return fileName, nil + +} + +func getTargetDir(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (string, error) { + + var targetDir string + + repository, err := getRepo(config, client) + + if err != nil { + return targetDir, err + } + + for _, config := range repository.Result.Config { + if config.Key == "VCS_TARGET_DIR" { + targetDir = config.Value + } + } + + return targetDir, nil + +} + +func checkReadableSource(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (readableSource bool, error error) { + + repoLayout, err := getRepositoryLayout(config, client) + if err != nil { + return readableSource, errors.Wrap(err, "could not check readable source format") + } + + if repoLayout.Layout.ReadableSource == "true" || repoLayout.Layout.ReadableSource == "only" || repoLayout.Layout.ReadableSource == "all" { + + readableSource = true + + } else { + + readableSource = false + + } + + return readableSource, nil +} + +func getRepo(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (repositoryResponse, error) { + + var repositoryResp repositoryResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + - "/getObjects?sap-client=" + config.Client + "?sap-client=" + config.Client + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return repositoryResponse{}, errors.Wrap(httpErr, "could not get repository") + } else if resp == nil { + return repositoryResponse{}, errors.New("could not get repository: did not retrieve a HTTP response") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repositoryResp) + if parsingErr != nil { + return repositoryResponse{}, errors.Errorf("%v", parsingErr) + } + + return repositoryResp, nil + +} + +func getRepositoryLayout(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (layoutResponse, error) { + + var repoLayoutResponse layoutResponse + url := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/layout?sap-client=" + config.Client resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) @@ -210,41 +1439,169 @@ func getPackageList(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Se }() if httpErr != nil { - return []string{}, errors.Wrap(httpErr, "getting repository object/package list failed") + return layoutResponse{}, errors.Wrap(httpErr, "could not get repository layout") } else if resp == nil { - return []string{}, errors.New("getting repository object/package list failed: did not retrieve a HTTP response") + return layoutResponse{}, errors.New("could not get repository layout: did not retrieve a HTTP response") } - var response objectsResponseBody - parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &response) + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoLayoutResponse) if parsingErr != nil { - return []string{}, errors.Errorf("%v", parsingErr) + return layoutResponse{}, errors.Errorf("%v", parsingErr) } - repoObjects := []string{} - for _, object := range response.Objects { - if object.Type == "DEVC" { - repoObjects = append(repoObjects, object.Object) + return repoLayoutResponse, nil +} + +func getCommitList(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender) (commitResponse, error) { + + var commitResp commitResponse + url := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/getCommit?sap-client=" + config.Client + + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() } + }() + + if httpErr != nil { + return commitResponse{}, errors.Wrap(httpErr, "get repository history failed") + } else if resp == nil { + return commitResponse{}, errors.New("get repository history failed: did not retrieve a HTTP response") } - return repoObjects, nil + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &commitResp) + if parsingErr != nil { + return commitResponse{}, errors.Errorf("%v", parsingErr) + } + + return commitResp, nil } -type gctsException struct { - Message string `json:"message"` - Description string `json:"description"` - Code int `json:"code"` +func getObjectDifference(config *gctsExecuteABAPUnitTestsOptions, fromCommit string, toCommit string, client piperhttp.Sender) (objectsResponse, error) { + var objectResponse objectsResponse + + url := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/compareCommits?fromCommit=" + fromCommit + "&toCommit=" + toCommit + "&sap-client=" + config.Client + + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return objectsResponse{}, errors.Wrap(httpErr, "get object difference failed") + } else if resp == nil { + return objectsResponse{}, errors.New("get object difference failed: did not retrieve a HTTP response") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &objectResponse) + if parsingErr != nil { + return objectsResponse{}, errors.Errorf("%v", parsingErr) + } + log.Entry().Info("get object differences: ", objectResponse.Objects) + return objectResponse, nil } -type gctsLogs struct { - Time int `json:"time"` - User string `json:"user"` - Section string `json:"section"` - Action string `json:"action"` - Severity string `json:"severity"` - Message string `json:"message"` - Code string `json:"code"` +func getObjectInfo(config *gctsExecuteABAPUnitTestsOptions, client piperhttp.Sender, objectName string, objectType string) (objectInfo, error) { + + var objectMetInfoResponse objectInfo + url := config.Host + + "/sap/bc/cts_abapvcs/objects/" + objectType + "/" + objectName + + "?sap-client=" + config.Client + + resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return objectInfo{}, errors.Wrap(httpErr, "resolve package failed") + } else if resp == nil { + return objectInfo{}, errors.New("resolve package failed: did not retrieve a HTTP response") + } + + parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &objectMetInfoResponse) + if parsingErr != nil { + return objectInfo{}, errors.Errorf("%v", parsingErr) + } + return objectMetInfoResponse, nil + +} + +type worklist struct { + XMLName xml.Name `xml:"worklist"` + Text string `xml:",chardata"` + ID string `xml:"id,attr"` + Timestamp string `xml:"timestamp,attr"` + UsedObjectSet string `xml:"usedObjectSet,attr"` + ObjectSetIsComplete string `xml:"objectSetIsComplete,attr"` + Atcworklist string `xml:"atcworklist,attr"` + ObjectSets struct { + Text string `xml:",chardata"` + ObjectSet []struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + Title string `xml:"title,attr"` + Kind string `xml:"kind,attr"` + } `xml:"objectSet"` + } `xml:"objectSets"` + Objects struct { + Text string `xml:",chardata"` + Object []struct { + Text string `xml:",chardata"` + URI string `xml:"uri,attr"` + Type string `xml:"type,attr"` + Name string `xml:"name,attr"` + PackageName string `xml:"packageName,attr"` + Author string `xml:"author,attr"` + Atcobject string `xml:"atcobject,attr"` + Adtcore string `xml:"adtcore,attr"` + Findings struct { + Text string `xml:",chardata"` + Finding []struct { + Text string `xml:",chardata"` + URI string `xml:"uri,attr"` + Location string `xml:"location,attr"` + Processor string `xml:"processor,attr"` + LastChangedBy string `xml:"lastChangedBy,attr"` + Priority string `xml:"priority,attr"` + CheckId string `xml:"checkId,attr"` + CheckTitle string `xml:"checkTitle,attr"` + MessageId string `xml:"messageId,attr"` + MessageTitle string `xml:"messageTitle,attr"` + ExemptionApproval string `xml:"exemptionApproval,attr"` + ExemptionKind string `xml:"exemptionKind,attr"` + Checksum string `xml:"checksum,attr"` + QuickfixInfo string `xml:"quickfixInfo,attr"` + Atcfinding string `xml:"atcfinding,attr"` + Link struct { + Text string `xml:",chardata"` + Href string `xml:"href,attr"` + Rel string `xml:"rel,attr"` + Type string `xml:"type,attr"` + Atom string `xml:"atom,attr"` + } `xml:"link"` + Quickfixes struct { + Text string `xml:",chardata"` + Manual string `xml:"manual,attr"` + Automatic string `xml:"automatic,attr"` + Pseudo string `xml:"pseudo,attr"` + } `xml:"quickfixes"` + } `xml:"finding"` + } `xml:"findings"` + } `xml:"object"` + } `xml:"objects"` } type runResult struct { @@ -252,12 +1609,38 @@ type runResult struct { Text string `xml:",chardata"` Aunit string `xml:"aunit,attr"` Program []struct { - Text string `xml:",chardata"` - URI string `xml:"uri,attr"` - Type string `xml:"type,attr"` - Name string `xml:"name,attr"` - URIType string `xml:"uriType,attr"` - Adtcore string `xml:"adtcore,attr"` + Text string `xml:",chardata"` + URI string `xml:"uri,attr"` + Type string `xml:"type,attr"` + Name string `xml:"name,attr"` + URIType string `xml:"uriType,attr"` + Adtcore string `xml:"adtcore,attr"` + Alerts struct { + Text string `xml:",chardata"` + Alert struct { + Text string `xml:",chardata"` + HasSyntaxErrors string `xml:"hasSyntaxErrors,attr"` + Kind string `xml:"kind,attr"` + Severity string `xml:"severity,attr"` + Title string `xml:"title"` + Details struct { + Text string `xml:",chardata"` + Detail struct { + Text string `xml:",chardata"` + AttrText string `xml:"text,attr"` + } `xml:"detail"` + } `xml:"details"` + Stack struct { + Text string `xml:",chardata"` + StackEntry struct { + Text string `xml:",chardata"` + URI string `xml:"uri,attr"` + Description string `xml:"description,attr"` + } `xml:"stackEntry"` + } `xml:"stack"` + } `xml:"alert"` + } `xml:"alerts"` + TestClasses struct { Text string `xml:",chardata"` TestClass []struct { @@ -319,3 +1702,138 @@ type runResult struct { } `xml:"testClasses"` } `xml:"program"` } + +type gctsException struct { + Message string `json:"message"` + Description string `json:"description"` + Code int `json:"code"` +} + +type gctsLogs struct { + Time int `json:"time"` + User string `json:"user"` + Section string `json:"section"` + Action string `json:"action"` + Severity string `json:"severity"` + Message string `json:"message"` + Code string `json:"code"` +} + +type commit struct { + ID string `json:"id"` +} + +type commitResponse struct { + Commits []commit `json:"commits"` + ErrorLog []gctsLogs `json:"errorLog"` + Log []gctsLogs `json:"log"` + Exception gctsException `json:"exception"` +} + +type objectInfo struct { + Pgmid string `json:"pgmid"` + Object string `json:"object"` + ObjName string `json:"objName"` + Srcsystem string `json:"srcsystem"` + Author string `json:"author"` + Devclass string `json:"devclass"` +} + +type repoConfig struct { + Key string `json:"key"` + Value string `json:"value"` + Cprivate string `json:"cprivate"` + Cprotected string `json:"cprotected"` + Cvisible string `json:"cvisible"` + Category string `json:"category"` + Scope string `json:"scope"` + ChangedAt float64 `json:"changeAt"` + ChangedBy string `json:"changedBy"` +} + +type repository struct { + Rid string `json:"rid"` + Name string `json:"name"` + Role string `json:"role"` + Type string `json:"type"` + Vsid string `json:"vsid"` + PrivateFlag string `json:"privateFlag"` + Status string `json:"status"` + Branch string `json:"branch"` + Url string `json:"url"` + CreatedBy string `json:"createdBy"` + CreatedDate string `json:"createdDate"` + Config []repoConfig `json:"config"` + Objects int `json:"objects"` + CurrentCommit string `json:"currentCommit"` +} + +type repositoryResponse struct { + Result repository `json:"result"` + Exception gctsException `json:"exception"` +} + +type objects struct { + Name string `json:"name"` + Type string `json:"type"` + Action string `json:"action"` +} +type objectsResponse struct { + Objects []objects `json:"objects"` + Log []gctsLogs `json:"log"` + Exception gctsException `json:"exception"` + ErrorLogs []gctsLogs `json:"errorLog"` +} + +type repoObject struct { + Pgmid string `json:"pgmid"` + Object string `json:"object"` + Type string `json:"type"` + Description string `json:"description"` +} + +type repoObjectResponse struct { + Objects []repoObject `json:"objects"` + Log []gctsLogs `json:"log"` + Exception gctsException `json:"exception"` + ErrorLogs []gctsLogs `json:"errorLog"` +} + +type layout struct { + FormatVersion int `json:"formatVersion"` + Format string `json:"format"` + ObjectStorage string `json:"objectStorage"` + MetaInformation string `json:"metaInformation"` + TableContent string `json:"tableContent"` + Subdirectory string `json:"subdirectory"` + ReadableSource string `json:"readableSource"` + KeepClient string `json:"keepClient"` +} + +type layoutResponse struct { + Layout layout `json:"layout"` + Log []gctsLogs `json:"log"` + Exception string `json:"exception"` + ErrorLogs []gctsLogs `json:"errorLog"` +} + +type checkstyleError struct { + Text string `xml:",chardata"` + Message string `xml:"message,attr"` + Source string `xml:"source,attr"` + Line string `xml:"line,attr"` + Severity string `xml:"severity,attr"` +} + +type file struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + Error []checkstyleError `xml:"error"` +} + +type checkstyle struct { + XMLName xml.Name `xml:"checkstyle"` + Text string `xml:",chardata"` + Version string `xml:"version,attr"` + File []file `xml:"file"` +} diff --git a/cmd/gctsExecuteABAPUnitTests_generated.go b/cmd/gctsExecuteABAPUnitTests_generated.go index d348dd508..3a76cb6a4 100644 --- a/cmd/gctsExecuteABAPUnitTests_generated.go +++ b/cmd/gctsExecuteABAPUnitTests_generated.go @@ -16,14 +16,22 @@ import ( ) type gctsExecuteABAPUnitTestsOptions struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Repository string `json:"repository,omitempty"` - Host string `json:"host,omitempty"` - Client string `json:"client,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Host string `json:"host,omitempty"` + Repository string `json:"repository,omitempty"` + Client string `json:"client,omitempty"` + AUnitTest bool `json:"aUnitTest,omitempty"` + AtcCheck bool `json:"atcCheck,omitempty"` + AtcVariant string `json:"atcVariant,omitempty"` + Scope string `json:"scope,omitempty"` + Commit string `json:"commit,omitempty"` + Workspace string `json:"workspace,omitempty"` + AtcResultsFileName string `json:"atcResultsFileName,omitempty"` + AUnitResultsFileName string `json:"aUnitResultsFileName,omitempty"` } -// GctsExecuteABAPUnitTestsCommand Runs ABAP unit tests for all packages of the specified repository +// GctsExecuteABAPUnitTestsCommand Runs ABAP unit tests and ATC (ABAP Test Cockpit) checks for a specified object scope. func GctsExecuteABAPUnitTestsCommand() *cobra.Command { const STEP_NAME = "gctsExecuteABAPUnitTests" @@ -36,8 +44,14 @@ func GctsExecuteABAPUnitTestsCommand() *cobra.Command { var createGctsExecuteABAPUnitTestsCmd = &cobra.Command{ Use: STEP_NAME, - Short: "Runs ABAP unit tests for all packages of the specified repository", - Long: `This step will execute every unit test associated with a package belonging to the specified local repository on an ABAP system.`, + Short: "Runs ABAP unit tests and ATC (ABAP Test Cockpit) checks for a specified object scope.", + Long: `This step executes ABAP unit test and ATC checks for a specified scope of objects that exist in a local Git repository on an ABAP system. +Depending on your use case, you can specify a scope of objects for which you want to execute the checks. In addition, you can choose whether you want to execute only ABAP units tests, or only ATC checks, or both. +By default, both checks are executed. +The results of the checks are stored in a [Checkstyle](https://checkstyle.sourceforge.io/) format. With the help of the Jenkins [Warnings-Next-Generation](https://plugins.jenkins.io/warnings-ng/) Plugin), you can view the issues found, and navigate to the exact line of the source code where the issue occurred. +To make the findings visible in Jenkins interface, you will need to use step recordIssues. An example will be shown in the Example section. +
+You can use this step as of SAP S/4HANA 2020.`, PreRunE: func(cmd *cobra.Command, _ []string) error { startTime = time.Now() log.SetStepName(STEP_NAME) @@ -114,17 +128,26 @@ func GctsExecuteABAPUnitTestsCommand() *cobra.Command { } func addGctsExecuteABAPUnitTestsFlags(cmd *cobra.Command, stepConfig *gctsExecuteABAPUnitTestsOptions) { - cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User to authenticate to the ABAP system") - cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password to authenticate to the ABAP system") - cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Specifies the name (ID) of the local repsitory on the ABAP system") - cmd.Flags().StringVar(&stepConfig.Host, "host", os.Getenv("PIPER_host"), "Specifies the protocol and host address, including the port. Please provide in the format `://:`. Supported protocols are `http` and `https`.") - cmd.Flags().StringVar(&stepConfig.Client, "client", os.Getenv("PIPER_client"), "Specifies the client of the ABAP system to be addressed") + cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User that authenticates to the ABAP system. Note – Don´t provide this parameter directly. Either set it in the environment, or in the Jenkins credentials store, and provide the ID as value of the abapCredentialsId parameter.") + cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password of the ABAP user that authenticates to the ABAP system. Note – Don´t provide this parameter directly. Either set it in the environment, or in the Jenkins credentials store, and provide the ID as value of the abapCredentialsId parameter.") + cmd.Flags().StringVar(&stepConfig.Host, "host", os.Getenv("PIPER_host"), "Protocol and host of the ABAP system, including the port. Please provide in the format ://:. Supported protocols are http and https.") + cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Name (ID) of the local repository on the ABAP system") + cmd.Flags().StringVar(&stepConfig.Client, "client", os.Getenv("PIPER_client"), "Client of the ABAP system in which you want to execute the checks") + cmd.Flags().BoolVar(&stepConfig.AUnitTest, "aUnitTest", true, "Indication whether you want to execute the unit test checks.") + cmd.Flags().BoolVar(&stepConfig.AtcCheck, "atcCheck", true, "Indication whether you want to execute the ATC checks.") + cmd.Flags().StringVar(&stepConfig.AtcVariant, "atcVariant", `DEFAULT_REMOTE_REF`, "Variant for ATC checks") + cmd.Flags().StringVar(&stepConfig.Scope, "scope", `repository`, "Scope of objects for which you want to execute the checks:\n\n * localChangedObjects - object delta between the commit that triggered the pipeline and the current commit in the local repository. The checks are executed for the individual objects.\n * remoteChangedObjects - object delta between the commit that triggered the pipeline and the current commit in the remote repository. The checks are executed for the individual objects.\n * localChangedPackages - object delta between the commit that triggered the pipeline and the current commit in the local repository. All objects are resolved into packages. The checks are executed for the packages.\n * remoteChangedPackages - object delta between the commit that triggered the pipeline and the current commit in the remote repository. All objects are resolved into packages. The checks are executed for the packages.\n * repository - all objects that are part of the local repository. The checks are executed for the individual objects. Packages (DEVC) are excluded. This is the default scope.\n * packages - all packages that are part of the local repository . The checks are executed for the packages.\n") + cmd.Flags().StringVar(&stepConfig.Commit, "commit", os.Getenv("PIPER_commit"), "ID of the commit that triggered the pipeline or any other commit to compare objects. For scopes localChangedObjects, remoteChangedObjects, localChangedPackages and remoteChangedPackages secifying a commit it's mandatory.") + cmd.Flags().StringVar(&stepConfig.Workspace, "workspace", os.Getenv("PIPER_workspace"), "Absolute path to directory which contains the source code that your CI/CD tool checks out. For example in Jenkins, the workspace parameter is /var/jenkins_home/workspace//") + cmd.Flags().StringVar(&stepConfig.AtcResultsFileName, "atcResultsFileName", `ATCResults.xml`, "Specifies output file name for the results from the ATC checks") + cmd.Flags().StringVar(&stepConfig.AUnitResultsFileName, "aUnitResultsFileName", `AUnitResults.xml`, "Specifies output file name for the results from the AUnit tests") cmd.MarkFlagRequired("username") cmd.MarkFlagRequired("password") - cmd.MarkFlagRequired("repository") cmd.MarkFlagRequired("host") + cmd.MarkFlagRequired("repository") cmd.MarkFlagRequired("client") + cmd.MarkFlagRequired("workspace") } // retrieve step metadata @@ -133,12 +156,12 @@ func gctsExecuteABAPUnitTestsMetadata() config.StepData { Metadata: config.StepMetadata{ Name: "gctsExecuteABAPUnitTests", Aliases: []config.Alias{}, - Description: "Runs ABAP unit tests for all packages of the specified repository", + Description: "Runs ABAP unit tests and ATC (ABAP Test Cockpit) checks for a specified object scope.", }, Spec: config.StepSpec{ Inputs: config.StepInputs{ Secrets: []config.StepSecrets{ - {Name: "abapCredentialsId", Description: "Jenkins credentials ID containing username and password for authentication to the ABAP system on which you want to perform the unit tests", Type: "jenkins"}, + {Name: "abapCredentialsId", Description: "ID taken from the Jenkins credentials store containing user name and password of the user that authenticates to the ABAP system on which you want to execute the checks.", Type: "jenkins"}, }, Parameters: []config.StepParameters{ { @@ -171,15 +194,6 @@ func gctsExecuteABAPUnitTestsMetadata() config.StepData { Aliases: []config.Alias{}, Default: os.Getenv("PIPER_password"), }, - { - Name: "repository", - ResourceRef: []config.ResourceReference{}, - Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, - Type: "string", - Mandatory: true, - Aliases: []config.Alias{}, - Default: os.Getenv("PIPER_repository"), - }, { Name: "host", ResourceRef: []config.ResourceReference{}, @@ -189,6 +203,15 @@ func gctsExecuteABAPUnitTestsMetadata() config.StepData { Aliases: []config.Alias{}, Default: os.Getenv("PIPER_host"), }, + { + Name: "repository", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_repository"), + }, { Name: "client", ResourceRef: []config.ResourceReference{}, @@ -198,6 +221,78 @@ func gctsExecuteABAPUnitTestsMetadata() config.StepData { Aliases: []config.Alias{}, Default: os.Getenv("PIPER_client"), }, + { + Name: "aUnitTest", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: true, + }, + { + Name: "atcCheck", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: true, + }, + { + Name: "atcVariant", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `DEFAULT_REMOTE_REF`, + }, + { + Name: "scope", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `repository`, + }, + { + Name: "commit", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_commit"), + }, + { + Name: "workspace", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_workspace"), + }, + { + Name: "atcResultsFileName", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `ATCResults.xml`, + }, + { + Name: "aUnitResultsFileName", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `AUnitResults.xml`, + }, }, }, }, diff --git a/cmd/gctsExecuteABAPUnitTests_test.go b/cmd/gctsExecuteABAPUnitTests_test.go index e26dc4859..32449d6f3 100644 --- a/cmd/gctsExecuteABAPUnitTests_test.go +++ b/cmd/gctsExecuteABAPUnitTests_test.go @@ -1,13 +1,19 @@ package cmd import ( + "bytes" + "encoding/xml" + "io" + "io/ioutil" "net/http" "testing" + piperhttp "github.com/SAP/jenkins-library/pkg/http" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -func TestDiscoverySuccess(t *testing.T) { +func TestDiscoverServerSuccess(t *testing.T) { config := gctsExecuteABAPUnitTestsOptions{ Host: "http://testHost.com:50000", @@ -15,9 +21,12 @@ func TestDiscoverySuccess(t *testing.T) { Repository: "testRepo", Username: "testUser", Password: "testPassword", + Scope: "localChangedObjects", + Commit: "0123456789abcdefghijkl", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", } - t.Run("discovery successful", func(t *testing.T) { + t.Run("discover server successful", func(t *testing.T) { httpClient := httpMockGcts{ StatusCode: 200, @@ -48,19 +57,22 @@ func TestDiscoverySuccess(t *testing.T) { }) } -func TestDiscoveryFailure(t *testing.T) { +func TestDiscoverServerFailure(t *testing.T) { config := gctsExecuteABAPUnitTestsOptions{ - Host: "http://testHost.com:50000", + Host: "http://testHost3.com:50000", Client: "000", Repository: "testRepo", Username: "testUser", Password: "testPassword", + Scope: "localChangedObjects", + Commit: "0123456789abcdefghijkl", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", } t.Run("a http error occurred", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 403, ResponseBody: ` + httpClient := httpMockGctsT{StatusCode: 403, ResponseBody: ` @@ -106,7 +118,7 @@ func TestDiscoveryFailure(t *testing.T) { t.Run("discovery response is nil", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: ``} + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: ``} header, err := discoverServer(&config, &httpClient) @@ -122,7 +134,7 @@ func TestDiscoveryFailure(t *testing.T) { t.Run("discovery header is nil", func(t *testing.T) { - httpClient := httpMockGcts{ + httpClient := httpMockGctsT{ StatusCode: 200, Header: nil, ResponseBody: ` @@ -143,49 +155,49 @@ func TestDiscoveryFailure(t *testing.T) { }) } -func TestGetPackageListSuccess(t *testing.T) { - - config := gctsExecuteABAPUnitTestsOptions{ - Host: "http://testHost.com:50000", - Client: "000", - Repository: "testRepo", - Username: "testUser", - Password: "testPassword", - } +func TestGetLocalObjectsSuccess(t *testing.T) { t.Run("return multiple objects successfully", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: ` - { - "objects": [ - { - "pgmid": "R3TR", - "object": "ZCL_GCTS", - "type": "CLAS", - "description": "Class (ABAP Objects)" - }, - { - "pgmid": "R3TR", - "object": "ZP_GCTS", - "type": "DEVC", - "description": "Package" - }, - { - "pgmid": "R3TR", - "object": "ZP_PIPER", - "type": "DEVC", - "description": "Package" - } - ] + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", } - `} - objects, err := getPackageList(&config, &httpClient) + httpClient := httpMockGctsT{StatusCode: 200} + + object1 := repoObject{ + + Object: "ZCL_GCTS", + Type: "CLAS", + } + object2 := repoObject{ + + Object: "ZP_GCTS", + Type: "DEVC", + } + object3 := repoObject{ + + Object: "ZIF_GCTS_API", + Type: "INTF", + } + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + repoObjects = append(repoObjects, object2) + repoObjects = append(repoObjects, object3) + + objects, err := getLocalObjects(&config, &httpClient) if assert.NoError(t, err) { t.Run("check url", func(t *testing.T) { - assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/getObjects?sap-client=000", httpClient.URL) + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/compareCommits?fromCommit=xyz987654321&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) }) t.Run("check method", func(t *testing.T) { @@ -193,7 +205,7 @@ func TestGetPackageListSuccess(t *testing.T) { }) t.Run("check package objects", func(t *testing.T) { - assert.Equal(t, []string{"ZP_GCTS", "ZP_PIPER"}, objects) + assert.Equal(t, repoObjects, objects) }) } @@ -202,14 +214,27 @@ func TestGetPackageListSuccess(t *testing.T) { t.Run("no objects returned by http call", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: `{}`} + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } - objects, err := getPackageList(&config, &httpClient) + var repoObjects []repoObject + + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: `{}`} + + objects, err := getLocalObjects(&config, &httpClient) if assert.NoError(t, err) { t.Run("check url", func(t *testing.T) { - assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/getObjects?sap-client=000", httpClient.URL) + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/compareCommits?fromCommit=xyz987654321&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) }) t.Run("check method", func(t *testing.T) { @@ -217,14 +242,14 @@ func TestGetPackageListSuccess(t *testing.T) { }) t.Run("check package objects", func(t *testing.T) { - assert.Equal(t, []string{}, objects) + assert.Equal(t, repoObjects, objects) }) } }) } -func TestGetPackageListFailure(t *testing.T) { +func TestGetLocalObjectsFailure(t *testing.T) { config := gctsExecuteABAPUnitTestsOptions{ Host: "http://testHost.com:50000", @@ -232,6 +257,9 @@ func TestGetPackageListFailure(t *testing.T) { Repository: "testRepo", Username: "testUser", Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", } t.Run("http error occurred", func(t *testing.T) { @@ -242,13 +270,13 @@ func TestGetPackageListFailure(t *testing.T) { } `} - _, err := getPackageList(&config, &httpClient) + _, err := getLocalObjects(&config, &httpClient) - assert.EqualError(t, err, "getting repository object/package list failed: a http error occurred") + assert.EqualError(t, err, "get local changed objects failed: could not get repository: a http error occurred") }) } -func TestExecuteTestsForPackageSuccess(t *testing.T) { +func TestGetRemoteObjectsSuccess(t *testing.T) { config := gctsExecuteABAPUnitTestsOptions{ Host: "http://testHost.com:50000", @@ -256,47 +284,615 @@ func TestExecuteTestsForPackageSuccess(t *testing.T) { Repository: "testRepo", Username: "testUser", Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", } - header := make(http.Header) - header.Add("Accept", "application/atomsvc+xml") - header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") - header.Add("saml2", "disabled") + t.Run("return multiple objects successfully", func(t *testing.T) { - t.Run("all unit tests were successful", func(t *testing.T) { + httpClient := httpMockGctsT{StatusCode: 200} - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: ` - - - - - - - - - - - - - `} + object1 := repoObject{ - err := executeTestsForPackage(&config, &httpClient, header, "ZP_PIPER") + Object: "ZCL_GCTS", + Type: "CLAS", + } + object2 := repoObject{ + + Object: "ZP_GCTS", + Type: "DEVC", + } + object3 := repoObject{ + + Object: "ZIF_GCTS_API", + Type: "INTF", + } + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + repoObjects = append(repoObjects, object2) + repoObjects = append(repoObjects, object3) + + objects, err := getRemoteObjects(&config, &httpClient) if assert.NoError(t, err) { t.Run("check url", func(t *testing.T) { - assert.Equal(t, "http://testHost.com:50000/sap/bc/adt/abapunit/testruns?sap-client=000", httpClient.URL) + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/compareCommits?fromCommit=7845abaujztrw785&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) }) t.Run("check method", func(t *testing.T) { - assert.Equal(t, "POST", httpClient.Method) + assert.Equal(t, "GET", httpClient.Method) }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + + } + + }) + + t.Run("no objects returned by http call", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: `{}`} + var repoObjects []repoObject + objects, err := getRemoteObjects(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/compareCommits?fromCommit=7845abaujztrw785&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + } + + }) +} + +func TestGetRemoteObjectsFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGcts{StatusCode: 500, ResponseBody: ` + { + "exception": "No relation between system and repository" + } + `} + + _, err := getRemoteObjects(&config, &httpClient) + + assert.EqualError(t, err, "get remote changed objects failed: get repository history failed: a http error occurred") + }) +} + +func TestGetLocalPackagesSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedPackages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("return multiple objects successfully", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object1 := repoObject{ + + Object: "SGCTS", + + Type: "DEVC", + } + object2 := repoObject{ + + Object: "SGCTS_2", + Type: "DEVC", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + repoObjects = append(repoObjects, object2) + + objects, err := getLocalPackages(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/objects/INTF/ZIF_GCTS_API?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + + } + + }) + + t.Run("no objects returned by http call", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedObjects", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: `{}`} + var repoObjects []repoObject + objects, err := getLocalPackages(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/compareCommits?fromCommit=xyz987654321&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + } + + }) +} + +func TestGetLocalPackagesFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "localChangedPackages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 500, ResponseBody: ` + { + "exception": "No relation between system and repository" + } + `} + + _, err := getLocalPackages(&config, &httpClient) + + assert.EqualError(t, err, "get local changed packages failed: could not get repository: a http error occurred") + }) +} + +func TestGetRemotePackagesSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedPackages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("return multiple objects successfully", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object1 := repoObject{ + + Object: "SGCTS", + + Type: "DEVC", + } + object2 := repoObject{ + + Object: "SGCTS_2", + Type: "DEVC", + } + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + repoObjects = append(repoObjects, object2) + + objects, err := getRemotePackages(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/objects/INTF/ZIF_GCTS_API?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + + } + + }) + + t.Run("no objects returned by http call", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedPackages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: `{}`} + var repoObjects []repoObject + objects, err := getRemoteObjects(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/compareCommits?fromCommit=7845abaujztrw785&toCommit=0123456789abcdefghijkl&sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + } + + }) +} + +func TestGetRemotePackagesFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "remoteChangedPackages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 500, ResponseBody: ` + { + "exception": "No relation between system and repository" + } + `} + + _, err := getRemotePackages(&config, &httpClient) + + assert.EqualError(t, err, "get remote changed packages failed: get repository history failed: a http error occurred") + }) +} + +func TestGetPackagesSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "packages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("return multiple objects successfully", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object1 := repoObject{ + + Pgmid: "R3TR", + Object: "ZP_GCTS", + Type: "DEVC", + Description: "Package(ABAP Objects)", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + + objects, err := getPackages(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/objects?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + + } + + }) + + t.Run("no objects returned by http call", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "packages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + httpClient := httpMockGcts{StatusCode: 200, ResponseBody: `{}`} + var repoObjects []repoObject + objects, err := getPackages(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/objects?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + } + + }) +} + +func TestGetPackagesFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "packages", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 500, ResponseBody: ` + { + "exception": "No relation between system and repository" + } + `} + + _, err := getPackages(&config, &httpClient) + + assert.EqualError(t, err, "get packages failed: could not get repository objects: a http error occurred") + }) +} + +func TestGetRepositoryObjectsSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("return multiple objects successfully", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object1 := repoObject{ + Pgmid: "R3TR", + Object: "ZCL_GCTS", + Type: "CLAS", + Description: "Class (ABAP Objects)", + } + + object3 := repoObject{ + Pgmid: "R3TR", + Object: "ZIF_GCTS_API", + Type: "INTF", + Description: "Interface (ABAP Objects)", + } + var repoObjects []repoObject + repoObjects = append(repoObjects, object1) + repoObjects = append(repoObjects, object3) + + objects, err := getRepositoryObjects(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/objects?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + + } + + }) + + t.Run("no objects returned by http call", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo2", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: `{}`} + var repoObjects []repoObject + objects, err := getRepositoryObjects(&config, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/objects?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check package objects", func(t *testing.T) { + assert.Equal(t, repoObjects, objects) + }) + } + + }) +} + +func TestGetRepositoryObjectsFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + } + + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 500, ResponseBody: ` + { + "exception": "No relation between system and repository" + } + `} + + _, err := getRepositoryObjects(&config, &httpClient) + + assert.EqualError(t, err, "could not get repository objects: a http error occurred") + }) +} + +func TestExecuteAUnitTestSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + AUnitResultsFileName: "AUnitResults.xml", + } + + t.Run("all unit tests were successful", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object := repoObject{ + Pgmid: "R3TR", + Object: "ZCL_GCTS", + Type: "CLAS", + Description: "Clas Object", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object) + + err := executeAUnitTest(&config, &httpClient, repoObjects) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + } }) t.Run("no unit tests found", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: ` + httpClient := httpMockGctsT{StatusCode: 200, ResponseBody: ` @@ -306,36 +902,259 @@ func TestExecuteTestsForPackageSuccess(t *testing.T) { `} + object := repoObject{ + Pgmid: "R3TR", + Object: "ZP_NON_EXISTANT", + Type: "CLAS", + Description: "Clas Object", + } - err := executeTestsForPackage(&config, &httpClient, header, "ZP_NON_EXISTANT") + var repoObjects []repoObject + repoObjects = append(repoObjects, object) + err := executeAUnitTest(&config, &httpClient, repoObjects) if assert.NoError(t, err) { t.Run("check url", func(t *testing.T) { - assert.Equal(t, "http://testHost.com:50000/sap/bc/adt/abapunit/testruns?sap-client=000", httpClient.URL) + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000", httpClient.URL) }) t.Run("check method", func(t *testing.T) { - assert.Equal(t, "POST", httpClient.Method) + assert.Equal(t, "GET", httpClient.Method) }) + } }) } -func TestExecuteTestsForPackageFailure(t *testing.T) { +func TestExecuteAUnitTestFailure(t *testing.T) { config := gctsExecuteABAPUnitTestsOptions{ - Host: "http://testHost.com:50000", - Client: "000", - Repository: "testRepo", - Username: "testUser", - Password: "testPassword", + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + AUnitResultsFileName: "AUnitResults.xml", } - t.Run("some unit tests failed", func(t *testing.T) { + var repoObjects []repoObject - httpClient := httpMockGcts{StatusCode: 200, ResponseBody: ` + t.Run("a http error occurred", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 403, ResponseBody: ` + CSRF token validation failed + `} + + header := make(http.Header) + header.Add("Accept", "application/atomsvc+xml") + header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") + header.Add("saml2", "disabled") + + err := executeAUnitTest(&config, &httpClient, repoObjects) + + assert.EqualError(t, err, "execute of Aunit test has failed: run of unit tests failed: discovery of the ABAP server failed: a http error occurred") + + }) +} + +func TestExecuteATCCheckSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "DEFAULT_REMOTE_REF", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + + header := make(http.Header) + header.Add("Accept", "application/atomsvc+xml") + header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") + header.Add("saml2", "disabled") + + t.Run("atc checks were found", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + object := repoObject{ + Pgmid: "R3TR", + Object: "ZCL_GCTS", + Type: "CLAS", + Description: "Clas Object", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object) + + err := executeATCCheck(&config, &httpClient, repoObjects) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + } + }) + + t.Run("no ATC Checks were found", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "CUSTOM_REMOTE_REF", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + + object := repoObject{ + Pgmid: "R3TR", + Object: "ZP_NON_EXISTANT", + Type: "CLAS", + Description: "Clas Object", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object) + err := executeATCCheck(&config, &httpClient, repoObjects) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/adt/atc/worklists/3E3D0764F95Z01ABCDHEF9C9F6B5C14P?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + } + }) +} + +func TestExecuteATCCheckFailure(t *testing.T) { + + object := repoObject{ + Pgmid: "R3TR", + Object: "ZP_PIPER", + Type: "CLAS", + Description: "Clas Object", + } + + var repoObjects []repoObject + repoObjects = append(repoObjects, object) + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + + t.Run("a http error occurred", func(t *testing.T) { + + httpClient := httpMockGcts{StatusCode: 403, ResponseBody: ` + CSRF token validation failed + `} + + header := make(http.Header) + header.Add("Accept", "application/atomsvc+xml") + header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") + header.Add("saml2", "disabled") + + err := executeATCCheck(&config, &httpClient, repoObjects) + + assert.EqualError(t, err, "execution of ATC Checks failed: get worklist failed: discovery of the ABAP server failed: a http error occurred") + + }) +} + +func TestParseAUnitResultSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AUnitResultsFileName: "AUnitResults.xml", + } + + t.Run("unit test is successful", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + var xmlBody = []byte(` + + + + + + + + + + + + `) + + var resp *runResult + xml.Unmarshal(xmlBody, &resp) + + parsedRes, err := parseUnitResult(&config, &httpClient, resp) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check file name", func(t *testing.T) { + assert.Equal(t, "/var/jenkins_home/workspace/myFirstPipeline//objects/CLAS/ZCL_GCTS/CINC ZCL_GCTS======================CCAU.abap", parsedRes.File[0].Name) + }) + + } + }) + + t.Run("unit test failed", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + var xmlBody = []byte(` @@ -363,32 +1182,620 @@ func TestExecuteTestsForPackageFailure(t *testing.T) { - - `} + `) - header := make(http.Header) - header.Add("Accept", "application/atomsvc+xml") - header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") - header.Add("saml2", "disabled") + var resp *runResult + xml.Unmarshal(xmlBody, &resp) - err := executeTestsForPackage(&config, &httpClient, header, "ZP_PIPER") + parsedRes, err := parseUnitResult(&config, &httpClient, resp) - assert.EqualError(t, err, "some unit tests failed") + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check file name", func(t *testing.T) { + assert.Equal(t, "/var/jenkins_home/workspace/myFirstPipeline//objects/CLAS/ZCL_GCTS_PIPER_DEMO/CINC ZCL_GCTS_PIPER_DEMO===========CCAU.abap", parsedRes.File[0].Name) + }) + + t.Run("check line number", func(t *testing.T) { + assert.Equal(t, "21", parsedRes.File[0].Error[0].Line) + }) + + t.Run("check severity", func(t *testing.T) { + assert.Equal(t, "error", parsedRes.File[0].Error[0].Severity) + }) + + t.Run("check source", func(t *testing.T) { + assert.Equal(t, "LTCL_MASTER/CHECK", parsedRes.File[0].Error[0].Source) + }) + + t.Run("check message", func(t *testing.T) { + assert.Equal(t, " Different Values: Expected [Hello] Actual [Hello2] Test 'LTCL_MASTER->CHECK' in Main Program 'ZCL_GCTS_PIPER_DEMO===========CP'.", parsedRes.File[0].Error[0].Message) + }) + + } }) +} + +func TestParseAUnitResultFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "DEFAULT_REMOTE_REF", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AUnitResultsFileName: "AUnitResults.xml", + } + + t.Run("parser fails", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 403} + + var xmlBody = []byte(` + + + + + + + + + Critical Assertion Error: 'Check: ASSERT_EQUALS' +
+ +
+ +
+
+ +
+ + + +
+
+
+
+
+
+
+
`) + + var resp *runResult + xml.Unmarshal(xmlBody, &resp) + + parsedRes, err := parseUnitResult(&config, &httpClient, resp) + + if assert.Error(t, err) { + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "parse AUnit Result failed: get file name has failed: could not check readable source format: could not get repository layout: a http error occurred", err.Error()) + }) + + assert.NotEmpty(t, parsedRes, "results are not empty") + + } + }) + +} + +func TestParseATCCheckResultSuccess(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "DEFAULT_REMOTE_REF", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + + t.Run("atc found", func(t *testing.T) { + + httpClient := httpMockGctsT{StatusCode: 200} + + var xmlBody = []byte(` + + + + + + + + + + + + + + + + `) + + var resp *worklist + + xml.Unmarshal(xmlBody, &resp) + + parsedRes, err := parseATCCheckResult(&config, &httpClient, resp) + + if assert.NoError(t, err) { + + t.Run("check file name", func(t *testing.T) { + assert.Equal(t, "/var/jenkins_home/workspace/myFirstPipeline//objects/CLAS/ZCL_GCTS/CPRI ZCL_GCTS.abap", parsedRes.File[0].Name) + }) + + t.Run("check line number", func(t *testing.T) { + assert.Equal(t, "20", parsedRes.File[0].Error[0].Line) + }) + + t.Run("check severity", func(t *testing.T) { + assert.Equal(t, "error", parsedRes.File[0].Error[0].Severity) + }) + + t.Run("check source", func(t *testing.T) { + assert.Equal(t, "ZCL_GCTS", parsedRes.File[0].Error[0].Source) + }) + + t.Run("check message", func(t *testing.T) { + assert.Equal(t, "Package Check (Remote-enabled) Package Violation - Error", parsedRes.File[0].Error[0].Message) + }) + + assert.NotEmpty(t, parsedRes, "results are not empty") + + } + }) + + t.Run("no ATC Checks were found", func(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "DEFAULT_REMOTE_REF", + Scope: "repository", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + + httpClient := httpMockGctsT{StatusCode: 200} + + var xmlBody = []byte(` + + + + + + + `) + var resp *worklist + xml.Unmarshal(xmlBody, &resp) + parsedRes, err := parseATCCheckResult(&config, &httpClient, resp) + + if assert.NoError(t, err) { + + assert.Equal(t, parsedRes.Version, "1.0") + + } + }) + +} + +func TestParseATCCheckResultFailure(t *testing.T) { + + config := gctsExecuteABAPUnitTestsOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + Commit: "0123456789abcdefghijkl", + AtcVariant: "DEFAULT_REMOTE_REF", + Scope: "repsoitory", + Workspace: "/var/jenkins_home/workspace/myFirstPipeline", + AtcResultsFileName: "ATCResults.xml", + } + t.Run("a http error occurred", func(t *testing.T) { - httpClient := httpMockGcts{StatusCode: 403, ResponseBody: ` - CSRF token validation failed - `} + httpClient := httpMockGctsT{StatusCode: 403} - header := make(http.Header) - header.Add("Accept", "application/atomsvc+xml") - header.Add("x-csrf-token", "ZegUEgfa50R7ZfGGxOtx2A==") - header.Add("saml2", "disabled") + var xmlBody = []byte(` + + + + + + + + + + + + + + + + `) - err := executeTestsForPackage(&config, &httpClient, header, "ZP_PIPER") + var resp *worklist + xml.Unmarshal(xmlBody, &resp) + parsedRes, err := parseATCCheckResult(&config, &httpClient, resp) - assert.EqualError(t, err, "execution of unit tests failed: a http error occurred") + assert.EqualError(t, err, "conversion of ATC check results to CheckStyle has failed: get file name has failed: could not check readable source format: could not get repository layout: a http error occurred") + assert.NotEmpty(t, parsedRes) }) + +} + +type httpMockGctsT struct { + Method string // is set during test execution + URL string // is set before test execution + Header map[string][]string // is set before test execution + ResponseBody string // is set before test execution + Options piperhttp.ClientOptions // is set during test + StatusCode int // is set during test +} + +func (c *httpMockGctsT) SetOptions(options piperhttp.ClientOptions) { + c.Options = options +} + +func (c *httpMockGctsT) SendRequest(method string, url string, r io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) { + + c.Method = method + c.URL = url + + switch url { + + case "http://testHost.com:50000/sap/bc/adt/core/discovery?sap-client=000": + + c.Header = map[string][]string{"X-Csrf-Token": {"ZegUEgfa50R7ZfGGxOtx2A=="}} + + c.ResponseBody = ` + + + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo?sap-client=000": + + c.ResponseBody = ` + { + "result": { + + "rid": "testRepo", + "name": "testRepo", + "role": "PROVIDED", + "type": "GIT", + "vsid": "vSID", + "privateFlag": "false", + "url": "http://github.com/testRepo", + "createdBy": "testUser", + "createdDate": "02/02/2022", + "objects": 3, + "currentCommit": "xyz987654321" + } + } + ` + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2?sap-client=000": + + c.ResponseBody = ` + { + "result": { + + "rid": "testRepo2", + "name": "testRepo2", + "role": "PROVIDED", + "type": "GIT", + "vsid": "vSID", + "privateFlag": "false", + "url": "http://github.com/testRepo2", + "createdBy": "testUser", + "createdDate": "02/02/2022", + "objects": 3, + "currentCommit": "xyz987654321" + } + + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/getCommit?sap-client=000": + + c.ResponseBody = ` + { + "commits": [ + { + + "id": "0123456789abcdefghijkl" + + }, + { + + "id": "7845abaujztrw785" + }, + { + + "id": "45poiztr785423" + } + ] + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo2/getCommit?sap-client=000": + + c.ResponseBody = ` + { + "commits": [ + { + + "id": "0123456789abcdefghijkl" + + }, + { + + "id": "7845abaujztrw785" + }, + { + + "id": "45poiztr785423" + } + ] + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/compareCommits?fromCommit=xyz987654321&toCommit=0123456789abcdefghijkl&sap-client=000": + + c.ResponseBody = ` + { + "objects": [ + { + + "name": "ZCL_GCTS", + "type": "CLAS", + "action": "Class (ABAP Objects)" + }, + { + + "name": "ZP_GCTS", + "type": "DEVC", + "action": "Package(ABAP Objects)" + }, + { + + "name": "ZIF_GCTS_API", + "type": "INTF", + "action": "Interface (ABAP Objects)" + } + ] + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/compareCommits?fromCommit=7845abaujztrw785&toCommit=0123456789abcdefghijkl&sap-client=000": + + c.ResponseBody = ` + { + "objects": [ + { + + "name": "ZCL_GCTS", + "type": "CLAS", + "action": "Class (ABAP Objects)" + }, + { + + "name": "ZP_GCTS", + "type": "DEVC", + "action": "Package(ABAP Objects)" + }, + { + + "name": "ZIF_GCTS_API", + "type": "INTF", + "action": "Interface (ABAP Objects)" + } + ] + } + ` + case "http://testHost.com:50000/sap/bc/cts_abapvcs/objects/CLAS/ZCL_GCTS?sap-client=000": + + c.ResponseBody = ` + + { + + "pgmid": "R3TR", + "object": "CLAS", + "objName": "ZCL_GCTS", + "srcsystem": "src", + "author": "HUGO", + "devclass": "SGCTS" + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/objects/INTF/ZIF_GCTS_API?sap-client=000": + + c.ResponseBody = ` + + { + + "pgmid": "R3TR", + "object": "INTF", + "objName": "ZIF_GCTS_API", + "srcsystem": "src", + "author": "HUGO", + "devclass": "SGCTS_2" + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/objects/DEVC/ZP_GCTS?sap-client=000": + + c.ResponseBody = ` + + { + + "pgmid": "R3TR", + "object": "DEVC", + "objName": "ZP_GCTS", + "srcsystem": "src", + "author": "HUGO", + "devclass": "SGCTS" + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/layout?sap-client=000": + + c.ResponseBody = + `{ + "layout": { + "formatVersion": 5, + "format": "json", + "objectStorage": "plain", + "metaInformation": ".gctsmetadata/", + "tableContent": "true", + "subdirectory": "src/", + "readableSource": "false" + } + } + ` + + case "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/objects?sap-client=000": + + c.ResponseBody = ` + { + "objects": [ + { + "pgmid": "R3TR", + "object": "ZCL_GCTS", + "type": "CLAS", + "description": "Class (ABAP Objects)" + }, + { + + "pgmid": "R3TR", + "object": "ZP_GCTS", + "type": "DEVC", + "description": "Package(ABAP Objects)" + }, + { + "pgmid": "R3TR", + "object": "ZIF_GCTS_API", + "type": "INTF", + "description": "Interface (ABAP Objects)" + } + ] + } + ` + + case "http://testHost.com:50000/sap/bc/adt/abapunit/testruns?sap-client=000": + + c.Header = map[string][]string{"Accept": {"application/xml"}} + c.Header = map[string][]string{"x-csrf-token": {"ZegUEgfa50R7ZfGGxOtx2A=="}} + c.Header = map[string][]string{"Content-Type": {"application/vnd.sap.adt.abapunit.testruns.result.v1+xml"}} + + c.ResponseBody = ` + + + + + + + + + + + + + ` + + case "http://testHost.com:50000/sap/bc/adt/atc/worklists?checkVariant=DEFAULT_REMOTE_REF&sap-client=000": + + c.Header = map[string][]string{"Location": {"/atc/worklists/worklistId/123Z076495C01ABCDEF9C9F6B257OD70"}} + + case "http://testHost.com:50000/sap/bc/adt/atc/worklists?checkVariant=CUSTOM_REMOTE_REF&sap-client=000": + + c.Header = map[string][]string{"Location": {"/atc/worklists/worklistId/3E3D0764F95Z01ABCDHEF9C9F6B5C14P"}} + + case "http://testHost.com:50000/sap/bc/adt/atc/runs?worklistId=123Z076495C01ABCDEF9C9F6B257OD70?sap-client=000": + + c.ResponseBody = + ` + + 123Z076495C01ABCDEF9C9F6B257OD70 + 2021-11-29T14:46:46Z + + + FINDING_STATS + 0,0,1 + + + ` + + case "http://testHost.com:50000/sap/bc/adt/atc/worklists/3E3D0764F95Z01ABCDHEF9C9F6B5C14P?sap-client=000": + c.ResponseBody = + ` + + + + + + + ` + + case "http://testHost.com:50000/sap/bc/adt/atc/worklists/123Z076495C01ABCDEF9C9F6B257OD70?sap-client=000": + c.ResponseBody = + ` + + + + + + + + + + + + + + + +` + } + + if r != nil { + _, err := ioutil.ReadAll(r) + + if err != nil { + return nil, err + } + } + + res := http.Response{ + StatusCode: c.StatusCode, + Header: c.Header, + Body: ioutil.NopCloser(bytes.NewReader([]byte(c.ResponseBody))), + } + + if c.StatusCode >= 400 { + return &res, errors.New("a http error occurred") + } + + return &res, nil } diff --git a/documentation/docs/steps/gctsExecuteABAPUnitTests.md b/documentation/docs/steps/gctsExecuteABAPUnitTests.md index cfbd04a5b..2a33192a0 100644 --- a/documentation/docs/steps/gctsExecuteABAPUnitTests.md +++ b/documentation/docs/steps/gctsExecuteABAPUnitTests.md @@ -4,9 +4,9 @@ ## Prerequisites -* You have gCTS installed and configured on your ABAP systems. - -Learn more about the SAP Git-enabled Change & Transport System (gCTS) [here](https://help.sap.com/viewer/4a368c163b08418890a406d413933ba7/201909.001/en-US/f319b168e87e42149e25e13c08d002b9.html). With gCTS, ABAP developments on ABAP servers can be maintained in Git repositories. +* [ATC](https://help.sap.com/viewer/c238d694b825421f940829321ffa326a/202110.000/en-US/4ec5711c6e391014adc9fffe4e204223.html) checks are enabled in transaction ATC in the ABAP systems where you want to use the step. +* [gCTS](https://help.sap.com/viewer/4a368c163b08418890a406d413933ba7/latest/en-US/26c9c6c5a89244cb9506c253d36c3fda.html) is available and configured in the ABAP systems where you want to use the step. +* The [Warnings-Next-Generation](https://plugins.jenkins.io/warnings-ng/) Plugin is installed in Jenkins. ## ${docGenParameters} @@ -24,7 +24,11 @@ gctsExecuteABAPUnitTests( host: 'https://abap.server.com:port', client: '000', abapCredentialsId: 'ABAPUserPasswordCredentialsId', - repository: 'myrepo' + repository: 'myrepo', + scope: 'localChangedObjects', + commit: "${GIT_COMMIT}", + workspace: "${WORKSPACE}" + ) ``` @@ -38,4 +42,82 @@ steps: client: '000' abapCredentialsId: 'ABAPUserPasswordCredentialsId' repository: 'myrepo' + scope: 'remoteChangedObjects' + commit: '38abb4814ae46b98e8e6c3e718cf1782afa9ca90' + workspace: '/var/jenkins_home/workspace/myFirstPipeline' +``` + +Example configuration when you define scope: *repository* or *packages*. For these two cases you do not need to specify a *commit*. + +```yaml +steps: + <...> + gctsExecuteABAPUnitTests: + host: 'https://abap.server.com:port' + client: '000' + abapCredentialsId: 'ABAPUserPasswordCredentialsId' + repository: 'myrepo' + scope: 'repository' + workspace: '/var/jenkins_home/workspace/myFirstPipeline' +``` + +Example configuration when you want to execute only ABAP Unit Test. + +```yaml +steps: + <...> + gctsExecuteABAPUnitTests: + host: 'https://abap.server.com:port' + client: '000' + abapCredentialsId: 'ABAPUserPasswordCredentialsId' + repository: 'myrepo' + atcCheck: false + scope: 'packages' + workspace: '/var/jenkins_home/workspace/myFirstPipeline' +``` + +Example configuration for the use of *recordIssue* step to make the findings visible in Jenkins interface. + +```groovy +stage('ABAP Unit Tests') { + steps{ + + script{ + + try{ + gctsExecuteABAPUnitTests( + script: this, + commit: "${GIT_COMMIT}", + workspace: "${WORKSPACE}") + } + catch (Exception ex) { + currentBuild.result = 'FAILURE' + unstable(message: "${STAGE_NAME} is unstable") + } + + } + } + } +stage('Results in Checkstyle') { + steps{ + + recordIssues( + enabledForFailure: true, aggregatingResults: true, + tools: [checkStyle(pattern: 'ATCResults.xml', reportEncoding: 'UTF8'),checkStyle(pattern: 'AUnitResults.xml', reportEncoding: 'UTF8')] + ) + + } + } + +} +``` + +**Note:** If you have disabled *atcCheck* or *aUnitTest*, than you also need to remove the corresponding *ATCResults.xml* or *AUnitResults.xml* from *recordIssues* step. In the example below the *atcCheck* was disabled, so *ATCResults.xml* was removed. + +```groovy +recordIssues( + enabledForFailure: true, aggregatingResults: true, + tools: [checkStyle(pattern: 'AUnitResults.xml', reportEncoding: 'UTF8')] + +) ``` diff --git a/go.sum b/go.sum index abfa55fcc..947c6ff2e 100644 --- a/go.sum +++ b/go.sum @@ -2625,4 +2625,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= \ No newline at end of file diff --git a/resources/metadata/gctsExecuteABAPUnitTests.yaml b/resources/metadata/gctsExecuteABAPUnitTests.yaml index 8199b7f3c..79300e3ff 100644 --- a/resources/metadata/gctsExecuteABAPUnitTests.yaml +++ b/resources/metadata/gctsExecuteABAPUnitTests.yaml @@ -1,19 +1,25 @@ metadata: name: gctsExecuteABAPUnitTests - description: Runs ABAP unit tests for all packages of the specified repository + description: Runs ABAP unit tests and ATC (ABAP Test Cockpit) checks for a specified object scope. longDescription: | - This step will execute every unit test associated with a package belonging to the specified local repository on an ABAP system. + This step executes ABAP unit test and ATC checks for a specified scope of objects that exist in a local Git repository on an ABAP system. + Depending on your use case, you can specify a scope of objects for which you want to execute the checks. In addition, you can choose whether you want to execute only ABAP units tests, or only ATC checks, or both. + By default, both checks are executed. + The results of the checks are stored in a [Checkstyle](https://checkstyle.sourceforge.io/) format. With the help of the Jenkins [Warnings-Next-Generation](https://plugins.jenkins.io/warnings-ng/) Plugin), you can view the issues found, and navigate to the exact line of the source code where the issue occurred. + To make the findings visible in Jenkins interface, you will need to use step recordIssues. An example will be shown in the Example section. +
+ You can use this step as of SAP S/4HANA 2020. spec: inputs: secrets: - name: abapCredentialsId - description: Jenkins credentials ID containing username and password for authentication to the ABAP system on which you want to perform the unit tests + description: ID taken from the Jenkins credentials store containing user name and password of the user that authenticates to the ABAP system on which you want to execute the checks. type: jenkins params: - name: username type: string - description: User to authenticate to the ABAP system + description: User that authenticates to the ABAP system. Note – Don´t provide this parameter directly. Either set it in the environment, or in the Jenkins credentials store, and provide the ID as value of the abapCredentialsId parameter. scope: - PARAMETERS - STAGES @@ -26,7 +32,7 @@ spec: param: username - name: password type: string - description: Password to authenticate to the ABAP system + description: Password of the ABAP user that authenticates to the ABAP system. Note – Don´t provide this parameter directly. Either set it in the environment, or in the Jenkins credentials store, and provide the ID as value of the abapCredentialsId parameter. scope: - PARAMETERS - STAGES @@ -37,17 +43,17 @@ spec: - name: abapCredentialsId type: secret param: password - - name: repository + - name: host type: string - description: Specifies the name (ID) of the local repsitory on the ABAP system + description: Protocol and host of the ABAP system, including the port. Please provide in the format ://:. Supported protocols are http and https. scope: - PARAMETERS - STAGES - STEPS mandatory: true - - name: host + - name: repository type: string - description: Specifies the protocol and host address, including the port. Please provide in the format `://:`. Supported protocols are `http` and `https`. + description: Name (ID) of the local repository on the ABAP system scope: - PARAMETERS - STAGES @@ -55,9 +61,90 @@ spec: mandatory: true - name: client type: string - description: Specifies the client of the ABAP system to be addressed + description: Client of the ABAP system in which you want to execute the checks scope: - PARAMETERS - STAGES - STEPS mandatory: true + - name: aUnitTest + type: bool + default: true + description: Indication whether you want to execute the unit test checks. + scope: + - PARAMETERS + - STAGES + - STEPS + - name: atcCheck + type: bool + default: true + description: Indication whether you want to execute the ATC checks. + scope: + - PARAMETERS + - STAGES + - STEPS + - name: atcVariant + type: string + default: "DEFAULT_REMOTE_REF" + description: Variant for ATC checks + scope: + - PARAMETERS + - STAGES + - STEPS + - name: scope + type: string + default: repository + enum: + - localChangedObjects + - remoteChangedObjects + - localChangedPackages + - remoteChangedPackages + - repository + - packages + description: | + Scope of objects for which you want to execute the checks: + + * localChangedObjects - object delta between the commit that triggered the pipeline and the current commit in the local repository. The checks are executed for the individual objects. + * remoteChangedObjects - object delta between the commit that triggered the pipeline and the current commit in the remote repository. The checks are executed for the individual objects. + * localChangedPackages - object delta between the commit that triggered the pipeline and the current commit in the local repository. All objects are resolved into packages. The checks are executed for the packages. + * remoteChangedPackages - object delta between the commit that triggered the pipeline and the current commit in the remote repository. All objects are resolved into packages. The checks are executed for the packages. + * repository - all objects that are part of the local repository. The checks are executed for the individual objects. Packages (DEVC) are excluded. This is the default scope. + * packages - all packages that are part of the local repository . The checks are executed for the packages. + scope: + - PARAMETERS + - STAGES + - STEPS + - name: commit + type: string + description: ID of the commit that triggered the pipeline or any other commit to compare objects. For scopes localChangedObjects, remoteChangedObjects, localChangedPackages and remoteChangedPackages secifying a commit it's mandatory. + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: false + - name: workspace + type: string + description: Absolute path to directory which contains the source code that your CI/CD tool checks out. For example in Jenkins, the workspace parameter is /var/jenkins_home/workspace// + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + - name: atcResultsFileName + type: string + description: Specifies output file name for the results from the ATC checks + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: false + default: "ATCResults.xml" + - name: aUnitResultsFileName + type: string + description: Specifies output file name for the results from the AUnit tests + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: false + default: "AUnitResults.xml"