From 57dddddd26185feb54e57b1fda9a7371eab532df Mon Sep 17 00:00:00 2001 From: Mike Cobbett <77053+techcobweb@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:22:03 +0100 Subject: [PATCH 01/15] makefile improvements, runs delete, unit tests missing. Signed-off-by: Mike Cobbett <77053+techcobweb@users.noreply.github.com> --- Makefile | 107 +++++++----- docs/generated/errors-list.md | 2 + docs/generated/galasactl_runs.md | 1 + docs/generated/galasactl_runs_delete.md | 31 ++++ pkg/cmd/commandCollection.go | 6 + pkg/cmd/runsDelete.go | 155 ++++++++++++++++++ pkg/cmd/runsSubmit.go | 2 +- pkg/errors/errorMessage.go | 2 + pkg/runs/runsDelete.go | 93 +++++++++++ pkg/runs/runsDelete_test.go | 65 ++++++++ ...ava_testUtils.go => java_test_fixtures.go} | 0 11 files changed, 423 insertions(+), 41 deletions(-) create mode 100644 docs/generated/galasactl_runs_delete.md create mode 100644 pkg/cmd/runsDelete.go create mode 100644 pkg/runs/runsDelete.go create mode 100644 pkg/runs/runsDelete_test.go rename pkg/utils/{java_testUtils.go => java_test_fixtures.go} (100%) diff --git a/Makefile b/Makefile index d512f5a6..01ec5c2a 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,15 @@ # # SPDX-License-Identifier: EPL-2.0 # + +# Rather than keep tabs on all of the source folders, lets create the list of things we are dependent upon +# dynamically using a cool shell script. + +# A list of source files. This is completely expanded-out. +# Evaluates to something like this: ./pkg/tokensformatter/tokensFormatter.go ./pkg/tokensformatter/summaryFormatter_test.go +# We want to run some targets if any of the source files have changed. +SOURCE_FILES := $(shell find . -iname "*.go"| tr "\n" " ") embedded_info + all: tests galasactl gendocs-galasactl galasactl: \ @@ -15,51 +24,69 @@ galasactl: \ # 'gendocs-galasactl' is a command-line tool which generates documentation about the galasactl tool. # When executed, the .md produced contain up-to-date information on tool syntax. -gendocs-galasactl: bin/gendocs-galasactl-darwin-arm64 bin/gendocs-galasactl-linux-arm64 bin/gendocs-galasactl-darwin-x86_64 bin/gendocs-galasactl-linux-x86_64 - -tests: galasactl-source build/coverage.txt build/coverage.html - -build/coverage.out : galasactl-source +gendocs-galasactl: \ + bin/gendocs-galasactl-darwin-arm64 \ + bin/gendocs-galasactl-linux-arm64 \ + bin/gendocs-galasactl-darwin-x86_64 \ + bin/gendocs-galasactl-linux-x86_64 + +tests: $(SOURCE_FILES) build/coverage.txt build/coverage.html + +# Build a list of the source packages +# Note: +# - We don't want to include the generated galasaapi +# - We don't want to include the top-level command code at ./cmd +# - We join them into a comma-separated list +# - We smarten-up the begining element (remove ".,") +# - We smarten-up the end element (remove trailing ",") +# - We remove any spaces in the whole thing,. +# So we end up with something like this: ./pkg/api,./pkg/auth,./pkg/cmd,./pkg/embedded,./pkg/errors,./pkg/files ...etc. +COVERAGE_SOURCE_PACKAGES := ${shell find . -iname "*.go" \ + | xargs dirname {} \ + | grep -v "/pkg/galasaapi" \ + | grep -v "[.]/cmd/" \ + | sort \ + | uniq \ + | tr '\n' ',' \ + | sed "s/[.],//g" \ + | sed "s/,$$//g" \ + | sed "s/ //g" \ +} + +# If any source code has changed, re-run the unit tests. +build/coverage.out : $(SOURCE_FILES) mkdir -p build - go test -v -cover -coverprofile=build/coverage.out -coverpkg pkg/api,./pkg/auth,./pkg/cmd,./pkg/runsformatter,./pkg/errors,./pkg/launcher,./pkg/utils,./pkg/runs,./pkg/properties,./pkg/propertiesformatter,./pkg/resources ./pkg/... + echo "Coverage source packages are $(COVERAGE_SOURCE_PACKAGES)" + go test -v -cover -coverprofile=build/coverage.out -coverpkg $(COVERAGE_SOURCE_PACKAGES) ./pkg/... + +build/coverage-sanitised.out : build/coverage.out + cat build/coverage.out \ + | grep -v "Mock" \ + | grep -v "ixtures" \ + > build/coverage-no-mocks.out -build/coverage.html : build/coverage.out +# Unit test output --> an html report. +build/coverage.html : build/coverage-sanitised.out go tool cover -html=build/coverage.out -o build/coverage.html -build/coverage.txt : build/coverage.out +# Unit test output --> a text file report. +build/coverage.txt : build/coverage-sanitised.out go tool cover -func=build/coverage.out > build/coverage.txt cat build/coverage.txt -galasactl-source : \ - ./cmd/galasactl/*.go \ - ./pkg/api/*.go \ - ./pkg/auth/*.go \ - ./pkg/runsformatter/*.go \ - ./pkg/cmd/*.go \ - ./pkg/utils/*.go \ - ./pkg/runs/*.go \ - ./pkg/launcher/*.go \ - ./pkg/files/*.go \ - ./pkg/images/*.go \ - ./pkg/props/*.go \ - ./pkg/properties/*.go \ - ./pkg/propertiesformatter/*.go \ - ./pkg/resources/*.go \ - ./pkg/spi/*.go \ - ./pkg/tokensformatter/*.go \ - embedded_info +coverage : build/coverage.txt # The build process -embedded_info : \ - pkg/embedded/templates/version/build.properties - +GENERATED_BUILD_PROPERTIES_FILE := pkg/embedded/templates/version/build.properties +embedded_info : $(GENERATED_BUILD_PROPERTIES_FILE) + pkg/embedded/templates/version : mkdir -p $@ # Build a properties file containing versions of things. # Then the galasactl can embed the data and read it at run-time. -pkg/embedded/templates/version/build.properties : VERSION pkg/embedded/templates/version Makefile build.gradle +$(GENERATED_BUILD_PROPERTIES_FILE) : VERSION pkg/embedded/templates/version Makefile build.gradle echo "# Property file generated at build-time" > $@ # Turn the contents of VERSION file into a properties file value. cat VERSION | sed "s/^/galasactl.version = /1" >> $@ ; echo "" >> $@ @@ -72,34 +99,34 @@ pkg/embedded/templates/version/build.properties : VERSION pkg/embedded/templates echo "# version of the rest api that is compiled and the client is expecting from the ecosystem." >> $@ cat build/dependencies/openapi.yaml | grep "version :" | cut -f2 -d'"' | sed "s/^/galasactl.rest.api.version = /" >> $@ -bin/galasactl-linux-x86_64 : galasactl-source +bin/galasactl-linux-x86_64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/galasactl-linux-x86_64 ./cmd/galasactl -bin/galasactl-windows-x86_64.exe : galasactl-source +bin/galasactl-windows-x86_64.exe : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/galasactl-windows-x86_64.exe ./cmd/galasactl -bin/galasactl-darwin-x86_64 : galasactl-source +bin/galasactl-darwin-x86_64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/galasactl-darwin-x86_64 ./cmd/galasactl -bin/galasactl-darwin-arm64 : galasactl-source +bin/galasactl-darwin-arm64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/galasactl-darwin-arm64 ./cmd/galasactl -bin/galasactl-linux-arm64 : galasactl-source +bin/galasactl-linux-arm64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/galasactl-linux-arm64 ./cmd/galasactl -bin/galasactl-linux-s390x : galasactl-source +bin/galasactl-linux-s390x : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -o bin/galasactl-linux-s390x ./cmd/galasactl -bin/gendocs-galasactl-darwin-arm64 : galasactl-source +bin/gendocs-galasactl-darwin-arm64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/gendocs-galasactl-darwin-arm64 ./cmd/gendocs-galasactl -bin/gendocs-galasactl-linux-arm64 : galasactl-source +bin/gendocs-galasactl-linux-arm64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/gendocs-galasactl-linux-arm64 ./cmd/gendocs-galasactl -bin/gendocs-galasactl-linux-x86_64 : galasactl-source +bin/gendocs-galasactl-linux-x86_64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/gendocs-galasactl-linux-x86_64 ./cmd/gendocs-galasactl -bin/gendocs-galasactl-darwin-x86_64 : galasactl-source +bin/gendocs-galasactl-darwin-x86_64 : $(SOURCE_FILES) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/gendocs-galasactl-darwin-x86_64 ./cmd/gendocs-galasactl clean: diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 19496af7..0d7bffad 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -152,6 +152,8 @@ The `galasactl` tool can generate the following errors: - GAL1154E: The provided token ID, '{}', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only. - GAL1155E: The id provided by the --id field cannot be an empty string. - GAL1156E: '{}' is not supported as a valid value. Valid values are 'me'. +- GAL1157E: An attempt to delete a run failed. Cause is {} +- GAL1158E: An attempt to delete a run failed. The server responded with an error. Cause is {} - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/docs/generated/galasactl_runs.md b/docs/generated/galasactl_runs.md index ab8c3551..abd4f5cd 100644 --- a/docs/generated/galasactl_runs.md +++ b/docs/generated/galasactl_runs.md @@ -24,6 +24,7 @@ Assembles, submits and monitors test runs in Galasa Ecosystem * [galasactl](galasactl.md) - CLI for Galasa * [galasactl runs cancel](galasactl_runs_cancel.md) - cancel an active run in the ecosystem +* [galasactl runs delete](galasactl_runs_delete.md) - Get the details of a test runname which ran or is running. * [galasactl runs download](galasactl_runs_download.md) - Download the artifacts of a test run which ran. * [galasactl runs get](galasactl_runs_get.md) - Get the details of a test runname which ran or is running. * [galasactl runs prepare](galasactl_runs_prepare.md) - prepares a list of tests diff --git a/docs/generated/galasactl_runs_delete.md b/docs/generated/galasactl_runs_delete.md new file mode 100644 index 00000000..6595cf40 --- /dev/null +++ b/docs/generated/galasactl_runs_delete.md @@ -0,0 +1,31 @@ +## galasactl runs delete + +Get the details of a test runname which ran or is running. + +### Synopsis + +Get the details of a test runname which ran or is running, displaying the results to the caller. + +``` +galasactl runs delete [flags] +``` + +### Options + +``` + -h, --help Displays the options for the 'runs delete' command. + --name string the name of the test run we want to delete. Cannot be used in conjunction with --requestor, --result or --active flags +``` + +### Options inherited from parent commands + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. +``` + +### SEE ALSO + +* [galasactl runs](galasactl_runs.md) - Manage test runs in the ecosystem + diff --git a/pkg/cmd/commandCollection.go b/pkg/cmd/commandCollection.go index a8048eb6..dec89295 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -53,6 +53,7 @@ const ( COMMAND_NAME_RUNS_SUBMIT_LOCAL = "runs submit local" COMMAND_NAME_RUNS_RESET = "runs reset" COMMAND_NAME_RUNS_CANCEL = "runs cancel" + COMMAND_NAME_RUNS_DELETE = "runs delete" COMMAND_NAME_RESOURCES = "resources" COMMAND_NAME_RESOURCES_APPLY = "resources apply" COMMAND_NAME_RESOURCES_CREATE = "resources create" @@ -297,6 +298,7 @@ func (commands *commandCollectionImpl) addRunsCommands(factory spi.Factory, root var runsSubmitLocalCommand spi.GalasaCommand var runsResetCommand spi.GalasaCommand var runsCancelCommand spi.GalasaCommand + var runsDeleteCommand spi.GalasaCommand runsCommand, err = NewRunsCmd(rootCommand) if err == nil { @@ -313,6 +315,9 @@ func (commands *commandCollectionImpl) addRunsCommands(factory spi.Factory, root runsResetCommand, err = NewRunsResetCommand(factory, runsCommand, rootCommand) if err == nil { runsCancelCommand, err = NewRunsCancelCommand(factory, runsCommand, rootCommand) + if err == nil { + runsDeleteCommand, err = NewRunsDeleteCommand(factory, runsCommand, rootCommand) + } } } } @@ -330,6 +335,7 @@ func (commands *commandCollectionImpl) addRunsCommands(factory spi.Factory, root commands.commandMap[runsSubmitLocalCommand.Name()] = runsSubmitLocalCommand commands.commandMap[runsResetCommand.Name()] = runsResetCommand commands.commandMap[runsCancelCommand.Name()] = runsCancelCommand + commands.commandMap[runsDeleteCommand.Name()] = runsDeleteCommand } return err diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go new file mode 100644 index 00000000..f360e469 --- /dev/null +++ b/pkg/cmd/runsDelete.go @@ -0,0 +1,155 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "log" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/runs" + "github.com/galasa-dev/cli/pkg/spi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +// Objective: Allow the user to do this: +// run get --runname 12345 +// And then show the results in a human-readable form. + +// Variables set by cobra's command-line parsing. +type RunsDeleteCmdValues struct { + runName string +} + +type RunsDeleteCommand struct { + values *RunsDeleteCmdValues + cobraCommand *cobra.Command +} + +func NewRunsDeleteCommand(factory spi.Factory, runsCommand spi.GalasaCommand, rootCommand spi.GalasaCommand) (spi.GalasaCommand, error) { + cmd := new(RunsDeleteCommand) + err := cmd.init(factory, runsCommand, rootCommand) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RunsDeleteCommand) Name() string { + return COMMAND_NAME_RUNS_DELETE +} + +func (cmd *RunsDeleteCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *RunsDeleteCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ + +func (cmd *RunsDeleteCommand) init(factory spi.Factory, runsCommand spi.GalasaCommand, rootCommand spi.GalasaCommand) error { + var err error + cmd.values = &RunsDeleteCmdValues{} + cmd.cobraCommand, err = cmd.createCobraCommand(factory, runsCommand, rootCommand.Values().(*RootCmdValues)) + return err +} + +func (cmd *RunsDeleteCommand) createCobraCommand( + factory spi.Factory, + runsCommand spi.GalasaCommand, + rootCmdValues *RootCmdValues, +) (*cobra.Command, error) { + + var err error + runsCmdValues := runsCommand.Values().(*RunsCmdValues) + + runsDeleteCobraCmd := &cobra.Command{ + Use: "delete", + Short: "Get the details of a test runname which ran or is running.", + Long: "Get the details of a test runname which ran or is running, displaying the results to the caller.", + Args: cobra.NoArgs, + Aliases: []string{"runs delete"}, + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.executeRunsDelete(factory, runsCmdValues, rootCmdValues) + }, + } + + runsDeleteCobraCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run we want to delete."+ + " Cannot be used in conjunction with --requestor, --result or --active flags") + + runsDeleteCobraCmd.MarkFlagRequired("name") + + runsCommand.CobraCommand().AddCommand(runsDeleteCobraCmd) + + return runsDeleteCobraCmd, err +} + +func (cmd *RunsDeleteCommand) executeRunsDelete( + factory spi.Factory, + runsCmdValues *RunsCmdValues, + rootCmdValues *RootCmdValues, +) error { + + var err error + + // Operations on the file system will all be relative to the current folder. + fileSystem := factory.GetFileSystem() + + err = utils.CaptureLog(fileSystem, rootCmdValues.logFileName) + if err == nil { + rootCmdValues.isCapturingLogs = true + + log.Println("Galasa CLI - Delete runs info about a execute") + + // Get the ability to query environment variables. + env := factory.GetEnvironment() + + var galasaHome spi.GalasaHome + galasaHome, err = utils.NewGalasaHome(fileSystem, env, rootCmdValues.CmdParamGalasaHomePath) + if err == nil { + + // Read the bootstrap properties. + var urlService *api.RealUrlResolutionService = new(api.RealUrlResolutionService) + var bootstrapData *api.BootstrapData + bootstrapData, err = api.LoadBootstrap(galasaHome, fileSystem, env, runsCmdValues.bootstrap, urlService) + if err == nil { + + var console = factory.GetStdOutConsole() + timeService := factory.GetTimeService() + + apiServerUrl := bootstrapData.ApiServerURL + log.Printf("The API server is at '%s'\n", apiServerUrl) + + authenticator := factory.GetAuthenticator( + apiServerUrl, + galasaHome, + ) + + var apiClient *galasaapi.APIClient + apiClient, err = authenticator.GetAuthenticatedAPIClient() + + if err == nil { + // Call to process the command in a unit-testable way. + err = runs.RunsDelete( + cmd.values.runName, + console, + apiServerUrl, + apiClient, + timeService, + ) + } + } + } + } + + log.Printf("executeRunsDelete returning %v", err) + return err +} diff --git a/pkg/cmd/runsSubmit.go b/pkg/cmd/runsSubmit.go index db8ce46c..4251cc01 100644 --- a/pkg/cmd/runsSubmit.go +++ b/pkg/cmd/runsSubmit.go @@ -170,7 +170,7 @@ func (cmd *RunsSubmitCommand) executeSubmit( if err == nil { timeService := factory.GetTimeService() - var launcherInstance launcher.Launcher = nil + var launcherInstance launcher.Launcher // The launcher we are going to use to start/monitor tests. apiServerUrl := bootstrapData.ApiServerURL diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 517fb77c..2fada191 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -249,6 +249,8 @@ var ( GALASA_ERROR_INVALID_TOKEN_ID_FORMAT = NewMessageType("GAL1154E: The provided token ID, '%s', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only.", 1154, STACK_TRACE_NOT_WANTED) GALASA_ERROR_MISSING_USER_LOGIN_ID_FLAG = NewMessageType("GAL1155E: The id provided by the --id field cannot be an empty string.", 1155, STACK_TRACE_NOT_WANTED) GALASA_ERROR_LOGIN_ID_NOT_SUPPORTED = NewMessageType("GAL1156E: '%s' is not supported as a valid value. Valid values are 'me'.", 1156, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run failed. The server responded with an error. Cause is %s", 1158, STACK_TRACE_NOT_WANTED) // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go new file mode 100644 index 00000000..a649dae7 --- /dev/null +++ b/pkg/runs/runsDelete.go @@ -0,0 +1,93 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "context" + "log" + "net/http" + "strconv" + + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/spi" +) + +// --------------------------------------------------- + +// GetRuns - performs all the logic to implement the `galasactl runs get` command, +// but in a unit-testable manner. +func RunsDelete( + runName string, + console spi.Console, + apiServerUrl string, + apiClient *galasaapi.APIClient, + timeService spi.TimeService, +) error { + var err error + + log.Printf("GetRuns entered.") + + if runName != "" { + // Validate the runName as best we can without contacting the ecosystem. + err = ValidateRunName(runName) + } + + if err == nil { + + requestorParameter := "" + resultParameter := "" + fromAgeHours := 0 + toAgeHours := 0 + shouldGetActive := false + var runs []galasaapi.Run + runs, err = GetRunsFromRestApi(runName, requestorParameter, resultParameter, fromAgeHours, toAgeHours, shouldGetActive, timeService, apiClient) + + if err == nil { + err = deleteRuns(runs, apiClient) + } + } + log.Printf("RunsDelete exiting. err is %v\n", err) + return err +} + +func deleteRuns( + runs []galasaapi.Run, + apiClient *galasaapi.APIClient, +) error { + var err error + + var restApiVersion string + var context context.Context = nil + + var httpResponse *http.Response + + for _, run := range runs { + runId := *run.RunId + + apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) + + httpResponse, err = apicall.Execute() + + if err != nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_DELETE_RUN_FAILED, err.Error()) + } else { + if httpResponse.StatusCode != http.StatusNoContent { + httpError := "\nhttp response status code: " + strconv.Itoa(httpResponse.StatusCode) + errString := httpError + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, errString) + } + } + + if err != nil { + break + } else { + log.Printf("Run runId:%s runName: %s was deleted OK.\n", runId, run.TestStructure.GetRunName()) + } + } + + return err +} diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go new file mode 100644 index 00000000..2d5cf339 --- /dev/null +++ b/pkg/runs/runsDelete_test.go @@ -0,0 +1,65 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func NewRunsDeleteServletMock( + t *testing.T, + runName string, + runId string, + runResultStrings []string, +) *httptest.Server { + + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, runName, runResultStrings) + } else if req.URL.Path == "/ras/runs/"+runId { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsPutStatusQueuedResponse(t, writer, req, runName) + } + })) + + return server +} + +// func TestCanDeleteARun(t *testing.T) { +// //Given +// runs := make([]galasaapi.Run, 0) + +// server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { +// assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) +// })) + +// runName := "J20" + +// //When +// console := NewMockConsole() +// apiServerUrl := server.URL +// apiClient := api.InitialiseAPI(apiServerUrl) +// mockTimeService := utils.NewMockTimeService() + +// err := RunsDelete( +// runName, +// console, +// apiServerUrl, +// apiClient, +// mockTimeService) + +// //Then +// assert.NotNil(t, err, "RunsDelete returned an unexpected error %s", err.Error) + +// } diff --git a/pkg/utils/java_testUtils.go b/pkg/utils/java_test_fixtures.go similarity index 100% rename from pkg/utils/java_testUtils.go rename to pkg/utils/java_test_fixtures.go From fd1bf6d24b825e205a8329b4e78fe7d7db0a9af8 Mon Sep 17 00:00:00 2001 From: Mike Cobbett <77053+techcobweb@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:30:20 +0100 Subject: [PATCH 02/15] some corrections of PR comments. Some error handling to do still Signed-off-by: Mike Cobbett <77053+techcobweb@users.noreply.github.com> --- pkg/cmd/runsDelete.go | 7 ++--- pkg/runs/runsDelete.go | 69 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index f360e469..f57b0bce 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -73,8 +73,8 @@ func (cmd *RunsDeleteCommand) createCobraCommand( runsDeleteCobraCmd := &cobra.Command{ Use: "delete", - Short: "Get the details of a test runname which ran or is running.", - Long: "Get the details of a test runname which ran or is running, displaying the results to the caller.", + Short: "Delete a named test run.", + Long: "Delete a named test run.", Args: cobra.NoArgs, Aliases: []string{"runs delete"}, RunE: func(cobraCmd *cobra.Command, args []string) error { @@ -82,8 +82,7 @@ func (cmd *RunsDeleteCommand) createCobraCommand( }, } - runsDeleteCobraCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run we want to delete."+ - " Cannot be used in conjunction with --requestor, --result or --active flags") + runsDeleteCobraCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run we want to delete.") runsDeleteCobraCmd.MarkFlagRequired("name") diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index a649dae7..60db09e1 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -18,7 +18,7 @@ import ( // --------------------------------------------------- -// GetRuns - performs all the logic to implement the `galasactl runs get` command, +// RunsDelete - performs all the logic to implement the `galasactl runs delete` command, // but in a unit-testable manner. func RunsDelete( runName string, @@ -29,7 +29,7 @@ func RunsDelete( ) error { var err error - log.Printf("GetRuns entered.") + log.Printf("RunsDelete entered.") if runName != "" { // Validate the runName as best we can without contacting the ecosystem. @@ -69,16 +69,27 @@ func deleteRuns( runId := *run.RunId apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) - httpResponse, err = apicall.Execute() + // 200-299 http status codes manifest in an error. if err != nil { - err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_DELETE_RUN_FAILED, err.Error()) - } else { - if httpResponse.StatusCode != http.StatusNoContent { - httpError := "\nhttp response status code: " + strconv.Itoa(httpResponse.StatusCode) - errString := httpError - err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, errString) + if httpResponse == nil { + // We never got a response, error sending it or something ? + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) + } else { + + contentType := httpResponse.Header.Get("Content-Type") + if contentType == "" { + // There is no content in the response + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, strconv.Itoa(httpResponse.StatusCode)) + } else { + // There is content in the response. + // Process the error response payload. + + // TODO: Fix this bit. + // httpResponse.Body + // GetApiErrorFromResponse() + } } } @@ -91,3 +102,43 @@ func deleteRuns( return err } + +// func convertToGalasaError( +// response *http.Response, +// identifier string, +// errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, +// errorMsgUnableToReadResponseBody *galasaErrors.MessageType, +// msg3 *galasaErrors.MessageType, +// errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, +// ) error { +// defer response.Body.Close() +// var err error +// var responseBodyBytes []byte +// statusCode := response.StatusCode + +// if response.ContentLength == 0 { +// log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) +// err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) +// } else { + +// responseBodyBytes, err = io.ReadAll(response.Body) +// if err != nil { +// err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) +// } else { + +// var errorFromServer *galasaErrors.GalasaAPIError +// errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBodyBytes) + +// if err != nil { +// //unable to parse response into api error. It should have been json. +// log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) +// err = galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode) +// } else { +// // server returned galasa api error structure we understand. +// log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode,errorMsg) +// err = galasaErrors.NewGalasaError(errorMsg, identifier, errorFromServer.Message) +// } +// } +// } +// return err +// } From 87efb62c8c87a94c32ae66e83a18e03beb7ca8c9 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:42:29 +0100 Subject: [PATCH 03/15] Add initial unit tests for runs delete Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- docs/generated/galasactl_runs.md | 2 +- docs/generated/galasactl_runs_delete.md | 6 +- pkg/cmd/runsDelete.go | 2 +- pkg/errors/errorMessage.go | 6 +- pkg/runs/runsDelete.go | 133 ++++++++++++------------ pkg/runs/runsDelete_test.go | 103 +++++++++++++----- 6 files changed, 155 insertions(+), 97 deletions(-) diff --git a/docs/generated/galasactl_runs.md b/docs/generated/galasactl_runs.md index abd4f5cd..f85b7169 100644 --- a/docs/generated/galasactl_runs.md +++ b/docs/generated/galasactl_runs.md @@ -24,7 +24,7 @@ Assembles, submits and monitors test runs in Galasa Ecosystem * [galasactl](galasactl.md) - CLI for Galasa * [galasactl runs cancel](galasactl_runs_cancel.md) - cancel an active run in the ecosystem -* [galasactl runs delete](galasactl_runs_delete.md) - Get the details of a test runname which ran or is running. +* [galasactl runs delete](galasactl_runs_delete.md) - Delete a named test run. * [galasactl runs download](galasactl_runs_download.md) - Download the artifacts of a test run which ran. * [galasactl runs get](galasactl_runs_get.md) - Get the details of a test runname which ran or is running. * [galasactl runs prepare](galasactl_runs_prepare.md) - prepares a list of tests diff --git a/docs/generated/galasactl_runs_delete.md b/docs/generated/galasactl_runs_delete.md index 6595cf40..0e54110f 100644 --- a/docs/generated/galasactl_runs_delete.md +++ b/docs/generated/galasactl_runs_delete.md @@ -1,10 +1,10 @@ ## galasactl runs delete -Get the details of a test runname which ran or is running. +Delete a named test run. ### Synopsis -Get the details of a test runname which ran or is running, displaying the results to the caller. +Delete a named test run. ``` galasactl runs delete [flags] @@ -14,7 +14,7 @@ galasactl runs delete [flags] ``` -h, --help Displays the options for the 'runs delete' command. - --name string the name of the test run we want to delete. Cannot be used in conjunction with --requestor, --result or --active flags + --name string the name of the test run we want to delete. ``` ### Options inherited from parent commands diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index f57b0bce..38f588cc 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -17,7 +17,7 @@ import ( ) // Objective: Allow the user to do this: -// run get --runname 12345 +// runs delete --name 12345 // And then show the results in a human-readable form. // Variables set by cobra's command-line parsing. diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 2fada191..0fbf41c5 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -249,8 +249,10 @@ var ( GALASA_ERROR_INVALID_TOKEN_ID_FORMAT = NewMessageType("GAL1154E: The provided token ID, '%s', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only.", 1154, STACK_TRACE_NOT_WANTED) GALASA_ERROR_MISSING_USER_LOGIN_ID_FLAG = NewMessageType("GAL1155E: The id provided by the --id field cannot be an empty string.", 1155, STACK_TRACE_NOT_WANTED) GALASA_ERROR_LOGIN_ID_NOT_SUPPORTED = NewMessageType("GAL1156E: '%s' is not supported as a valid value. Valid values are 'me'.", 1156, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run failed. The server responded with an error. Cause is %s", 1158, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run named '%s' failed. The server responded with an error. Cause is %s", 1158, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_BADLY_FORMATTED = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 60db09e1..85d8889d 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -7,10 +7,11 @@ package runs import ( "context" + "io" "log" "net/http" - "strconv" + "github.com/galasa-dev/cli/pkg/embedded" galasaErrors "github.com/galasa-dev/cli/pkg/errors" "github.com/galasa-dev/cli/pkg/galasaapi" "github.com/galasa-dev/cli/pkg/spi" @@ -48,6 +49,8 @@ func RunsDelete( if err == nil { err = deleteRuns(runs, apiClient) + } else { + console.WriteString(err.Error()) } } log.Printf("RunsDelete exiting. err is %v\n", err) @@ -65,80 +68,78 @@ func deleteRuns( var httpResponse *http.Response - for _, run := range runs { - runId := *run.RunId - - apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) - httpResponse, err = apicall.Execute() - - // 200-299 http status codes manifest in an error. - if err != nil { - if httpResponse == nil { - // We never got a response, error sending it or something ? - err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) - } else { - - contentType := httpResponse.Header.Get("Content-Type") - if contentType == "" { - // There is no content in the response - err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, strconv.Itoa(httpResponse.StatusCode)) + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + if err == nil { + for _, run := range runs { + runId := run.GetRunId() + // runName := *run.GetTestStructure().RunName + + apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) + httpResponse, err = apicall.Execute() + + // 200-299 http status codes manifest in an error. + if err != nil { + if httpResponse == nil { + // We never got a response, error sending it or something ? + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) } else { - // There is content in the response. - // Process the error response payload. + // err = convertToGalasaError( + // httpResponse, + // runName, + // GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, + // GALASA_ERROR_DELETE_RUN_FAILED, - // TODO: Fix this bit. - // httpResponse.Body - // GetApiErrorFromResponse() + // ) } } + + if err != nil { + break + } else { + log.Printf("Run runId:%s runName: %s was deleted OK.\n", runId, run.TestStructure.GetRunName()) + } } + } + return err +} + +func convertToGalasaError( + response *http.Response, + identifier string, + errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, + errorMsgUnableToReadResponseBody *galasaErrors.MessageType, + errorMsgReceivedFromApiServer *galasaErrors.MessageType, + errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, +) error { + defer response.Body.Close() + var err error + var responseBodyBytes []byte + statusCode := response.StatusCode + + if response.ContentLength == 0 { + log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) + err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) + } else { + + responseBodyBytes, err = io.ReadAll(response.Body) if err != nil { - break + err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) } else { - log.Printf("Run runId:%s runName: %s was deleted OK.\n", runId, run.TestStructure.GetRunName()) + + var errorFromServer *galasaErrors.GalasaAPIError + errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBodyBytes) + + if err != nil { + //unable to parse response into api error. It should have been json. + log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) + err = galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode) + } else { + // server returned galasa api error structure we understand. + log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) + err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + } } } - return err } - -// func convertToGalasaError( -// response *http.Response, -// identifier string, -// errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, -// errorMsgUnableToReadResponseBody *galasaErrors.MessageType, -// msg3 *galasaErrors.MessageType, -// errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, -// ) error { -// defer response.Body.Close() -// var err error -// var responseBodyBytes []byte -// statusCode := response.StatusCode - -// if response.ContentLength == 0 { -// log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) -// err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) -// } else { - -// responseBodyBytes, err = io.ReadAll(response.Body) -// if err != nil { -// err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) -// } else { - -// var errorFromServer *galasaErrors.GalasaAPIError -// errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBodyBytes) - -// if err != nil { -// //unable to parse response into api error. It should have been json. -// log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) -// err = galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode) -// } else { -// // server returned galasa api error structure we understand. -// log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode,errorMsg) -// err = galasaErrors.NewGalasaError(errorMsg, identifier, errorFromServer.Message) -// } -// } -// } -// return err -// } diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index 2d5cf339..7e5dbc36 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -6,10 +6,14 @@ package runs import ( + "encoding/json" "net/http" "net/http/httptest" "testing" + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -17,7 +21,8 @@ func NewRunsDeleteServletMock( t *testing.T, runName string, runId string, - runResultStrings []string, + runResultJsonStrings []string, + deleteRunStatusCode int, ) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { @@ -26,40 +31,90 @@ func NewRunsDeleteServletMock( acceptHeader := req.Header.Get("Accept") if req.URL.Path == "/ras/runs" { assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, runResultStrings) + WriteMockRasRunsResponse(t, writer, req, runName, runResultJsonStrings) } else if req.URL.Path == "/ras/runs/"+runId { assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsPutStatusQueuedResponse(t, writer, req, runName) + WriteMockRasRunsDeleteResponse(t, writer, req, runName, deleteRunStatusCode) } })) return server } -// func TestCanDeleteARun(t *testing.T) { -// //Given -// runs := make([]galasaapi.Run, 0) +func WriteMockRasRunsDeleteResponse( + t *testing.T, + writer http.ResponseWriter, + req *http.Request, + runName string, + statusCode int) { + + writer.WriteHeader(statusCode) + +} + +func createMockRun(runName string) galasaapi.Run { + run := *galasaapi.NewRun() + run.SetRunId(runName) + testStructure := *galasaapi.NewTestStructure() + testStructure.SetRunName(runName) + + run.SetTestStructure(testStructure) + return run +} -// server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { -// assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) -// })) +func TestCanDeleteARun(t *testing.T) { + // Given... + runName := "J20" -// runName := "J20" + // Create the mock run to be deleted + runToDelete := createMockRun(runName) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) -// //When -// console := NewMockConsole() -// apiServerUrl := server.URL -// apiClient := api.InitialiseAPI(apiServerUrl) -// mockTimeService := utils.NewMockTimeService() + server := NewRunsDeleteServletMock(t, runName, runName, []string{ runToDeleteJson }, 204) -// err := RunsDelete( -// runName, -// console, -// apiServerUrl, -// apiClient, -// mockTimeService) + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() -// //Then -// assert.NotNil(t, err, "RunsDelete returned an unexpected error %s", err.Error) + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) -// } + // Then... + assert.Nil(t, err, "RunsDelete returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") +} + +func TestDeleteNonExistantRunDisplaysError(t *testing.T) { + // Given... + nonExistantRunName := "run-does-not-exist" + + existingRunName := "J20" + existingRun := createMockRun(existingRunName) + existingRunBytes, _ := json.Marshal(existingRun) + existingRunJson := string(existingRunBytes) + + server := NewRunsDeleteServletMock(t, nonExistantRunName, nonExistantRunName, []string{ existingRunJson }, 404) + + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + nonExistantRunName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") +} From aa6f9e0e7a57d82032017d14b485607a2813b756 Mon Sep 17 00:00:00 2001 From: Mike Cobbett <77053+techcobweb@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:39:03 +0100 Subject: [PATCH 04/15] Make sure Mocks are not included in code coverage statistics Signed-off-by: Mike Cobbett <77053+techcobweb@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 01ec5c2a..08d460b1 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ build/coverage-sanitised.out : build/coverage.out cat build/coverage.out \ | grep -v "Mock" \ | grep -v "ixtures" \ - > build/coverage-no-mocks.out + > build/coverage-sanitised.out # Unit test output --> an html report. build/coverage.html : build/coverage-sanitised.out From 6cc5db4ea94d2383096b17b9b6952a6c82e105cc Mon Sep 17 00:00:00 2001 From: Mike Cobbett <77053+techcobweb@users.noreply.github.com> Date: Mon, 2 Sep 2024 23:10:35 +0100 Subject: [PATCH 05/15] test coverage ignores ixture files. Signed-off-by: Mike Cobbett <77053+techcobweb@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 08d460b1..9fa93a30 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ build/coverage.out : $(SOURCE_FILES) build/coverage-sanitised.out : build/coverage.out cat build/coverage.out \ | grep -v "Mock" \ - | grep -v "ixtures" \ + | grep -v "ixture" \ > build/coverage-sanitised.out # Unit test output --> an html report. From cf0a37535177ac7f7c4075b70feb3064d505f5f1 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:50:37 +0100 Subject: [PATCH 06/15] Add more runs delete unit tests Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- docs/generated/errors-list.md | 9 +- pkg/errors/errorMessage.go | 13 ++- pkg/errors/galasaAPIError.go | 14 ++- pkg/runs/runsDelete.go | 69 +++++++----- pkg/runs/runsDelete_test.go | 204 +++++++++++++++++++++++++++------- 5 files changed, 233 insertions(+), 76 deletions(-) diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 0d7bffad..a22a2879 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -152,8 +152,13 @@ The `galasactl` tool can generate the following errors: - GAL1154E: The provided token ID, '{}', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only. - GAL1155E: The id provided by the --id field cannot be an empty string. - GAL1156E: '{}' is not supported as a valid value. Valid values are 'me'. -- GAL1157E: An attempt to delete a run failed. Cause is {} -- GAL1158E: An attempt to delete a run failed. The server responded with an error. Cause is {} +- GAL1157E: An attempt to delete a run named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} +- GAL1159E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. +- GAL1160E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1161E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1162E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: {} +- GAL1163E: The run named '{}' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete +- GAL1164E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 0fbf41c5..1b0fb002 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -250,9 +250,16 @@ var ( GALASA_ERROR_MISSING_USER_LOGIN_ID_FLAG = NewMessageType("GAL1155E: The id provided by the --id field cannot be an empty string.", 1155, STACK_TRACE_NOT_WANTED) GALASA_ERROR_LOGIN_ID_NOT_SUPPORTED = NewMessageType("GAL1156E: '%s' is not supported as a valid value. Valid values are 'me'.", 1156, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run named '%s' failed. The server responded with an error. Cause is %s", 1158, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_BADLY_FORMATTED = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1157, STACK_TRACE_NOT_WANTED) + + // 4 related but slightly different errors, when an HTTP response arrives from the Galasa server, and we can/can't parse the payload to get the message details out. + GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server.", 1159, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE = NewMessageType("GAL1160E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1160, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT = NewMessageType("GAL1161E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1161, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR = NewMessageType("GAL1162E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: %s", 1162, STACK_TRACE_NOT_WANTED) + + GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON = NewMessageType("GAL1164E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1164, STACK_TRACE_NOT_WANTED) // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/errors/galasaAPIError.go b/pkg/errors/galasaAPIError.go index 4b756744..eb04bef8 100644 --- a/pkg/errors/galasaAPIError.go +++ b/pkg/errors/galasaAPIError.go @@ -20,7 +20,15 @@ type GalasaAPIError struct { // the reason for the failure. // NOTE: when this function is called ensure that the calling function has the `defer resp.Body.Close()` // called in order to ensure that the response body is closed when the function completes -func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error){ +func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error) { + return GetApiErrorFromResponseBytes(body, func(marshallingError error) error{ + err := NewGalasaError(GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, marshallingError) + return err + }, + ) +} + +func GetApiErrorFromResponseBytes(body []byte, marshallingErrorLambda func(marshallingError error) error) (*GalasaAPIError, error) { var err error apiError := new(GalasaAPIError) @@ -28,8 +36,8 @@ func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error){ err = json.Unmarshal(body, &apiError) if err != nil { - log.Printf("GetApiErrorFromResponse FAIL - %v", err) - err = NewGalasaError(GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, err.Error()) + log.Printf("GetApiErrorFromResponseBytes failed to unmarshal bytes into a galasa api error structure. %v", err.Error()) + err = marshallingErrorLambda(err) } return apiError, err } diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 85d8889d..184ba4c8 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -48,8 +48,15 @@ func RunsDelete( runs, err = GetRunsFromRestApi(runName, requestorParameter, resultParameter, fromAgeHours, toAgeHours, shouldGetActive, timeService, apiClient) if err == nil { - err = deleteRuns(runs, apiClient) - } else { + + if len(runs) == 0 { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND, runName) + } else { + err = deleteRuns(runs, apiClient) + } + } + + if err != nil { console.WriteString(err.Error()) } } @@ -72,7 +79,7 @@ func deleteRuns( if err == nil { for _, run := range runs { runId := run.GetRunId() - // runName := *run.GetTestStructure().RunName + runName := *run.GetTestStructure().RunName apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) httpResponse, err = apicall.Execute() @@ -83,13 +90,15 @@ func deleteRuns( // We never got a response, error sending it or something ? err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) } else { - // err = convertToGalasaError( - // httpResponse, - // runName, - // GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, - // GALASA_ERROR_DELETE_RUN_FAILED, - - // ) + err = httpResponseToGalasaError( + httpResponse, + runName, + galasaErrors.GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE, + galasaErrors.GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON, + ) } } @@ -104,13 +113,14 @@ func deleteRuns( return err } -func convertToGalasaError( +func httpResponseToGalasaError( response *http.Response, identifier string, errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, errorMsgUnableToReadResponseBody *galasaErrors.MessageType, - errorMsgReceivedFromApiServer *galasaErrors.MessageType, errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, + errorMsgReceivedFromApiServer *galasaErrors.MessageType, + errorMsgResponseContentTypeNotJson *galasaErrors.MessageType, ) error { defer response.Body.Close() var err error @@ -121,23 +131,30 @@ func convertToGalasaError( log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) } else { - - responseBodyBytes, err = io.ReadAll(response.Body) - if err != nil { - err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) + + contentType := response.Header.Get("Content-Type") + if contentType != "application/json" { + err = galasaErrors.NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) } else { - - var errorFromServer *galasaErrors.GalasaAPIError - errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBodyBytes) - + responseBodyBytes, err = io.ReadAll(response.Body) if err != nil { - //unable to parse response into api error. It should have been json. - log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) - err = galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode) + err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) } else { - // server returned galasa api error structure we understand. - log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) - err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + + var errorFromServer *galasaErrors.GalasaAPIError + errorFromServer, err = galasaErrors.GetApiErrorFromResponseBytes( + responseBodyBytes, + func (marshallingError error) error { + log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) + return galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode, marshallingError) + }, + ) + + if err == nil { + // server returned galasa api error structure we understand. + log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) + err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + } } } } diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index 7e5dbc36..a6debbe3 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -9,6 +9,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/galasa-dev/cli/pkg/api" @@ -17,13 +18,25 @@ import ( "github.com/stretchr/testify/assert" ) -func NewRunsDeleteServletMock( - t *testing.T, - runName string, - runId string, - runResultJsonStrings []string, - deleteRunStatusCode int, -) *httptest.Server { +func createMockRun(runName string, runId string) galasaapi.Run { + run := *galasaapi.NewRun() + run.SetRunId(runId) + testStructure := *galasaapi.NewTestStructure() + testStructure.SetRunName(runName) + + run.SetTestStructure(testStructure) + return run +} + +func TestCanDeleteARun(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { @@ -31,47 +44,87 @@ func NewRunsDeleteServletMock( acceptHeader := req.Header.Get("Accept") if req.URL.Path == "/ras/runs" { assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, runResultJsonStrings) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } else if req.URL.Path == "/ras/runs/"+runId { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsDeleteResponse(t, writer, req, runName, deleteRunStatusCode) + writer.WriteHeader(http.StatusNoContent) } })) - return server -} + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() -func WriteMockRasRunsDeleteResponse( - t *testing.T, - writer http.ResponseWriter, - req *http.Request, - runName string, - statusCode int) { + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) - writer.WriteHeader(statusCode) - + // Then... + assert.Nil(t, err, "RunsDelete returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") } -func createMockRun(runName string) galasaapi.Run { - run := *galasaapi.NewRun() - run.SetRunId(runName) - testStructure := *galasaapi.NewTestStructure() - testStructure.SetRunName(runName) +func TestDeleteNonExistantRunDisplaysError(t *testing.T) { + // Given... + nonExistantRunName := "runDoesNotExist123" - run.SetTestStructure(testStructure) - return run + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, nonExistantRunName, []string{}) + } else { + assert.Fail(t, "An unexpected http request was issued to the test case.") + } + })) + + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + nonExistantRunName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") + consoleOutputText := console.ReadText() + assert.Contains(t, consoleOutputText, nonExistantRunName) + assert.Contains(t, consoleOutputText, "GAL1163E") + assert.Contains(t, consoleOutputText, "The run named 'runDoesNotExist123' could not be deleted") } -func TestCanDeleteARun(t *testing.T) { +func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { // Given... runName := "J20" + runId := "J234567890" // Create the mock run to be deleted - runToDelete := createMockRun(runName) + runToDelete := createMockRun(runName, runId) runToDeleteBytes, _ := json.Marshal(runToDelete) runToDeleteJson := string(runToDeleteBytes) - server := NewRunsDeleteServletMock(t, runName, runName, []string{ runToDeleteJson }, 204) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } else if req.URL.Path == "/ras/runs/"+runId { + writer.WriteHeader(http.StatusInternalServerError) + } + })) console := utils.NewMockConsole() apiServerUrl := server.URL @@ -87,20 +140,35 @@ func TestCanDeleteARun(t *testing.T) { mockTimeService) // Then... - assert.Nil(t, err, "RunsDelete returned an unexpected error") - assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText , runName) + assert.Contains(t, consoleText , "GAL1159E") } -func TestDeleteNonExistantRunDisplaysError(t *testing.T) { +func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { // Given... - nonExistantRunName := "run-does-not-exist" - - existingRunName := "J20" - existingRun := createMockRun(existingRunName) - existingRunBytes, _ := json.Marshal(existingRun) - existingRunJson := string(existingRunBytes) + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) - server := NewRunsDeleteServletMock(t, nonExistantRunName, nonExistantRunName, []string{ existingRunJson }, 404) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } else if req.URL.Path == "/ras/runs/"+runId { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + })) console := utils.NewMockConsole() apiServerUrl := server.URL @@ -109,12 +177,64 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { // When... err := RunsDelete( - nonExistantRunName, + runName, console, apiServerUrl, apiClient, mockTimeService) // Then... - assert.NotNil(t, err, "RunsDelete did not return an error but it should have") + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1164E") + assert.Contains(t, consoleText, "Error details from the server are not in the json format") } + + +// func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { +// // Given... +// runName := "J20" +// runId := "J234567890" + +// // Create the mock run to be deleted +// runToDelete := createMockRun(runName, runId) +// runToDeleteBytes, _ := json.Marshal(runToDelete) +// runToDeleteJson := string(runToDeleteBytes) + +// server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + +// assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) +// acceptHeader := req.Header.Get("Accept") +// if req.URL.Path == "/ras/runs" { +// assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) +// WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) +// } else if req.URL.Path == "/ras/runs/"+runId { +// writer.WriteHeader(http.StatusInternalServerError) +// writer.Header().Add("Content-Type", "application/json") +// writer.Write([]byte(`{ "this", "isBadJson because it doesnt end in a close braces" `)) +// } +// })) + +// console := utils.NewMockConsole() +// apiServerUrl := server.URL +// apiClient := api.InitialiseAPI(apiServerUrl) +// mockTimeService := utils.NewMockTimeService() + +// // When... +// err := RunsDelete( +// runName, +// console, +// apiServerUrl, +// apiClient, +// mockTimeService) + +// // Then... +// assert.NotNil(t, err, "RunsDelete returned an unexpected error") +// consoleText := console.ReadText() +// assert.Contains(t, consoleText, runName) +// assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) +// assert.Contains(t, consoleText, "Gxxxx") +// assert.Contains(t, consoleText, "Error details from the server are not in the json format") +// } From d6f61c9102bf88e874efb4faf7dc1413dcaca7e3 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:40:25 +0100 Subject: [PATCH 07/15] Rework runs delete unit tests to use HTTP interactions Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- pkg/errors/errorMessage.go | 6 +- pkg/runs/runsDelete.go | 2 +- pkg/runs/runsDelete_test.go | 491 ++++++++++++++++++------------- pkg/utils/httpInteractionMock.go | 77 +++++ 4 files changed, 368 insertions(+), 208 deletions(-) create mode 100644 pkg/utils/httpInteractionMock.go diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 1b0fb002..69206fbd 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -250,15 +250,15 @@ var ( GALASA_ERROR_MISSING_USER_LOGIN_ID_FLAG = NewMessageType("GAL1155E: The id provided by the --id field cannot be an empty string.", 1155, STACK_TRACE_NOT_WANTED) GALASA_ERROR_LOGIN_ID_NOT_SUPPORTED = NewMessageType("GAL1156E: '%s' is not supported as a valid value. Valid values are 'me'.", 1156, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1157, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1158, STACK_TRACE_NOT_WANTED) // 4 related but slightly different errors, when an HTTP response arrives from the Galasa server, and we can/can't parse the payload to get the message details out. GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server.", 1159, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE = NewMessageType("GAL1160E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1160, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT = NewMessageType("GAL1161E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1161, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR = NewMessageType("GAL1162E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: %s", 1162, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR = NewMessageType("GAL1162E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1162, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON = NewMessageType("GAL1164E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1164, STACK_TRACE_NOT_WANTED) // Warnings... diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 184ba4c8..f0fb696a 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -153,7 +153,7 @@ func httpResponseToGalasaError( if err == nil { // server returned galasa api error structure we understand. log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) - err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, statusCode, errorFromServer.Message) } } } diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index a6debbe3..99e161a8 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -8,233 +8,316 @@ package runs import ( "encoding/json" "net/http" - "net/http/httptest" "strconv" "testing" "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/errors" "github.com/galasa-dev/cli/pkg/galasaapi" "github.com/galasa-dev/cli/pkg/utils" "github.com/stretchr/testify/assert" ) func createMockRun(runName string, runId string) galasaapi.Run { - run := *galasaapi.NewRun() - run.SetRunId(runId) - testStructure := *galasaapi.NewTestStructure() - testStructure.SetRunName(runName) + run := *galasaapi.NewRun() + run.SetRunId(runId) + testStructure := *galasaapi.NewTestStructure() + testStructure.SetRunName(runName) - run.SetTestStructure(testStructure) - return run + run.SetTestStructure(testStructure) + return run } func TestCanDeleteARun(t *testing.T) { - // Given... - runName := "J20" - runId := "J234567890" - - // Create the mock run to be deleted - runToDelete := createMockRun(runName, runId) - runToDeleteBytes, _ := json.Marshal(runToDelete) - runToDeleteJson := string(runToDeleteBytes) - - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - - assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) - acceptHeader := req.Header.Get("Accept") - if req.URL.Path == "/ras/runs" { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) - } else if req.URL.Path == "/ras/runs/"+runId { - writer.WriteHeader(http.StatusNoContent) - } - })) - - console := utils.NewMockConsole() - apiServerUrl := server.URL - apiClient := api.InitialiseAPI(apiServerUrl) - mockTimeService := utils.NewMockTimeService() - - // When... - err := RunsDelete( - runName, - console, - apiServerUrl, - apiClient, - mockTimeService) - - // Then... - assert.Nil(t, err, "RunsDelete returned an unexpected error") - assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.GetServerURL() + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.Nil(t, err, "RunsDelete returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") } func TestDeleteNonExistantRunDisplaysError(t *testing.T) { - // Given... - nonExistantRunName := "runDoesNotExist123" - - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) - acceptHeader := req.Header.Get("Accept") - if req.URL.Path == "/ras/runs" { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, nonExistantRunName, []string{}) - } else { - assert.Fail(t, "An unexpected http request was issued to the test case.") - } - })) - - console := utils.NewMockConsole() - apiServerUrl := server.URL - apiClient := api.InitialiseAPI(apiServerUrl) - mockTimeService := utils.NewMockTimeService() - - // When... - err := RunsDelete( - nonExistantRunName, - console, - apiServerUrl, - apiClient, - mockTimeService) - - // Then... - assert.NotNil(t, err, "RunsDelete did not return an error but it should have") - consoleOutputText := console.ReadText() - assert.Contains(t, consoleOutputText, nonExistantRunName) - assert.Contains(t, consoleOutputText, "GAL1163E") - assert.Contains(t, consoleOutputText, "The run named 'runDoesNotExist123' could not be deleted") + // Given... + nonExistantRunName := "runDoesNotExist123" + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, nonExistantRunName, []string{}) + } + + interactions := []utils.HttpInteraction{ getRunsInteraction } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.GetServerURL() + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + nonExistantRunName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") + consoleOutputText := console.ReadText() + assert.Contains(t, consoleOutputText, nonExistantRunName) + assert.Contains(t, consoleOutputText, "GAL1163E") + assert.Contains(t, consoleOutputText, "The run named 'runDoesNotExist123' could not be deleted") } func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { - // Given... - runName := "J20" - runId := "J234567890" - - // Create the mock run to be deleted - runToDelete := createMockRun(runName, runId) - runToDeleteBytes, _ := json.Marshal(runToDelete) - runToDeleteJson := string(runToDeleteBytes) - - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - - assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) - acceptHeader := req.Header.Get("Accept") - if req.URL.Path == "/ras/runs" { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) - } else if req.URL.Path == "/ras/runs/"+runId { - writer.WriteHeader(http.StatusInternalServerError) - } - })) - - console := utils.NewMockConsole() - apiServerUrl := server.URL - apiClient := api.InitialiseAPI(apiServerUrl) - mockTimeService := utils.NewMockTimeService() - - // When... - err := RunsDelete( - runName, - console, - apiServerUrl, - apiClient, - mockTimeService) - - // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") - consoleText := console.ReadText() - assert.Contains(t, consoleText , runName) - assert.Contains(t, consoleText , "GAL1159E") + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText , runName) + assert.Contains(t, consoleText , "GAL1159E") } func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { - // Given... - runName := "J20" - runId := "J234567890" - - // Create the mock run to be deleted - runToDelete := createMockRun(runName, runId) - runToDeleteBytes, _ := json.Marshal(runToDelete) - runToDeleteJson := string(runToDeleteBytes) - - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - - assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) - acceptHeader := req.Header.Get("Accept") - if req.URL.Path == "/ras/runs" { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) - } else if req.URL.Path == "/ras/runs/"+runId { - writer.WriteHeader(http.StatusInternalServerError) - writer.Header().Set("Content-Type", "application/notJsonOnPurpose") - writer.Write([]byte("something not json but non-zero-length.")) - } - })) - - console := utils.NewMockConsole() - apiServerUrl := server.URL - apiClient := api.InitialiseAPI(apiServerUrl) - mockTimeService := utils.NewMockTimeService() - - // When... - err := RunsDelete( - runName, - console, - apiServerUrl, - apiClient, - mockTimeService) - - // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") - consoleText := console.ReadText() - assert.Contains(t, consoleText, runName) - assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) - assert.Contains(t, consoleText, "GAL1164E") - assert.Contains(t, consoleText, "Error details from the server are not in the json format") + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.GetServerURL() + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1164E") + assert.Contains(t, consoleText, "Error details from the server are not in the json format") } +func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.GetServerURL() + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1161E") + assert.Contains(t, consoleText, "Error details from the server are not in a valid json format") + assert.Contains(t, consoleText, "Cause: 'unexpected end of JSON input'") +} -// func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { -// // Given... -// runName := "J20" -// runId := "J234567890" - -// // Create the mock run to be deleted -// runToDelete := createMockRun(runName, runId) -// runToDeleteBytes, _ := json.Marshal(runToDelete) -// runToDeleteJson := string(runToDeleteBytes) - -// server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - -// assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) -// acceptHeader := req.Header.Get("Accept") -// if req.URL.Path == "/ras/runs" { -// assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) -// WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) -// } else if req.URL.Path == "/ras/runs/"+runId { -// writer.WriteHeader(http.StatusInternalServerError) -// writer.Header().Add("Content-Type", "application/json") -// writer.Write([]byte(`{ "this", "isBadJson because it doesnt end in a close braces" `)) -// } -// })) - -// console := utils.NewMockConsole() -// apiServerUrl := server.URL -// apiClient := api.InitialiseAPI(apiServerUrl) -// mockTimeService := utils.NewMockTimeService() - -// // When... -// err := RunsDelete( -// runName, -// console, -// apiServerUrl, -// apiClient, -// mockTimeService) - -// // Then... -// assert.NotNil(t, err, "RunsDelete returned an unexpected error") -// consoleText := console.ReadText() -// assert.Contains(t, consoleText, runName) -// assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) -// assert.Contains(t, consoleText, "Gxxxx") -// assert.Contains(t, consoleText, "Error details from the server are not in the json format") -// } +func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + apiErrorCode := 5000 + apiErrorMessage := "this is an error from the API server" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + + apiError := errors.GalasaAPIError{ + Code: apiErrorCode, + Message: apiErrorMessage, + } + apiErrorBytes, _ := json.Marshal(apiError) + writer.Write(apiErrorBytes) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + + console := utils.NewMockConsole() + apiServerUrl := server.GetServerURL() + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1162E") + assert.Contains(t, consoleText, apiErrorMessage) +} diff --git a/pkg/utils/httpInteractionMock.go b/pkg/utils/httpInteractionMock.go new file mode 100644 index 00000000..ef776b02 --- /dev/null +++ b/pkg/utils/httpInteractionMock.go @@ -0,0 +1,77 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// The implementation of a HTTP interaction that allows unit tests to define +// interactions with the Galasa API server, with methods to validate requests +// and a lambda to write HTTP responses (which can be overridden as desired) +type HttpInteraction struct { + ExpectedPath string + ExpectedHttpMethod string + + // An override-able function to write a HTTP response for this interaction + WriteHttpResponseLambda func(writer http.ResponseWriter, req *http.Request) +} + +func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInteraction { + httpInteraction := HttpInteraction{ + ExpectedPath: expectedPath, + ExpectedHttpMethod: expectedHttpMethod, + } + + httpInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusOK) + } + + return httpInteraction +} + +func (interaction *HttpInteraction) ValidateRequest(t *testing.T, req *http.Request) { + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + assert.Equal(t, interaction.ExpectedHttpMethod, req.Method, "Actual HTTP request method did not match the expected method") + assert.Equal(t, interaction.ExpectedPath, req.URL.Path, "Actual request path did not match the expected path") +} + +//----------------------------------------------------------------------------- +// Wrapper of a mock HTTP server that uses HTTP interactions to handle requests +//----------------------------------------------------------------------------- +type MockHttpServer struct { + CurrentInteractionIndex int + Server *httptest.Server +} + +func NewMockHttpServer(t *testing.T, interactions []HttpInteraction) MockHttpServer { + mockHttpServer := MockHttpServer{} + mockHttpServer.CurrentInteractionIndex = 0 + + mockHttpServer.Server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + currentInteractionIndex := &mockHttpServer.CurrentInteractionIndex + if *currentInteractionIndex >= len(interactions) { + assert.Fail(t, "Mock server received an unexpected request to '%s' when it should not have", req.URL.Path) + } else { + currentInteraction := interactions[*currentInteractionIndex] + currentInteraction.ValidateRequest(t, req) + currentInteraction.WriteHttpResponseLambda(writer, req) + + // The next request to the server should get the next interaction, so advance the index by one + *currentInteractionIndex++ + } + })) + return mockHttpServer +} + +func (mockServer *MockHttpServer) GetServerURL() string { + return mockServer.Server.URL +} \ No newline at end of file From 3b421cdb2870bb0ec7d54fea62dc034cb584c373 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:03:24 +0100 Subject: [PATCH 08/15] Close the test server in runs delete tests Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- docs/generated/errors-list.md | 5 +++-- pkg/runs/runsDelete_test.go | 16 +++++++++++----- pkg/utils/httpInteractionMock.go | 5 +---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index a22a2879..3b75dd85 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -152,11 +152,12 @@ The `galasactl` tool can generate the following errors: - GAL1154E: The provided token ID, '{}', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only. - GAL1155E: The id provided by the --id field cannot be an empty string. - GAL1156E: '{}' is not supported as a valid value. Valid values are 'me'. -- GAL1157E: An attempt to delete a run named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} +- GAL1157E: An attempt to delete a run named '{}' failed. Cause is {} +- GAL1158E: An attempt to delete a run named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} - GAL1159E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. - GAL1160E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} - GAL1161E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' -- GAL1162E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: {} +- GAL1162E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1163E: The run named '{}' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete - GAL1164E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index 99e161a8..eb7b4aff 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -55,9 +55,10 @@ func TestCanDeleteARun(t *testing.T) { } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() - apiServerUrl := server.GetServerURL() + apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() @@ -87,9 +88,10 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { interactions := []utils.HttpInteraction{ getRunsInteraction } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() - apiServerUrl := server.GetServerURL() + apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() @@ -136,6 +138,7 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() apiServerUrl := server.Server.URL @@ -186,9 +189,10 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() - apiServerUrl := server.GetServerURL() + apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() @@ -238,9 +242,10 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() - apiServerUrl := server.GetServerURL() + apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() @@ -299,9 +304,10 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test } server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() console := utils.NewMockConsole() - apiServerUrl := server.GetServerURL() + apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() diff --git a/pkg/utils/httpInteractionMock.go b/pkg/utils/httpInteractionMock.go index ef776b02..c4f44821 100644 --- a/pkg/utils/httpInteractionMock.go +++ b/pkg/utils/httpInteractionMock.go @@ -30,6 +30,7 @@ func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInte ExpectedHttpMethod: expectedHttpMethod, } + // Set a basic implementation of the lambda to write a default response, which can be overridden by tests httpInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(http.StatusOK) } @@ -71,7 +72,3 @@ func NewMockHttpServer(t *testing.T, interactions []HttpInteraction) MockHttpSer })) return mockHttpServer } - -func (mockServer *MockHttpServer) GetServerURL() string { - return mockServer.Server.URL -} \ No newline at end of file From 7a5217d4ae40cb08adf75cf56b9de3935ec946b6 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:30:27 +0100 Subject: [PATCH 09/15] Fix 'runs delete' not exiting with a cobra error when not specifying --name Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- pkg/cmd/runsDelete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index 38f588cc..9b2a3257 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -82,7 +82,7 @@ func (cmd *RunsDeleteCommand) createCobraCommand( }, } - runsDeleteCobraCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run we want to delete.") + runsDeleteCobraCmd.Flags().StringVar(&cmd.values.runName, "name", "", "the name of the test run we want to delete.") runsDeleteCobraCmd.MarkFlagRequired("name") From c06bf1140687029c26b626ac96bf0804c0dbc0bf Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:42:21 +0100 Subject: [PATCH 10/15] Initial mocking-out of io.ReadAll in runs delete, added runs delete to readme Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- README.md | 14 ++++++ pkg/cmd/factory.go | 4 ++ pkg/cmd/runsDelete.go | 3 ++ pkg/runs/runsDelete.go | 9 ++-- pkg/runs/runsDelete_test.go | 89 ++++++++++++++++++++++++++++++++----- pkg/spi/byteReader.go | 13 ++++++ pkg/spi/factory.go | 1 + pkg/utils/byteReader.go | 24 ++++++++++ pkg/utils/byteReaderMock.go | 39 ++++++++++++++++ pkg/utils/factoryMock.go | 8 ++++ 10 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 pkg/spi/byteReader.go create mode 100644 pkg/utils/byteReader.go create mode 100644 pkg/utils/byteReaderMock.go diff --git a/README.md b/README.md index c9e00991..7bf1641a 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,20 @@ galasactl runs get --name C1234 --format badFormatterName ``` For a complete list of supported parameters see [here](./docs/generated/galasactl_runs_get.md). +## runs delete + +This command deletes a test run from an ecosystem's RAS. The name of the test run to delete can be provided to delete it along with any associated artifacts that have been stored. + +### Examples + +A run named "C1234" can be deleted using the following command: + +``` +galasactl runs delete --name C1234 +``` + +A complete list of supported parameters for the `runs delete` command is available [here](./docs/generated/galasactl_runs_delete.md) + ## runs download This command downloads all artifacts for a test run that are stored in an ecosystem's RAS. diff --git a/pkg/cmd/factory.go b/pkg/cmd/factory.go index 148a364e..f8d2aca3 100644 --- a/pkg/cmd/factory.go +++ b/pkg/cmd/factory.go @@ -60,3 +60,7 @@ func (factory *RealFactory) GetAuthenticator(apiServerUrl string, galasaHome spi jwtCache := auth.NewJwtCache(factory.GetFileSystem(), galasaHome, factory.GetTimeService()) return auth.NewAuthenticator(apiServerUrl, factory.GetFileSystem(), galasaHome, factory.GetTimeService(), factory.GetEnvironment(), jwtCache) } + +func (*RealFactory) GetByteReader() spi.ByteReader { + return utils.NewByteReader() +} diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index 9b2a3257..44d5e6aa 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -135,6 +135,8 @@ func (cmd *RunsDeleteCommand) executeRunsDelete( var apiClient *galasaapi.APIClient apiClient, err = authenticator.GetAuthenticatedAPIClient() + byteReader := factory.GetByteReader() + if err == nil { // Call to process the command in a unit-testable way. err = runs.RunsDelete( @@ -143,6 +145,7 @@ func (cmd *RunsDeleteCommand) executeRunsDelete( apiServerUrl, apiClient, timeService, + byteReader, ) } } diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index f0fb696a..44c28c9d 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -7,7 +7,6 @@ package runs import ( "context" - "io" "log" "net/http" @@ -27,6 +26,7 @@ func RunsDelete( apiServerUrl string, apiClient *galasaapi.APIClient, timeService spi.TimeService, + byteReader spi.ByteReader, ) error { var err error @@ -52,7 +52,7 @@ func RunsDelete( if len(runs) == 0 { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND, runName) } else { - err = deleteRuns(runs, apiClient) + err = deleteRuns(runs, apiClient, byteReader) } } @@ -67,6 +67,7 @@ func RunsDelete( func deleteRuns( runs []galasaapi.Run, apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, ) error { var err error @@ -93,6 +94,7 @@ func deleteRuns( err = httpResponseToGalasaError( httpResponse, runName, + byteReader, galasaErrors.GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, galasaErrors.GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE, galasaErrors.GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT, @@ -116,6 +118,7 @@ func deleteRuns( func httpResponseToGalasaError( response *http.Response, identifier string, + byteReader spi.ByteReader, errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, errorMsgUnableToReadResponseBody *galasaErrors.MessageType, errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, @@ -136,7 +139,7 @@ func httpResponseToGalasaError( if contentType != "application/json" { err = galasaErrors.NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) } else { - responseBodyBytes, err = io.ReadAll(response.Body) + responseBodyBytes, err = byteReader.ReadAll(response.Body) if err != nil { err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) } else { diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index eb7b4aff..e0017e2f 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -61,6 +61,7 @@ func TestCanDeleteARun(t *testing.T) { apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -68,7 +69,8 @@ func TestCanDeleteARun(t *testing.T) { console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... assert.Nil(t, err, "RunsDelete returned an unexpected error") @@ -94,6 +96,7 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -101,7 +104,8 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... assert.NotNil(t, err, "RunsDelete did not return an error but it should have") @@ -144,6 +148,7 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -151,10 +156,11 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText , runName) assert.Contains(t, consoleText , "GAL1159E") @@ -195,6 +201,7 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -202,10 +209,11 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) @@ -248,6 +256,7 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -255,10 +264,11 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) @@ -310,6 +320,7 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -317,13 +328,71 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) assert.Contains(t, consoleText, "GAL1162E") assert.Contains(t, consoleText, apiErrorMessage) } + + +func TestRunsDeleteFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService, + mockByteReader) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1160E") + assert.Contains(t, consoleText, "GAL1160E") + assert.Contains(t, consoleText, "Error details from the server could not be read") +} diff --git a/pkg/spi/byteReader.go b/pkg/spi/byteReader.go new file mode 100644 index 00000000..2c6a22db --- /dev/null +++ b/pkg/spi/byteReader.go @@ -0,0 +1,13 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package spi + +import "io" + +// An interface to allow for mocking out "io" package reading-related methods +type ByteReader interface { + ReadAll(reader io.Reader) ([]byte, error) +} diff --git a/pkg/spi/factory.go b/pkg/spi/factory.go index 54084ea2..5ef9b5ae 100644 --- a/pkg/spi/factory.go +++ b/pkg/spi/factory.go @@ -24,4 +24,5 @@ type Factory interface { GetStdErrConsole() Console GetTimeService() TimeService GetAuthenticator(apiServerUrl string, galasaHome GalasaHome) Authenticator + GetByteReader() ByteReader } diff --git a/pkg/utils/byteReader.go b/pkg/utils/byteReader.go new file mode 100644 index 00000000..5a8d22be --- /dev/null +++ b/pkg/utils/byteReader.go @@ -0,0 +1,24 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "io" + + "github.com/galasa-dev/cli/pkg/spi" +) + +// Implementation of a byte reader to allow mocking out methods from the io package +type ByteReaderImpl struct { +} + +func NewByteReader() spi.ByteReader { + return new(ByteReaderImpl) +} + +func (*ByteReaderImpl) ReadAll(reader io.Reader) ([]byte, error) { + return io.ReadAll(reader) +} diff --git a/pkg/utils/byteReaderMock.go b/pkg/utils/byteReaderMock.go new file mode 100644 index 00000000..7c65c206 --- /dev/null +++ b/pkg/utils/byteReaderMock.go @@ -0,0 +1,39 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "errors" + "io" + + "github.com/galasa-dev/cli/pkg/spi" +) + +// Mock implementation of a byte reader to allow for simulating failed read operations +type MockByteReader struct { + throwReadError bool +} + +func NewMockByteReaderAsMock(throwReadError bool) *MockByteReader { + return &MockByteReader{ + throwReadError: throwReadError, + } +} + +func NewMockByteReader() spi.ByteReader { + return NewMockByteReaderAsMock(false) +} + +func (mockReader *MockByteReader) ReadAll(reader io.Reader) ([]byte, error) { + var err error + var bytes []byte + if mockReader.throwReadError { + err = errors.New("simulating a read failure") + } else { + bytes, err = io.ReadAll(reader) + } + return bytes, err +} diff --git a/pkg/utils/factoryMock.go b/pkg/utils/factoryMock.go index 56b043d2..bb1443e0 100644 --- a/pkg/utils/factoryMock.go +++ b/pkg/utils/factoryMock.go @@ -18,6 +18,7 @@ type MockFactory struct { StdErrConsole spi.Console TimeService spi.TimeService Authenticator spi.Authenticator + ByteReader spi.ByteReader } func NewMockFactory() *MockFactory { @@ -72,3 +73,10 @@ func (factory *MockFactory) GetAuthenticator(apiServerUrl string, galasaHome spi } return factory.Authenticator } + +func (factory *MockFactory) GetByteReader() spi.ByteReader { + if factory.ByteReader == nil { + factory.ByteReader = NewMockByteReader() + } + return factory.ByteReader +} From 226b6b31cea66e19b0f475e65ecbe54e486c0f3a Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:06:32 +0100 Subject: [PATCH 11/15] Add runs delete CLI tests to test-ecosystem script Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- pkg/cmd/runsDelete.go | 2 +- pkg/runs/runsDelete.go | 2 +- test-scripts/runs-tests.sh | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index 44d5e6aa..4ffc4720 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -106,7 +106,7 @@ func (cmd *RunsDeleteCommand) executeRunsDelete( if err == nil { rootCmdValues.isCapturingLogs = true - log.Println("Galasa CLI - Delete runs info about a execute") + log.Println("Galasa CLI - Delete runs about to execute") // Get the ability to query environment variables. env := factory.GetEnvironment() diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 44c28c9d..7aae4f46 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -107,7 +107,7 @@ func deleteRuns( if err != nil { break } else { - log.Printf("Run runId:%s runName: %s was deleted OK.\n", runId, run.TestStructure.GetRunName()) + log.Printf("Run with runId '%s' and runName '%s', was deleted OK.\n", runId, run.TestStructure.GetRunName()) } } } diff --git a/test-scripts/runs-tests.sh b/test-scripts/runs-tests.sh index 83539996..0c05de1b 100755 --- a/test-scripts/runs-tests.sh +++ b/test-scripts/runs-tests.sh @@ -975,6 +975,60 @@ function launch_test_from_unknown_portfolio { success "Unknown portfolio could not be read. galasactl reported this error correctly." } +#-------------------------------------------------------------------------- +function runs_delete_check_run_can_be_deleted { + run_name=$1 + + h2 "Attempting to delete the run named '${run_name}' using runs delete..." + + cd ${BASEDIR}/temp + + cmd="${ORIGINAL_DIR}/bin/${binary} runs delete \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + info "Command is: $cmd" + + # We expect a return code of '0' because the run should have been deleted successfully. + $cmd + rc=$? + if [[ "${rc}" != "0" ]]; then + error "Failed to delete run '${run_name}'" + exit 1 + fi + + success "galasactl runs delete was able to delete an existing run OK." +} + +#-------------------------------------------------------------------------- +function runs_delete_non_existant_run_returns_error { + run_name="NonExistantRun123" + + h2 "Attempting to delete the non-existant run named '${run_name}' using runs delete..." + + cd ${BASEDIR}/temp + + cmd="${ORIGINAL_DIR}/bin/${binary} runs delete \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + info "Command is: $cmd" + + output_file="runs-delete-output.txt" + set -o pipefail + $cmd | tee $output_file + + # We expect a return code of '1' because the run does not exist and an error should be reported. + rc=$? + if [[ "${rc}" != "1" ]]; then + error "Failed to return an error when attempting to delete non-existant run '${run_name}'" + exit 1 + fi + + success "galasactl runs delete correctly reported an error when attempting to delete a non-existant run." +} + +#-------------------------------------------------------------------------- function test_runs_commands { # Launch test on ecosystem without a portfolio ... launch_test_on_ecosystem_without_portfolio @@ -1016,6 +1070,10 @@ function test_runs_commands { # Attempt to cancel an active run... # Temporarily commented out as failing and will block CLI builds. # runs_cancel_check_test_is_finished_and_cancelled + + # Attempt to delete a run... + runs_delete_check_run_can_be_deleted $RUN_NAME + runs_delete_non_existant_run_returns_error } From 77559e5ee9e6d12d377b02863d6a438c2d159e21 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:24:28 +0100 Subject: [PATCH 12/15] Check if run has been deleted in test-ecosystem script Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- test-scripts/runs-tests.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-scripts/runs-tests.sh b/test-scripts/runs-tests.sh index 0c05de1b..80715b54 100755 --- a/test-scripts/runs-tests.sh +++ b/test-scripts/runs-tests.sh @@ -981,6 +981,7 @@ function runs_delete_check_run_can_be_deleted { h2 "Attempting to delete the run named '${run_name}' using runs delete..." + mkdir -p ${BASEDIR}/temp cd ${BASEDIR}/temp cmd="${ORIGINAL_DIR}/bin/${binary} runs delete \ @@ -997,6 +998,23 @@ function runs_delete_check_run_can_be_deleted { exit 1 fi + h3 "Checking that the run '${run_name}' no longer exists" + + cmd="${ORIGINAL_DIR}/bin/${binary} runs get \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + output_file="runs-delete-output.txt" + set -o pipefail + $cmd | tee $output_file | grep -q "Total:0" + + # We expect a return code of '0' because there should be no runs with the given run name anymore. + rc=$? + if [[ "${rc}" != "0" ]]; then + error "Failed when checking if run '${run_name}' has been deleted. The run still exists when it should not." + exit 1 + fi + success "galasactl runs delete was able to delete an existing run OK." } @@ -1006,6 +1024,7 @@ function runs_delete_non_existant_run_returns_error { h2 "Attempting to delete the non-existant run named '${run_name}' using runs delete..." + mkdir -p ${BASEDIR}/temp cd ${BASEDIR}/temp cmd="${ORIGINAL_DIR}/bin/${binary} runs delete \ From b6566023fcf1bbedb0a3f1c2401afbd34482ae1d Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:37:20 +0100 Subject: [PATCH 13/15] Add unit test for deleting reruns Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- pkg/runs/runsDelete_test.go | 101 +++++++++++++++++++++++++++---- pkg/utils/httpInteractionMock.go | 14 ++--- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index e0017e2f..950a4685 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -40,12 +40,12 @@ func TestCanDeleteARun(t *testing.T) { // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(http.StatusNoContent) } @@ -77,13 +77,88 @@ func TestCanDeleteARun(t *testing.T) { assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") } +func TestCanDeleteRunAndReruns(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + reRun1Id := "ABC123" + reRun2Id := "DEF456" + + // Create the mock runs to be deleted - re-runs should have the same run name but different run IDs + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + reRun1 := createMockRun(runName, reRun1Id) + reRun1Bytes, _ := json.Marshal(reRun1) + reRun1Json := string(reRun1Bytes) + + reRun2 := createMockRun(runName, reRun2Id) + reRun2Bytes, _ := json.Marshal(reRun2) + reRun2Json := string(reRun2Bytes) + + runsAsJsonStrings := []string{ + runToDeleteJson, + reRun1Json, + reRun2Json, + } + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, runsAsJsonStrings) + } + + successfulDeleteFunc := func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + deleteRunInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunInteraction.WriteHttpResponseFunc = successfulDeleteFunc + + deleteRerun1Interaction := utils.NewHttpInteraction("/ras/runs/" + reRun1Id, http.MethodDelete) + deleteRerun1Interaction.WriteHttpResponseFunc = successfulDeleteFunc + + deleteRerun2Interaction := utils.NewHttpInteraction("/ras/runs/" + reRun2Id, http.MethodDelete) + deleteRerun2Interaction.WriteHttpResponseFunc = successfulDeleteFunc + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunInteraction, + deleteRerun1Interaction, + deleteRerun2Interaction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService, + mockByteReader) + + // Then... + assert.Nil(t, err, "RunsDelete returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") +} + func TestDeleteNonExistantRunDisplaysError(t *testing.T) { // Given... nonExistantRunName := "runDoesNotExist123" // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, nonExistantRunName, []string{}) } @@ -127,12 +202,12 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(http.StatusInternalServerError) } @@ -178,12 +253,12 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(http.StatusInternalServerError) writer.Header().Set("Content-Type", "application/notJsonOnPurpose") writer.Write([]byte("something not json but non-zero-length.")) @@ -233,12 +308,12 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusInternalServerError) writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) @@ -291,12 +366,12 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusInternalServerError) @@ -353,12 +428,12 @@ func TestRunsDeleteFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *test // Create the expected HTTP interactions with the API server getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) - getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + getRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) - deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + deleteRunsInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusInternalServerError) writer.Write([]byte(`{}`)) diff --git a/pkg/utils/httpInteractionMock.go b/pkg/utils/httpInteractionMock.go index c4f44821..6bb754fe 100644 --- a/pkg/utils/httpInteractionMock.go +++ b/pkg/utils/httpInteractionMock.go @@ -6,11 +6,11 @@ package utils import ( - "net/http" - "net/http/httptest" - "testing" + "net/http" + "net/http/httptest" + "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert" ) // The implementation of a HTTP interaction that allows unit tests to define @@ -21,7 +21,7 @@ type HttpInteraction struct { ExpectedHttpMethod string // An override-able function to write a HTTP response for this interaction - WriteHttpResponseLambda func(writer http.ResponseWriter, req *http.Request) + WriteHttpResponseFunc func(writer http.ResponseWriter, req *http.Request) } func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInteraction { @@ -31,7 +31,7 @@ func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInte } // Set a basic implementation of the lambda to write a default response, which can be overridden by tests - httpInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + httpInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(http.StatusOK) } @@ -64,7 +64,7 @@ func NewMockHttpServer(t *testing.T, interactions []HttpInteraction) MockHttpSer } else { currentInteraction := interactions[*currentInteractionIndex] currentInteraction.ValidateRequest(t, req) - currentInteraction.WriteHttpResponseLambda(writer, req) + currentInteraction.WriteHttpResponseFunc(writer, req) // The next request to the server should get the next interaction, so advance the index by one *currentInteractionIndex++ From ba9076bf753fc5c02b034f07a7f5618b976b9ed8 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:15:27 +0100 Subject: [PATCH 14/15] Make runs submit print result summary to stdout, interim report messages go to log instead of stdout Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- pkg/runs/submitter.go | 21 ++++++++++----------- pkg/runs/submitter_test.go | 2 +- test-scripts/runs-tests.sh | 25 ++++++++++++------------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/pkg/runs/submitter.go b/pkg/runs/submitter.go index dc012e3e..d2af4c1c 100644 --- a/pkg/runs/submitter.go +++ b/pkg/runs/submitter.go @@ -186,7 +186,7 @@ func (submitter *Submitter) executeSubmitRuns( now := submitter.timeService.Now() if now.After(nextProgressReport) { //convert TestRun - displayInterrimProgressReport(readyRuns, submittedRuns, finishedRuns, lostRuns, throttle) + submitter.displayInterrimProgressReport(readyRuns, submittedRuns, finishedRuns, lostRuns, throttle) nextProgressReport = now.Add(progressReportInterval) } } @@ -206,7 +206,7 @@ func (submitter *Submitter) executeSubmitRuns( return finishedRuns, lostRuns, err } -func displayInterrimProgressReport(readyRuns []TestRun, +func (submitter *Submitter) displayInterrimProgressReport(readyRuns []TestRun, submittedRuns map[string]*TestRun, finishedRuns map[string]*TestRun, lostRuns map[string]*TestRun, @@ -217,17 +217,16 @@ func displayInterrimProgressReport(readyRuns []TestRun, finished := len(finishedRuns) lost := len(lostRuns) - fmt.Println("Progress report") + log.Println("Progress report") for runName, run := range submittedRuns { log.Printf("*** Run %v is currently %v - %v/%v/%v\n", runName, run.Status, run.Stream, run.Bundle, run.Class) - fmt.Printf("Run %v - %v/%v/%v\n", runName, run.Stream, run.Bundle, run.Class) } - fmt.Println("----------------------------------------------------------------------------") - fmt.Printf("Run status: Ready=%v, Submitted=%v, Finished=%v, Lost=%v\n", ready, submitted, finished, lost) - fmt.Printf("Throttle=%v\n", throttle) + log.Println("----------------------------------------------------------------------------") + log.Printf("Run status: Ready=%v, Submitted=%v, Finished=%v, Lost=%v\n", ready, submitted, finished, lost) + log.Printf("Throttle=%v\n", throttle) if finished > 0 { - displayTestRunResults(finishedRuns, lostRuns) + submitter.displayTestRunResults(finishedRuns, lostRuns) } } @@ -453,7 +452,7 @@ func (submitter *Submitter) createReports(params utils.RunsSubmitCmdValues, finishedRuns map[string]*TestRun, lostRuns map[string]*TestRun) error { //convert TestRun tests into formattable data - displayTestRunResults(finishedRuns, lostRuns) + submitter.displayTestRunResults(finishedRuns, lostRuns) var err error if params.ReportYamlFilename != "" { @@ -475,7 +474,7 @@ func (submitter *Submitter) createReports(params utils.RunsSubmitCmdValues, return err } -func displayTestRunResults(finishedRuns map[string]*TestRun, lostRuns map[string]*TestRun) { +func (submitter *Submitter) displayTestRunResults(finishedRuns map[string]*TestRun, lostRuns map[string]*TestRun) { var formatter = runsformatter.NewSummaryFormatter() var err error var outputText string @@ -483,7 +482,7 @@ func displayTestRunResults(finishedRuns map[string]*TestRun, lostRuns map[string formattableTest := FormattableTestFromTestRun(finishedRuns, lostRuns) outputText, err = formatter.FormatRuns(formattableTest) if err == nil { - print(outputText) + submitter.console.WriteString(outputText) } } diff --git a/pkg/runs/submitter_test.go b/pkg/runs/submitter_test.go index b10a92cb..a4785929 100644 --- a/pkg/runs/submitter_test.go +++ b/pkg/runs/submitter_test.go @@ -466,7 +466,7 @@ func TestLocalLaunchCanUseAPortfolioOk(t *testing.T) { assert.Equal(t, obrName, launchesRecorded[0].ObrFromPortfolio) assert.Equal(t, bundleName+"/"+className, launchesRecorded[0].ClassName) } - + assert.Contains(t, console.ReadText(), bundleName + "/" + className) } func TestSubmitRunwithGherkinFile(t *testing.T) { diff --git a/test-scripts/runs-tests.sh b/test-scripts/runs-tests.sh index 80715b54..104b21ac 100755 --- a/test-scripts/runs-tests.sh +++ b/test-scripts/runs-tests.sh @@ -158,8 +158,6 @@ function runs_download_check_folder_names_during_test_run { # checks the folder names are correct with timestamps where appropriate h2 "Performing runs download while test is running..." - run_name=$1 - mkdir -p ${BASEDIR}/temp cd ${BASEDIR}/temp @@ -186,7 +184,7 @@ function runs_download_check_folder_names_during_test_run { cd ${BASEDIR}/temp - log_file="runs-submit-output.txt" + log_file="runs-submit-output-for-download.txt" cmd="${ORIGINAL_DIR}/bin/${binary} runs submit \ --bootstrap ${bootstrap} \ @@ -523,22 +521,23 @@ function get_result_with_runname { cd ${BASEDIR}/temp # Get the RunName from the output of galasactl runs submit + # The output of runs submit should look like: + # submitted-time(UTC) name requestor status result test-name + # 2024-09-05 12:45:33 C9955 galasa building Passed inttests/dev.galasa.inttests/dev.galasa.inttests.core.local.CoreLocalJava11Ubuntu + # + # Total:1 Passed:1 - # Gets the line from the last part of the output stream the RunName is found in - cat runs-submit-output.txt | grep -o "Run.*-" | tail -1 > line.txt - - # Get just the RunName from the line. - # There is a line in the output like this: - # Run C6967 - inttests/dev.galasa.inttests/dev.galasa.inttests.core.local.CoreLocalJava11Ubuntu - # Environment failure of the test results in "C6976(EnvFail)" ... so the '('...')' part needs removing also. - sed 's/Run //; s/ -//; s/[(].*[)]//;' line.txt > runname.txt - runname=$(cat runname.txt) + # Gets the run name from the second line of the runs submit output (after the headers). + # The run name should be the third field, after the date and time fields. + runname=$(cat runs-submit-output.txt | sed -n "2{p;q;}" | cut -f3 -d' ') if [[ "$runname" == "" ]]; then error "Run name not captured from previous run launch." exit 1 fi + info "Run name is: ${runname}" + cmd="${ORIGINAL_DIR}/bin/${binary} runs get \ --name ${runname} \ --bootstrap ${bootstrap} \ @@ -998,7 +997,7 @@ function runs_delete_check_run_can_be_deleted { exit 1 fi - h3 "Checking that the run '${run_name}' no longer exists" + h2 "Checking that the run '${run_name}' no longer exists" cmd="${ORIGINAL_DIR}/bin/${binary} runs get \ --name ${run_name} \ From a6d8f72564e78161a6361fcf845b745715e65aae Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:18:43 +0100 Subject: [PATCH 15/15] Disable gradle cache Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .github/workflows/pr-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index eb6dd555..114d7b7b 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -27,6 +27,7 @@ jobs: uses: gradle/actions/setup-gradle@v3 with: gradle-version: 8.9 + cache-disabled: true # Pull down dependencies with Gradle and put them in the right places. - name: Gather dependencies using Gradle