diff --git a/README.md b/README.md index d1c8ab23..ccac0315 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,35 @@ galasactl runs download --name C1234 --destination /Users/me/my/folder A complete list of supported parameters for the `runs download` command is available [here](./docs/generated/galasactl_runs_download.md). +## runs reset + +This command will reset a running test in the Ecosystem that is either stuck in a timeout condition or looping, by requeing the test. Note: The reset command does not wait for the server to complete the act of resetting the test, but if the command succeeds, then the server has accepted the request to reset the test. + + +## Example + +The run "C1234" can be reset using the following command: + +``` +galasactl runs reset --name C1234 +``` + + +## runs cancel + +If after running `runs reset` the test is still not able to run through successfully, it can be abandoned with `runs cancel`. + +This command will cancel a running test in the Ecosystem. It will not delete any information that is already stored in the RAS about the test, it will only cancel the execution of the test. Note: The cancel command does not wait for the server to complete the act of cancelling the test, but if the command succeeds, then the server has accepted the request to cancel the test. + +## Example + +The run "C1234" can be cancelled using the following command: + +``` +galasactl runs cancel --name C1234 +``` + + ## properties get This command retrieves details of properties in a namespace. diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 0a7a57e0..2dff61c9 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -125,6 +125,11 @@ The `galasactl` tool can generate the following errors: - GAL1123E: Failed to read 3270 terminal JSON because the content is in the wrong format. Reason: {} - GAL1124E: Internal Failure. Terminal image could not be encoded into PNG format. Reason: {} - GAL1125E: Authentication property {} is invalid. Please ensure that it the value is made up of two parts that are separated by a '{}'. +- GAL1132E: No active run found matching run name '{}'. +- GAL1133E: Error resetting run '{}'. Reason: '{}' +- GAL1134E: The runs reset operation failed. Unable to process the error information returned from the server. +- GAL1135E: Error cancelling run '{}'. Reason: '{}' +- GAL1136E: The runs cancel operation failed. Unable to process the error information returned from the server. - 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: {} @@ -136,3 +141,5 @@ The `galasactl` tool can generate the following errors: - GAL2502I: Rendered {} image files. +- GAL2503I: Run '{}' has been reset successfully. +- GAL2504I: Run '{}' has been cancelled successfully. diff --git a/docs/generated/galasactl_runs.md b/docs/generated/galasactl_runs.md index 188e11c6..ab8c3551 100644 --- a/docs/generated/galasactl_runs.md +++ b/docs/generated/galasactl_runs.md @@ -23,8 +23,10 @@ Assembles, submits and monitors test runs in Galasa Ecosystem ### SEE ALSO * [galasactl](galasactl.md) - CLI for Galasa +* [galasactl runs cancel](galasactl_runs_cancel.md) - cancel an active run in the ecosystem * [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 +* [galasactl runs reset](galasactl_runs_reset.md) - reset an active run in the ecosystem * [galasactl runs submit](galasactl_runs_submit.md) - submit a list of tests to the ecosystem diff --git a/docs/generated/galasactl_runs_cancel.md b/docs/generated/galasactl_runs_cancel.md new file mode 100644 index 00000000..f189af1b --- /dev/null +++ b/docs/generated/galasactl_runs_cancel.md @@ -0,0 +1,31 @@ +## galasactl runs cancel + +cancel an active run in the ecosystem + +### Synopsis + +Cancel an active test run in the ecosystem if it is stuck or looping. + +``` +galasactl runs cancel [flags] +``` + +### Options + +``` + -h, --help Displays the options for the 'runs cancel' command. + --name string the name of the test run to cancel +``` + +### 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/docs/generated/galasactl_runs_reset.md b/docs/generated/galasactl_runs_reset.md new file mode 100644 index 00000000..21babede --- /dev/null +++ b/docs/generated/galasactl_runs_reset.md @@ -0,0 +1,31 @@ +## galasactl runs reset + +reset an active run in the ecosystem + +### Synopsis + +Reset an active test run in the ecosystem if it is stuck or looping. + +``` +galasactl runs reset [flags] +``` + +### Options + +``` + -h, --help Displays the options for the 'runs reset' command. + --name string the name of the test run to reset +``` + +### 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 3ec00457..505ca533 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -47,6 +47,8 @@ const ( COMMAND_NAME_RUNS_PREPARE = "runs prepare" COMMAND_NAME_RUNS_SUBMIT = "runs submit" COMMAND_NAME_RUNS_SUBMIT_LOCAL = "runs submit local" + COMMAND_NAME_RUNS_RESET = "runs reset" + COMMAND_NAME_RUNS_CANCEL = "runs cancel" COMMAND_NAME_RESOURCES = "resources" COMMAND_NAME_RESOURCES_APPLY = "resources apply" COMMAND_NAME_RESOURCES_CREATE = "resources create" @@ -78,7 +80,7 @@ func (commands *commandCollectionImpl) GetRootCommand() GalasaCommand { func (commands *commandCollectionImpl) GetCommand(name string) (GalasaCommand, error) { var err error cmd, _ := commands.commandMap[name] - if cmd == nil{ + if cmd == nil { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_COMMAND_NOT_FOUND_IN_CMD_COLLECTION) log.Printf("Caller tried to lookup %s in the command collection and it was not found.\n", name) } @@ -266,6 +268,8 @@ func (commands *commandCollectionImpl) addRunsCommands(factory Factory, rootComm var runsPrepareCommand GalasaCommand var runsSubmitCommand GalasaCommand var runsSubmitLocalCommand GalasaCommand + var runsResetCommand GalasaCommand + var runsCancelCommand GalasaCommand if err == nil { runsCommand, err = NewRunsCmd(rootCommand) @@ -279,6 +283,12 @@ func (commands *commandCollectionImpl) addRunsCommands(factory Factory, rootComm runsSubmitCommand, err = NewRunsSubmitCommand(factory, runsCommand, rootCommand) if err == nil { runsSubmitLocalCommand, err = NewRunsSubmitLocalCommand(factory, runsSubmitCommand, runsCommand, rootCommand) + if err == nil { + runsResetCommand, err = NewRunsResetCommand(factory, runsCommand, rootCommand) + if err == nil { + runsCancelCommand, err = NewRunsCancelCommand(factory, runsCommand, rootCommand) + } + } } } } @@ -293,6 +303,8 @@ func (commands *commandCollectionImpl) addRunsCommands(factory Factory, rootComm commands.commandMap[runsPrepareCommand.Name()] = runsPrepareCommand commands.commandMap[runsSubmitCommand.Name()] = runsSubmitCommand commands.commandMap[runsSubmitLocalCommand.Name()] = runsSubmitLocalCommand + commands.commandMap[runsResetCommand.Name()] = runsResetCommand + commands.commandMap[runsCancelCommand.Name()] = runsCancelCommand } return err diff --git a/pkg/cmd/runsCancel.go b/pkg/cmd/runsCancel.go new file mode 100644 index 00000000..910e58ca --- /dev/null +++ b/pkg/cmd/runsCancel.go @@ -0,0 +1,149 @@ +/* + * 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/auth" + "github.com/galasa-dev/cli/pkg/runs" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +// Objective: Allow the user to do this: +// runs cancel --name U123 +// And then galasactl cancels the run by abandoning it. + +type RunsCancelCommand struct { + values *RunsCancelCmdValues + cobraCommand *cobra.Command +} + +type RunsCancelCmdValues struct { + runName string +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewRunsCancelCommand(factory Factory, runsCommand GalasaCommand, rootCommand GalasaCommand) (GalasaCommand, error) { + cmd := new(RunsCancelCommand) + err := cmd.init(factory, runsCommand, rootCommand) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RunsCancelCommand) Name() string { + return COMMAND_NAME_RUNS_CANCEL +} + +func (cmd *RunsCancelCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *RunsCancelCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RunsCancelCommand) init(factory Factory, runsCommand GalasaCommand, rootCommand GalasaCommand) error { + var err error + cmd.values = &RunsCancelCmdValues{} + cmd.cobraCommand, err = cmd.createRunsCancelCobraCmd( + factory, + runsCommand, + rootCommand.Values().(*RootCmdValues), + ) + return err +} + +func (cmd *RunsCancelCommand) createRunsCancelCobraCmd(factory Factory, + runsCommand GalasaCommand, + rootCmdValues *RootCmdValues, +) (*cobra.Command, error) { + + var err error = nil + runsCmdValues := runsCommand.Values().(*RunsCmdValues) + + runsCancelCmd := &cobra.Command{ + Use: "cancel", + Short: "cancel an active run in the ecosystem", + Long: "Cancel an active test run in the ecosystem if it is stuck or looping.", + Args: cobra.NoArgs, + Aliases: []string{"runs cancel"}, + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.executeCancel(factory, runsCmdValues, rootCmdValues) + }, + } + + runsCancelCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run to cancel") + + runsCancelCmd.MarkPersistentFlagRequired("name") + + runsCommand.CobraCommand().AddCommand(runsCancelCmd) + + return runsCancelCmd, err +} + +func (cmd *RunsCancelCommand) executeCancel( + factory 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 - Cancel an active run by abandoning it.") + + // Get the ability to query environment variables. + env := factory.GetEnvironment() + + var galasaHome utils.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) + + apiClient := auth.GetAuthenticatedAPIClient(apiServerUrl, fileSystem, galasaHome, timeService, env) + + // Call to process command in unit-testable way. + err = runs.CancelRun( + cmd.values.runName, + timeService, + console, + apiServerUrl, + apiClient, + ) + } + } + } + + log.Printf("executeRunsCancel returning %v\n", err) + return err +} diff --git a/pkg/cmd/runsCancel_test.go b/pkg/cmd/runsCancel_test.go new file mode 100644 index 00000000..7d452865 --- /dev/null +++ b/pkg/cmd/runsCancel_test.go @@ -0,0 +1,131 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunsCancelCommandInCommandCollection(t *testing.T) { + + factory := NewMockFactory() + commands, _ := NewCommandCollection(factory) + + runsCancelCommand, err := commands.GetCommand(COMMAND_NAME_RUNS_CANCEL) + assert.Nil(t, err) + + assert.Equal(t, COMMAND_NAME_RUNS_CANCEL, runsCancelCommand.Name()) + assert.NotNil(t, runsCancelCommand.Values()) + assert.IsType(t, &RunsCancelCmdValues{}, runsCancelCommand.Values()) + assert.NotNil(t, runsCancelCommand.CobraCommand()) +} + +func TestRunsCancelHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := NewMockFactory() + + var args []string = []string{"runs", "cancel", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + assert.Nil(t, err) + + // Check what the user saw is reasonable. + checkOutput("Displays the options for the 'runs cancel' command.", "", "", factory, t) +} + +func TestRunsCancelNoFlagsReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + + var args []string = []string{"runs", "cancel"} + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + // Check what the user saw is reasonable. + checkOutput("", "Error: required flag(s) \"name\" not set", "", factory, t) +} + +func TestRunsCancelNameFlagReturnsOk(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, cmd := setupTestCommandCollection(COMMAND_NAME_RUNS_CANCEL, factory, t) + + var args []string = []string{"runs", "cancel", "--name", "name"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.Nil(t, err) + + checkOutput("", "", "", factory, t) + + assert.Contains(t, cmd.Values().(*RunsCancelCmdValues).runName, "name") +} + +func TestRunsCancelNameNoParameterReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_RUNS_CANCEL, factory, t) + + var args []string = []string{"runs", "cancel", "--name"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "flag needs an argument: --name") + + // Check what the user saw was reasonable + checkOutput("", "Error: flag needs an argument: --name", "", factory, t) +} + +func TestRunsCancelUnknownParameterReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_RUNS_CANCEL, factory, t) + + var args []string = []string{"runs", "cancel", "--name", "name1", "--random", "random"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "unknown flag: --random") + + // Check what the user saw was reasonable + checkOutput("", "Error: unknown flag: --random", "", factory, t) +} + +func TestRunsCancelNameTwiceOverridesToLatestValue(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, cmd := setupTestCommandCollection(COMMAND_NAME_RUNS_CANCEL, factory, t) + + var args []string = []string{"runs", "cancel", "--name", "name1", "--name", "name2"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.Nil(t, err) + + // Check what the user saw was reasonable + checkOutput("", "", "", factory, t) + + assert.Contains(t, cmd.Values().(*RunsCancelCmdValues).runName, "name2") +} diff --git a/pkg/cmd/runsReset.go b/pkg/cmd/runsReset.go new file mode 100644 index 00000000..cd8f7114 --- /dev/null +++ b/pkg/cmd/runsReset.go @@ -0,0 +1,149 @@ +/* + * 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/auth" + "github.com/galasa-dev/cli/pkg/runs" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +// Objective: Allow the user to do this: +// runs reset --name U123 +// And then galasactl resets the run by requeuing it. + +type RunsResetCommand struct { + values *RunsResetCmdValues + cobraCommand *cobra.Command +} + +type RunsResetCmdValues struct { + runName string +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewRunsResetCommand(factory Factory, runsCommand GalasaCommand, rootCommand GalasaCommand) (GalasaCommand, error) { + cmd := new(RunsResetCommand) + err := cmd.init(factory, runsCommand, rootCommand) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RunsResetCommand) Name() string { + return COMMAND_NAME_RUNS_RESET +} + +func (cmd *RunsResetCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *RunsResetCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RunsResetCommand) init(factory Factory, runsCommand GalasaCommand, rootCommand GalasaCommand) error { + var err error + cmd.values = &RunsResetCmdValues{} + cmd.cobraCommand, err = cmd.createRunsResetCobraCmd( + factory, + runsCommand, + rootCommand.Values().(*RootCmdValues), + ) + return err +} + +func (cmd *RunsResetCommand) createRunsResetCobraCmd(factory Factory, + runsCommand GalasaCommand, + rootCmdValues *RootCmdValues, +) (*cobra.Command, error) { + + var err error = nil + runsCmdValues := runsCommand.Values().(*RunsCmdValues) + + runsResetCmd := &cobra.Command{ + Use: "reset", + Short: "reset an active run in the ecosystem", + Long: "Reset an active test run in the ecosystem if it is stuck or looping.", + Args: cobra.NoArgs, + Aliases: []string{"runs reset"}, + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.executeReset(factory, runsCmdValues, rootCmdValues) + }, + } + + runsResetCmd.PersistentFlags().StringVar(&cmd.values.runName, "name", "", "the name of the test run to reset") + + runsResetCmd.MarkPersistentFlagRequired("name") + + runsCommand.CobraCommand().AddCommand(runsResetCmd) + + return runsResetCmd, err +} + +func (cmd *RunsResetCommand) executeReset( + factory 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 - Reset an active run by requeuing it.") + + // Get the ability to query environment variables. + env := factory.GetEnvironment() + + var galasaHome utils.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) + + apiClient := auth.GetAuthenticatedAPIClient(apiServerUrl, fileSystem, galasaHome, timeService, env) + + // Call to process command in unit-testable way. + err = runs.ResetRun( + cmd.values.runName, + timeService, + console, + apiServerUrl, + apiClient, + ) + } + } + } + + log.Printf("executeRunsReset returning %v\n", err) + return err +} diff --git a/pkg/cmd/runsReset_test.go b/pkg/cmd/runsReset_test.go new file mode 100644 index 00000000..0030b129 --- /dev/null +++ b/pkg/cmd/runsReset_test.go @@ -0,0 +1,131 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunsResetCommandInCommandCollection(t *testing.T) { + + factory := NewMockFactory() + commands, _ := NewCommandCollection(factory) + + runsResetCommand, err := commands.GetCommand(COMMAND_NAME_RUNS_RESET) + assert.Nil(t, err) + + assert.Equal(t, COMMAND_NAME_RUNS_RESET, runsResetCommand.Name()) + assert.NotNil(t, runsResetCommand.Values()) + assert.IsType(t, &RunsResetCmdValues{}, runsResetCommand.Values()) + assert.NotNil(t, runsResetCommand.CobraCommand()) +} + +func TestRunsResetHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := NewMockFactory() + + var args []string = []string{"runs", "reset", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + assert.Nil(t, err) + + // Check what the user saw is reasonable. + checkOutput("Displays the options for the 'runs reset' command.", "", "", factory, t) +} + +func TestRunsResetNoFlagsReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + + var args []string = []string{"runs", "reset"} + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + // Check what the user saw is reasonable. + checkOutput("", "Error: required flag(s) \"name\" not set", "", factory, t) +} + +func TestRunsResetNameFlagReturnsOk(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, cmd := setupTestCommandCollection(COMMAND_NAME_RUNS_RESET, factory, t) + + var args []string = []string{"runs", "reset", "--name", "name"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.Nil(t, err) + + checkOutput("", "", "", factory, t) + + assert.Contains(t, cmd.Values().(*RunsResetCmdValues).runName, "name") +} + +func TestRunsResetNameNoParameterReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_RUNS_RESET, factory, t) + + var args []string = []string{"runs", "reset", "--name"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "flag needs an argument: --name") + + // Check what the user saw was reasonable + checkOutput("", "Error: flag needs an argument: --name", "", factory, t) +} + +func TestRunsResetUnknownParameterReturnsError(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_RUNS_RESET, factory, t) + + var args []string = []string{"runs", "reset", "--name", "name1", "--random", "random"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "unknown flag: --random") + + // Check what the user saw was reasonable + checkOutput("", "Error: unknown flag: --random", "", factory, t) +} + +func TestRunsResetNameTwiceOverridesToLatestValue(t *testing.T) { + // Given... + factory := NewMockFactory() + commandCollection, cmd := setupTestCommandCollection(COMMAND_NAME_RUNS_RESET, factory, t) + + var args []string = []string{"runs", "reset", "--name", "name1", "--name", "name2"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.Nil(t, err) + + // Check what the user saw was reasonable + checkOutput("", "", "", factory, t) + + assert.Contains(t, cmd.Values().(*RunsResetCmdValues).runName, "name2") +} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index da1238a6..1aed3853 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -217,10 +217,18 @@ var ( GALASA_ERROR_FAILED_TO_COMPRESS_BINARY_DATA = NewMessageType("GAL1228E: Internal failure. Contents of gzip could not be encoded and compressed. %v error: %v", 1228, STACK_TRACE_NOT_WANTED) GALASA_ERROR_FAILED_TO_FLUSH_BINARY_DATA = NewMessageType("GAL1229E: Internal failure. Contents of gzip could not be flushed while encoding and compressing. %v error: %v", 1229, STACK_TRACE_NOT_WANTED) GALASA_ERROR_FAILED_TO_CLOSE_GZIP_FILE = NewMessageType("GAL1230E: Internal failure. Gzip file could not be closed while encoding and compressing. %v error: %v", 1230, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_NO_ACTIVE_RUNS_WITH_RUNNAME = NewMessageType("GAL1132E: No active run found matching run name '%s'.", 1132, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_RESET_RUN_FAILED = NewMessageType("GAL1133E: Error resetting run '%v'. Reason: '%s'", 1133, STACK_TRACE_WANTED) + GALASA_ERROR_RESET_RUN_RESPONSE_PARSING = NewMessageType("GAL1134E: The runs reset operation failed. Unable to process the error information returned from the server.", 1134, STACK_TRACE_WANTED) + GALASA_ERROR_CANCEL_RUN_FAILED = NewMessageType("GAL1135E: Error cancelling run '%v'. Reason: '%s'", 1135, STACK_TRACE_WANTED) + GALASA_ERROR_CANCEL_RUN_RESPONSE_PARSING = NewMessageType("GAL1136E: The runs cancel operation failed. Unable to process the error information returned from the server.", 1136, STACK_TRACE_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) + // Information messages... GALASA_INFO_FOLDER_DOWNLOADED_TO = NewMessageType("GAL2501I: Downloaded %d artifacts to folder '%s'\n", 2501, STACK_TRACE_NOT_WANTED) GALASA_INFO_RENDERED_IMAGE_COUNT = NewMessageType("GAL2502I: Rendered %d image files.\n", 2502, STACK_TRACE_NOT_WANTED) + GALASA_INFO_RUNS_RESET_SUCCESS = NewMessageType("GAL2503I: Run '%s' has been reset successfully.", 2503, STACK_TRACE_NOT_WANTED) + GALASA_INFO_RUNS_CANCEL_SUCCESS = NewMessageType("GAL2504I: Run '%s' has been cancelled successfully.", 2504, STACK_TRACE_NOT_WANTED) ) diff --git a/pkg/runs/runs.go b/pkg/runs/runs.go new file mode 100644 index 00000000..3588785b --- /dev/null +++ b/pkg/runs/runs.go @@ -0,0 +1,80 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package runs + +import ( + "log" + "time" + + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" +) + +func getRunIdFromRunName(runName string, + timeService utils.TimeService, + apiClient *galasaapi.APIClient, +) (string, error) { + var err error + var runs []galasaapi.Run + var runId string + + requestorParameter := "" + resultParameter := "" + fromAgeHours := 0 + toAgeHours := 0 + shouldGetActive := true + + runs, err = GetRunsFromRestApi(runName, requestorParameter, resultParameter, fromAgeHours, toAgeHours, shouldGetActive, timeService, apiClient) + + if err == nil { + + if len(runs) > 1 { + + // More than 1 active run has been found with this runName, as multiple runs might be stuck in active state like ending + // So find the run with the first startTime, and attempt to reset that one + + firstRun := runs[0] + for _, run := range runs { + + firstRunStart := firstRun.TestStructure.GetStartTime() + thisRunStart := run.TestStructure.GetStartTime() + + firstRunStartTime, _ := time.Parse(time.RFC3339, firstRunStart) + thisRunStartTime, _ := time.Parse(time.RFC3339, thisRunStart) + + if thisRunStartTime.Before(firstRunStartTime) { + firstRun = run + } + + } + + runId = firstRun.GetRunId() + + } else if len(runs) == 1 { + + runId = runs[0].GetRunId() + + } else { + + log.Printf("No active runs found matching run name: '%s'", runName) + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_NO_ACTIVE_RUNS_WITH_RUNNAME, runName) + + } + } + + return runId, err +} + +func createUpdateRunStatusRequest(status string, result string) *galasaapi.UpdateRunStatusRequest { + var updateRunStatusRequest = galasaapi.NewUpdateRunStatusRequest() + + updateRunStatusRequest.SetStatus(status) + updateRunStatusRequest.SetResult(result) + + return updateRunStatusRequest +} diff --git a/pkg/runs/runsCancel.go b/pkg/runs/runsCancel.go new file mode 100644 index 00000000..c6671c4f --- /dev/null +++ b/pkg/runs/runsCancel.go @@ -0,0 +1,116 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/galasa-dev/cli/pkg/embedded" + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + galasaapi "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" +) + +var ( + CANCEL_STATUS = "finished" + CANCEL_RESULT = "cancelled" +) + +func CancelRun( + runName string, + timeService utils.TimeService, + console utils.Console, + apiServerUrl string, + apiClient *galasaapi.APIClient, +) error { + var err error + var runId string + + log.Println("CancelRun entered.") + + if runName == "" { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_MISSING_NAME_FLAG, runName) + } + + if (err == nil) && (runName != "") { + err = ValidateRunName(runName) + } + + if err == nil { + + runId, err = getRunIdFromRunName(runName, timeService, apiClient) + + if err == nil { + + updateRunStatusRequest := createUpdateRunStatusRequest(CANCEL_STATUS, CANCEL_RESULT) + + err = cancelRun(runName, runId, updateRunStatusRequest, apiClient) + + if err == nil { + consoleErr := console.WriteString(fmt.Sprintf(galasaErrors.GALASA_INFO_RUNS_CANCEL_SUCCESS.Template, runName)) + + // Console error is not as important to report as the original error if there was one. + if consoleErr != nil && err == nil { + err = consoleErr + } + } + + } + + } + + log.Printf("CancelRun exiting. err is %v\n", err) + return err +} + +func cancelRun(runName string, + runId string, + runStatusUpdateRequest *galasaapi.UpdateRunStatusRequest, + apiClient *galasaapi.APIClient, +) error { + var err error = nil + var resp *http.Response + var context context.Context = nil + var restApiVersion string + var responseBody []byte + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + + if err == nil { + + _, resp, err = apiClient.ResultArchiveStoreAPIApi.PutRasRunStatusById(context, runId). + UpdateRunStatusRequest(*runStatusUpdateRequest). + ClientApiVersion(restApiVersion).Execute() + + if (resp != nil) && (resp.StatusCode != http.StatusAccepted) { + defer resp.Body.Close() + + responseBody, err = io.ReadAll(resp.Body) + log.Printf("putRasRunStatusById Failed - HTTP Response - Status Code: '%v' Payload: '%v'\n", resp.StatusCode, string(responseBody)) + + if err == nil { + var errorFromServer *galasaErrors.GalasaAPIError + errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBody) + + if err == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_CANCEL_RUN_FAILED, runName, errorFromServer.Message) + } else { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_CANCEL_RUN_RESPONSE_PARSING) + } + + } else { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, err) + } + } + + } + + return err +} diff --git a/pkg/runs/runsCancel_test.go b/pkg/runs/runsCancel_test.go new file mode 100644 index 00000000..b4391b5d --- /dev/null +++ b/pkg/runs/runsCancel_test.go @@ -0,0 +1,223 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func WriteMockRasRunsPutStatusFinishedResponse( + t *testing.T, + writer http.ResponseWriter, + req *http.Request, + runName string) { + + var statusCode int + var response string + + if runName == "U123" { + statusCode = 202 + response = fmt.Sprintf("The request to cancel run %s has been received.", runName) + writer.Header().Set("Content-Type", "text/plain") + } else if runName == "U120" { + statusCode = 400 + response = `{ + "error_code": 5049, + "error_message": "GAL5049E: Error occured when trying to cancel the run 'U120'. The run has already completed." + }` + writer.Header().Set("Content-Type", "application/json") + } else if runName == "U121" { + statusCode = 400 + response = `{{ + not for parsing + }` + } + + writer.WriteHeader(statusCode) + writer.Write([]byte(response)) +} + +func NewRunsCancelServletMock( + 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) + WriteMockRasRunsPutStatusFinishedResponse(t, writer, req, runName) + } + })) + + return server +} + +//------------------------------------------------------------------ +// Test methods +//------------------------------------------------------------------ + +func TestRunsCancelWithOneActiveRunReturnsOK(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx123xxx" + + runResultStrings := []string{RUN_U123_RE_RUN} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Nil(t, err) + textGotBack := mockConsole.ReadText() + assert.Contains(t, textGotBack, "GAL2504I") + assert.Contains(t, textGotBack, runName) +} + +func TestRunsCancelWithMultipleActiveRunsReturnsOK(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx122xxx" + + runResultStrings := []string{RUN_U123_FIRST_RUN, RUN_U123_RE_RUN, RUN_U123_RE_RUN_2} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Nil(t, err) + textGotBack := mockConsole.ReadText() + assert.Contains(t, textGotBack, "GAL2504I") + assert.Contains(t, textGotBack, runName) +} + +func TestRunsCancelWithNoActiveRunReturnsError(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx123xxx" + + runResultStrings := []string{} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Contains(t, err.Error(), "GAL1132") + assert.Contains(t, err.Error(), runName) +} + +func TestRunsCancelWithInvalidRunNameReturnsError(t *testing.T) { + // Given ... + runName := "garbage" + runId := "xxx123xxx" + + runResultStrings := []string{RUN_U123_RE_RUN} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Contains(t, err.Error(), "GAL1075") + assert.Contains(t, err.Error(), runName) +} + +func TestRunsCancelWhereOperationFailedServerSideReturnsError(t *testing.T) { + // Given ... + runName := "U120" + runId := "xxx120xxx" + + runResultStrings := []string{RUN_U120} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Error(t, err) + assert.ErrorContains(t, err, "GAL1135") +} + +func TestRunsCancelWhereServerSideResponseCannotBeParsedReturnsError(t *testing.T) { + // Given ... + runName := "U121" + runId := "xxx121xxx" + + runResultStrings := []string{RUN_U121} + + server := NewRunsCancelServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := CancelRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Error(t, err) + assert.ErrorContains(t, err, "GAL1136") +} diff --git a/pkg/runs/runsReset.go b/pkg/runs/runsReset.go new file mode 100644 index 00000000..d57b8d86 --- /dev/null +++ b/pkg/runs/runsReset.go @@ -0,0 +1,116 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/galasa-dev/cli/pkg/embedded" + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + galasaapi "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" +) + +var ( + RESET_STATUS = "queued" + RESET_RESULT = "" +) + +func ResetRun( + runName string, + timeService utils.TimeService, + console utils.Console, + apiServerUrl string, + apiClient *galasaapi.APIClient, +) error { + var err error + var runId string + + log.Println("ResetRun entered.") + + if runName == "" { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_MISSING_NAME_FLAG, runName) + } + + if (err == nil) && (runName != "") { + err = ValidateRunName(runName) + } + + if err == nil { + + runId, err = getRunIdFromRunName(runName, timeService, apiClient) + + if err == nil { + + updateRunStatusRequest := createUpdateRunStatusRequest(RESET_STATUS, RESET_RESULT) + + err = resetRun(runName, runId, updateRunStatusRequest, apiClient) + + if err == nil { + consoleErr := console.WriteString(fmt.Sprintf(galasaErrors.GALASA_INFO_RUNS_RESET_SUCCESS.Template, runName)) + + // Console error is not as important to report as the original error if there was one. + if consoleErr != nil && err == nil { + err = consoleErr + } + } + + } + + } + + log.Printf("ResetRun exiting. err is %v\n", err) + return err +} + +func resetRun(runName string, + runId string, + runStatusUpdateRequest *galasaapi.UpdateRunStatusRequest, + apiClient *galasaapi.APIClient, +) error { + var err error = nil + var resp *http.Response + var context context.Context = nil + var restApiVersion string + var responseBody []byte + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + + if err == nil { + + _, resp, err = apiClient.ResultArchiveStoreAPIApi.PutRasRunStatusById(context, runId). + UpdateRunStatusRequest(*runStatusUpdateRequest). + ClientApiVersion(restApiVersion).Execute() + + if (resp != nil) && (resp.StatusCode != http.StatusAccepted) { + defer resp.Body.Close() + + responseBody, err = io.ReadAll(resp.Body) + log.Printf("putRasRunStatusById Failed - HTTP Response - Status Code: '%v' Payload: '%v'\n", resp.StatusCode, string(responseBody)) + + if err == nil { + var errorFromServer *galasaErrors.GalasaAPIError + errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBody) + + if err == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_RESET_RUN_FAILED, runName, errorFromServer.Message) + } else { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_RESET_RUN_RESPONSE_PARSING) + } + + } else { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, err.Error()) + } + } + + } + + return err +} diff --git a/pkg/runs/runsReset_test.go b/pkg/runs/runsReset_test.go new file mode 100644 index 00000000..4c82e25d --- /dev/null +++ b/pkg/runs/runsReset_test.go @@ -0,0 +1,224 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func WriteMockRasRunsPutStatusQueuedResponse( + t *testing.T, + writer http.ResponseWriter, + req *http.Request, + runName string) { + + var statusCode int + var response string + + if runName == "U123" { + statusCode = 202 + response = fmt.Sprintf("The request to reset run %s has been received.", runName) + writer.Header().Set("Content-Type", "text/plain") + } else if runName == "U120" { + statusCode = 400 + response = `{ + "error_code": 5049, + "error_message": "GAL5049E: Error occured when trying to reset the run 'U120'. The run has already completed." + }` + writer.Header().Set("Content-Type", "application/json") + } else if runName == "U121" { + statusCode = 400 + response = `{{ + not for parsing + }` + writer.Header().Set("Content-Type", "application/json") + } + + writer.WriteHeader(statusCode) + writer.Write([]byte(response)) +} + +func NewRunsResetServletMock( + 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 +} + +//------------------------------------------------------------------ +// Test methods +//------------------------------------------------------------------ + +func TestRunsResetWithOneActiveRunReturnsOK(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx123xxx" + + runResultStrings := []string{RUN_U123_RE_RUN} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Nil(t, err) + textGotBack := mockConsole.ReadText() + assert.Contains(t, textGotBack, "GAL2503I") + assert.Contains(t, textGotBack, runName) +} + +func TestRunsResetWithMultipleActiveRunsReturnsOK(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx122xxx" + + runResultStrings := []string{RUN_U123_FIRST_RUN, RUN_U123_RE_RUN, RUN_U123_RE_RUN_2} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Nil(t, err) + textGotBack := mockConsole.ReadText() + assert.Contains(t, textGotBack, "GAL2503I") + assert.Contains(t, textGotBack, runName) +} + +func TestRunsResetWithNoActiveRunReturnsError(t *testing.T) { + // Given ... + runName := "U123" + runId := "xxx123xxx" + + runResultStrings := []string{} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Contains(t, err.Error(), "GAL1132") + assert.Contains(t, err.Error(), runName) +} + +func TestRunsResetWithInvalidRunNameReturnsError(t *testing.T) { + // Given ... + runName := "garbage" + runId := "xxx123xxx" + + runResultStrings := []string{RUN_U123_RE_RUN} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Contains(t, err.Error(), "GAL1075") + assert.Contains(t, err.Error(), runName) +} + +func TestRunsResetWhereOperationFailedServerSideReturnsError(t *testing.T) { + // Given ... + runName := "U120" + runId := "xxx120xxx" + + runResultStrings := []string{RUN_U120} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Error(t, err) + assert.ErrorContains(t, err, "GAL1133") +} + +func TestRunsResetWhereServerSideResponseCannotBeParsedReturnsError(t *testing.T) { + // Given ... + runName := "U121" + runId := "xxx121xxx" + + runResultStrings := []string{RUN_U121} + + server := NewRunsResetServletMock(t, runName, runId, runResultStrings) + defer server.Close() + + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := ResetRun(runName, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Error(t, err) + assert.ErrorContains(t, err, "GAL1134") +} diff --git a/pkg/runs/runsTestObjects.go b/pkg/runs/runsTestObjects.go new file mode 100644 index 00000000..a8024c19 --- /dev/null +++ b/pkg/runs/runsTestObjects.go @@ -0,0 +1,143 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package runs + +// Dummy test run objects to use in unit testing. + +const ( + // An active run that should be finished now. + RUN_U123_FIRST_RUN = `{ + "runId": "xxx122xxx", + "artifacts": [], + "testStructure": { + "runName": "U123", + "bundle": "myBundleId", + "testName": "myTestPackage.MyTestName", + "testShortName": "MyTestName", + "requestor": "unitTesting", + "status" : "building", + "queued" : "2023-05-10T06:00:00.000000Z", + "startTime": "2023-05-10T06:00:10.000000Z", + "methods": [{ + "className": "myTestPackage.MyTestName", + "methodName": "myTestMethodName", + "type": "test", + "runLogStart":null, + "runLogEnd":null, + "befores":[], + "afters": [] + }] + } + }` + // An active run + RUN_U123_RE_RUN = `{ + "runId": "xxx123xxx", + "artifacts": [], + "testStructure": { + "runName": "U123", + "bundle": "myBundleId", + "testName": "myTestPackage.MyTestName", + "testShortName": "MyTestName", + "requestor": "unitTesting", + "status" : "building", + "queued" : "2023-05-10T06:00:00.000000Z", + "startTime": "2023-05-10T06:05:10.000000Z", + "methods": [{ + "className": "myTestPackage.MyTestName", + "methodName": "myTestMethodName", + "type": "test", + "runLogStart":null, + "runLogEnd":null, + "befores":[], + "afters": [] + }] + } + }` + // Another active run + RUN_U123_RE_RUN_2 = `{ + "runId": "xxx124xxx", + "artifacts": [], + "testStructure": { + "runName": "U123", + "bundle": "myBundleId", + "testName": "myTestPackage.MyTestName", + "testShortName": "MyTestName", + "requestor": "unitTesting", + "status" : "building", + "queued" : "2023-05-10T06:00:00.000000Z", + "startTime": "2023-05-10T10:10:10.000000Z", + "methods": [{ + "className": "myTestPackage.MyTestName", + "methodName": "myTestMethodName", + "type": "test", + "runLogStart":null, + "runLogEnd":null, + "befores":[], + "afters": [] + }] + } + }` + // A finished run + RUN_U120 = `{ + "runId": "xxx120xxx", + "artifacts": [], + "testStructure": { + "runName": "U120", + "bundle": "myBundleId", + "testName": "myTestPackage.MyTestName", + "testShortName": "MyTestName", + "requestor": "unitTesting", + "status" : "finished", + "result": "Passed", + "queued" : "2023-05-10T06:00:13.043037Z", + "startTime": "2023-05-10T06:00:36.159003Z", + "endTime": "2023-05-10T06:01:36.159003Z", + "methods": [{ + "className": "myTestPackage.MyTestName", + "methodName": "myTestMethodName", + "type": "test", + "status": "finished", + "result": "Passed", + "startTime": "2023-05-10T06:00:36.159003Z", + "endTime": "2023-05-10T06:01:36.159003Z", + "runLogStart":0, + "runLogEnd":0, + "befores":[], + "afters": [] + }] + } + }` + // Another finished run + RUN_U121 = `{ + "runId": "xxx121xxx", + "artifacts": [], + "testStructure": { + "runName": "U121", + "bundle": "myBundleId", + "testName": "myTestPackage.MyTestName", + "testShortName": "MyTestName", + "requestor": "unitTesting", + "status" : "finished", + "result": "Passed", + "queued" : "2023-05-10T06:00:13.043037Z", + "startTime": "2023-05-10T06:00:36.159003Z", + "endTime": "2023-05-10T06:01:36.159003Z", + "methods": [{ + "className": "myTestPackage.MyTestName", + "methodName": "myTestMethodName", + "type": "test", + "status": "finished", + "result": "Passed", + "startTime": "2023-05-10T06:00:36.159003Z", + "endTime": "2023-05-10T06:01:36.159003Z", + "runLogStart":0, + "runLogEnd":0, + "befores":[], + "afters": [] + }] + } + }` +) diff --git a/test-scripts/runs-tests.sh b/test-scripts/runs-tests.sh index 8facab03..6eb37257 100755 --- a/test-scripts/runs-tests.sh +++ b/test-scripts/runs-tests.sh @@ -303,6 +303,213 @@ function runs_download_check_folder_names_during_test_run { success "Downloading artifacts from a running test results in folder names with a timestamp. OK" } +function runs_reset_check_retry_present { + + h2 "Performing runs reset on an active test run..." + + run_name=$1 + + h2 "First, launching test on an ecosystem without a portfolio in a background process, so it can be reset." + + mkdir -p ${BASEDIR}/temp + cd ${BASEDIR}/temp + + runs_submit_log_file="runs-submit-output-for-reset.txt" + + cmd="${ORIGINAL_DIR}/bin/${binary} runs submit \ + --bootstrap $bootstrap \ + --class dev.galasa.inttests/dev.galasa.inttests.core.local.CoreLocalJava11Ubuntu \ + --stream inttests + --throttle 1 \ + --poll 10 \ + --progress 1 \ + --noexitcodeontestfailures \ + --log ${runs_submit_log_file}" + + info "Command is: $cmd" + + set -o pipefail # Fail everything if anything in the pipeline fails. Else we are just checking the 'tee' return code. + + # Start the test running inside a background process... so we can try to reset it while it's running + $cmd & + + run_name_found="false" + retries=0 + max=100 + target_line="" + + # Loop waiting until we can extract the name of the test run which is running in the background. + while [[ "${run_name_found}" == "false" ]]; do + if [[ -e $runs_submit_log_file ]]; then + success "file exists" + # Check the run has reached building stage before attempting to reset + target_line=$(cat ${runs_submit_log_file} | grep "status is now 'building'") + + if [[ "$target_line" != "" ]]; then + info "Target line is found - the test is now building." + run_name_found="true" + fi + fi + sleep 3 + ((retries++)) + if (( $retries > $max )); then + error "Too many retries." + exit 1 + fi + done + + run_name=$(echo $target_line | cut -f4 -d' ') + info "Run name is $run_name" + + h2 "Now attempting to reset the run while it's running in the background process." + + cmd="${ORIGINAL_DIR}/bin/${binary} runs reset \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + info "Command is: $cmd" + $cmd + + h2 "Now using runs get to check when the run is finished." + + runs_get_log_file="runs-get-output-for-reset.txt" + + # Now poll runs get to check when the test is finished + cmd="${ORIGINAL_DIR}/bin/${binary} runs get \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + is_test_finished="false" + retries=0 + max=100 + target_line="" + while [[ "${is_test_finished}" == "false" ]]; do + sleep 5 + + # Run the runs get command + $cmd | tee $runs_get_log_file + # Check for line in the runs get output to signify the test is finished + target_line=$(cat ${runs_get_log_file} | grep "finished") + if [[ "$target_line" != "" ]]; then + success "Target line is found - the test is finished." + is_test_finished="true" + fi + + # Give up if we've been waiting for the test to finish for too long. Test could be stuck. + ((retries++)) + if (( $retries > $max )); then + error "Too many retries." + exit 1 + fi + done + + h2 "Now checking if two results for the runName are shown - the original run and the reset run." + + # Now check if the runs get shows two runs with a retry. + target_line=$(cat ${runs_get_log_file} | grep "Total:2") + if [[ "$target_line" != "" ]]; then + success "Target line found - the original and reset run were found." + fi + +} + +function runs_cancel_check_test_is_lost { + + h2 "Performing runs cancel on an active test run..." + + run_name=$1 + + h2 "First, launching test on an ecosystem without a portfolio in a background process, so it can be cancelled." + + mkdir -p ${BASEDIR}/temp + cd ${BASEDIR}/temp + + runs_submit_log_file="runs-submit-output-for-cancel.txt" + + cmd="${ORIGINAL_DIR}/bin/${binary} runs submit \ + --bootstrap $bootstrap \ + --class dev.galasa.inttests/dev.galasa.inttests.core.local.CoreLocalJava11Ubuntu \ + --stream inttests + --throttle 1 \ + --poll 10 \ + --progress 1 \ + --noexitcodeontestfailures \ + --log ${runs_submit_log_file}" + + info "Command is: $cmd" + + set -o pipefail # Fail everything if anything in the pipeline fails. Else we are just checking the 'tee' return code. + + # Start the test running inside a background process... so we can try to cancel it while it's running + $cmd & + + run_name_found="false" + retries=0 + max=100 + target_line="" + + # Loop waiting until we can extract the name of the test run which is running in the background. + while [[ "${run_name_found}" == "false" ]]; do + if [[ -e $runs_submit_log_file ]]; then + success "file exists" + # Check the run has reached building stage before attempting to cancel + target_line=$(cat ${runs_submit_log_file} | grep "status is now 'building'") + + if [[ "$target_line" != "" ]]; then + info "Target line is found - the test is now building." + run_name_found="true" + fi + fi + sleep 3 + ((retries++)) + if (( $retries > $max )); then + error "Too many retries." + exit 1 + fi + done + + run_name=$(echo $target_line | cut -f4 -d' ') + info "Run name is $run_name" + + h2 "Now attempting to cancel the run while it's running in the background process." + + cmd="${ORIGINAL_DIR}/bin/${binary} runs cancel \ + --name ${run_name} \ + --bootstrap ${bootstrap}" + + info "Command is: $cmd" + + $cmd + + h2 "Now using the runs submit output to check the run was cancelled." + + is_test_cancelled="false" + retries=0 + max=100 + target_line="" + while [[ "${is_test_cancelled}" == "false" ]]; do + sleep 5 + + if [[ -e $runs_submit_log_file ]]; then + success "file exists" + target_line=$(cat ${runs_submit_log_file} | grep "was lost") + + if [[ "$target_line" != "" ]]; then + info "Target line is found - the test was cancelled." + is_test_cancelled="true" + fi + fi + + # Give up if we've been waiting for the test to show as cancelled for too long. + ((retries++)) + if (( $retries > $max )); then + error "Too many retries." + exit 1 + fi + done + +} + #-------------------------------------------------------------------------- function get_result_with_runname { h2 "Querying the result of the test we just ran..." @@ -796,6 +1003,12 @@ function test_runs_commands { launch_test_from_unknown_portfolio runs_download_check_folder_names_during_test_run + + # Attempt to reset an active run... + runs_reset_check_retry_present + + # Attempt to cancel an active run... + runs_cancel_check_test_is_lost }