From 204d9dc36943b4b80d628c4b906e791cdd594f6a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 30 May 2024 12:43:07 -0700 Subject: [PATCH 01/27] [add] new test folder for the test ui rewrite and refactor the codeblock state to be more generic. --- internal/engine/codeblock.go | 16 ++ internal/engine/commands.go | 15 +- internal/engine/engine.go | 2 +- internal/engine/interactive.go | 20 +-- internal/engine/test/components.go | 26 +++ internal/engine/test/input.go | 21 +++ internal/engine/test/model.go | 244 +++++++++++++++++++++++++++++ 7 files changed, 320 insertions(+), 24 deletions(-) create mode 100644 internal/engine/codeblock.go create mode 100644 internal/engine/test/components.go create mode 100644 internal/engine/test/input.go create mode 100644 internal/engine/test/model.go diff --git a/internal/engine/codeblock.go b/internal/engine/codeblock.go new file mode 100644 index 00000000..f726dde7 --- /dev/null +++ b/internal/engine/codeblock.go @@ -0,0 +1,16 @@ +package engine + +import "github.com/Azure/InnovationEngine/internal/parsers" + +// State for the codeblock in interactive mode. Used to keep track of the +// state of each codeblock. +type StatefulCodeBlock struct { + CodeBlock parsers.CodeBlock + CodeBlockNumber int + Error error + StdErr string + StdOut string + StepName string + StepNumber int + Success bool +} diff --git a/internal/engine/commands.go b/internal/engine/commands.go index 52db6ab9..38b0a1c7 100644 --- a/internal/engine/commands.go +++ b/internal/engine/commands.go @@ -88,12 +88,15 @@ func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) te logging.GlobalLogger.Info("Executing command synchronously: ", codeBlock.Content) program.ReleaseTerminal() - output, err := shells.ExecuteBashCommand(codeBlock.Content, shells.BashCommandConfiguration{ - EnvironmentVariables: env, - InheritEnvironment: true, - InteractiveCommand: true, - WriteToHistory: true, - }) + output, err := shells.ExecuteBashCommand( + codeBlock.Content, + shells.BashCommandConfiguration{ + EnvironmentVariables: env, + InheritEnvironment: true, + InteractiveCommand: true, + WriteToHistory: true, + }, + ) program.RestoreTerminal() diff --git a/internal/engine/engine.go b/internal/engine/engine.go index caf71753..a5da2706 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -54,7 +54,7 @@ func (e *Engine) TestScenario(scenario *Scenario) error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) // Test the steps - fmt.Println(ui.ScenarioTitleStyle.Render(scenario.Name)) + fmt.Println(ui.ScenarioTitleStyle.Render("Now testing " + scenario.Name)) err := e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) return err }) diff --git a/internal/engine/interactive.go b/internal/engine/interactive.go index 156615d1..2756f7fe 100644 --- a/internal/engine/interactive.go +++ b/internal/engine/interactive.go @@ -9,7 +9,6 @@ import ( "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/patterns" "github.com/Azure/InnovationEngine/internal/ui" "github.com/charmbracelet/bubbles/help" @@ -28,19 +27,6 @@ type InteractiveModeCommands struct { next key.Binding } -// State for the codeblock in interactive mode. Used to keep track of the -// state of each codeblock. -type CodeBlockState struct { - CodeBlock parsers.CodeBlock - CodeBlockNumber int - Error error - StdErr string - StdOut string - StepName string - StepNumber int - Success bool -} - type interactiveModeComponents struct { paginator paginator.Model stepViewport viewport.Model @@ -50,7 +36,7 @@ type interactiveModeComponents struct { type InteractiveModeModel struct { azureStatus environments.AzureDeploymentStatus - codeBlockState map[int]CodeBlockState + codeBlockState map[int]StatefulCodeBlock commands InteractiveModeCommands currentCodeBlock int env map[string]string @@ -508,7 +494,7 @@ func NewInteractiveModeModel( azureStatus := environments.NewAzureDeploymentStatus() azureStatus.CurrentStep = 1 totalCodeBlocks := 0 - codeBlockState := make(map[int]CodeBlockState) + codeBlockState := make(map[int]StatefulCodeBlock) err := az.SetSubscription(engine.Configuration.Subscription) if err != nil { @@ -526,7 +512,7 @@ func NewInteractiveModeModel( Description: block.Description, }) - codeBlockState[totalCodeBlocks] = CodeBlockState{ + codeBlockState[totalCodeBlocks] = StatefulCodeBlock{ StepName: step.Name, CodeBlock: block, StepNumber: stepNumber, diff --git a/internal/engine/test/components.go b/internal/engine/test/components.go new file mode 100644 index 00000000..78b02a92 --- /dev/null +++ b/internal/engine/test/components.go @@ -0,0 +1,26 @@ +package test + +import "github.com/charmbracelet/bubbles/viewport" + +// Components used for test mode. +type testModeComponents struct { + commandViewport viewport.Model +} + +// Initializes the viewports for the interactive mode model. +func initializeComponents(model TestModeModel, width, height int) testModeComponents { + commandViewport := viewport.New(width, height) + + components := testModeComponents{ + commandViewport: commandViewport, + } + + components.updateViewportSizing(width, height) + return components +} + +// Update the viewport height for the test mode components. +func (components *testModeComponents) updateViewportSizing(terminalWidth int, terminalHeight int) { + components.commandViewport.Width = terminalWidth + components.commandViewport.Height = terminalHeight - 1 +} diff --git a/internal/engine/test/input.go b/internal/engine/test/input.go new file mode 100644 index 00000000..1feff30b --- /dev/null +++ b/internal/engine/test/input.go @@ -0,0 +1,21 @@ +package test + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// Handle user input for Test mode. +func handleUserInput( + model TestModeModel, + message tea.KeyMsg, +) (TestModeModel, []tea.Cmd) { + var commands []tea.Cmd + + switch { + case key.Matches(message, model.commands.quit): + commands = append(commands, tea.Quit) + } + + return model, commands +} diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go new file mode 100644 index 00000000..cd0e13ec --- /dev/null +++ b/internal/engine/test/model.go @@ -0,0 +1,244 @@ +package test + +import ( + "fmt" + "strings" + + "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/environments" + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/patterns" + "github.com/Azure/InnovationEngine/internal/ui" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// Commands accessible to the user for test mode. +type TestModeCommands struct { + quit key.Binding +} + +// The state required for testing scenarios. +type TestModeModel struct { + azureStatus environments.AzureDeploymentStatus + codeBlockState map[int]engine.StatefulCodeBlock + commands TestModeCommands + currentCodeBlock int + env map[string]string + environment string + executingCommand bool + height int + help help.Model + resourceGroupName string + scenarioTitle string + width int + scenarioCompleted bool + components testModeComponents + ready bool + commandLines []string +} + +// Init the test mode model. +func (model TestModeModel) Init() tea.Cmd { + return nil +} + +// Update the test mode model. +func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { + var commands []tea.Cmd + + switch message := message.(type) { + + case tea.WindowSizeMsg: + model.width = message.Width + model.height = message.Height + logging.GlobalLogger.Debugf("Window size changed to: %d x %d", message.Width, message.Height) + if !model.ready { + model.components = initializeComponents(model, message.Width, message.Height) + model.ready = true + } else { + model.components.updateViewportSizing(message.Height) + } + + case tea.KeyMsg: + model, commands = handleUserInput(model, message) + + case engine.SuccessfulCommandMessage: + // Handle successful command executions + model.executingCommand = false + step := model.currentCodeBlock + + // Update the state of the codeblock which finished executing. + codeBlockState := model.codeBlockState[step] + codeBlockState.StdOut = message.StdOut + codeBlockState.StdErr = message.StdErr + codeBlockState.Success = true + model.codeBlockState[step] = codeBlockState + + logging.GlobalLogger.Infof("Finished executing:\n %s", codeBlockState.CodeBlock.Content) + + // Extract the resource group name from the command output if + // it's not already set. + if model.resourceGroupName == "" && patterns.AzCommand.MatchString(codeBlockState.CodeBlock.Content) { + logging.GlobalLogger.Debugf("Attempting to extract resource group name from command output") + tmpResourceGroup := az.FindResourceGroupName(codeBlockState.StdOut) + if tmpResourceGroup != "" { + logging.GlobalLogger.Infof("Found resource group named: %s", tmpResourceGroup) + model.resourceGroupName = tmpResourceGroup + } + } + model.commandLines = append(model.commandLines, codeBlockState.StdOut) + + // Increment the codeblock and update the viewport content. + model.currentCodeBlock++ + + if model.currentCodeBlock < len(model.codeBlockState) { + nextCommand := model.codeBlockState[model.currentCodeBlock].CodeBlock.Content + nextLanguage := model.codeBlockState[model.currentCodeBlock].CodeBlock.Language + + model.commandLines = append(model.commandLines, ui.CommandPrompt(nextLanguage)+nextCommand) + } + + // Only increment the step for azure if the step name has changed. + nextCodeBlockState := model.codeBlockState[model.currentCodeBlock] + + if codeBlockState.StepName != nextCodeBlockState.StepName { + logging.GlobalLogger.Debugf("Step name has changed, incrementing step for Azure") + model.azureStatus.CurrentStep++ + } else { + logging.GlobalLogger.Debugf("Step name has not changed, not incrementing step for Azure") + } + + // If the scenario has been completed, we need to update the azure + // status and quit the program. else, + if model.currentCodeBlock == len(model.codeBlockState) { + model.scenarioCompleted = true + model.azureStatus.Status = "Succeeded" + commands = append( + commands, + tea.Quit, + ) + } else { + // If the scenario has not been completed, we need to execute the next command + commands = append( + commands, + engine.ExecuteCodeBlockAsync(nextCodeBlockState.CodeBlock, model.env), + ) + } + + case engine.FailedCommandMessage: + // Handle failed command executions + + // Update the state of the codeblock which finished executing. + step := model.currentCodeBlock + codeBlockState := model.codeBlockState[step] + codeBlockState.StdOut = message.StdOut + codeBlockState.StdErr = message.StdErr + codeBlockState.Success = false + + model.codeBlockState[step] = codeBlockState + model.commandLines = append(model.commandLines, codeBlockState.StdErr) + + // Report the error + model.executingCommand = false + model.azureStatus.SetError(message.Error) + environments.AttachResourceURIsToAzureStatus( + &model.azureStatus, + model.resourceGroupName, + model.environment, + ) + commands = append(commands, tea.Quit) + } + + model.components.commandViewport.SetContent(strings.Join(model.commandLines, "\n")) + + // Update all the viewports and append resulting commands. + var command tea.Cmd + + model.components.commandViewport, command = model.components.commandViewport.Update(message) + commands = append(commands, command) + + return model, tea.Batch(commands...) +} + +// View the test mode model. +func (model TestModeModel) View() string { + return model.components.commandViewport.View() +} + +// Create a new test mode model. +func NewTestModeModel( + title string, + engine *engine.Engine, + steps []engine.Step, + env map[string]string, +) (TestModeModel, error) { + // TODO: In the future we should just set the current step for the azure status + // to one as the default. + azureStatus := environments.NewAzureDeploymentStatus() + azureStatus.CurrentStep = 1 + totalCodeBlocks := 0 + codeBlockState := make(map[int]engine.StatefulCodeBlock) + + err := az.SetSubscription(engine.Configuration.Subscription) + if err != nil { + logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) + azureStatus.SetError(err) + environments.ReportAzureStatus(azureStatus, engine.Configuration.Environment) + return TestModeModel{}, err + } + + // TODO(vmarcella): The codeblock state building should be reused across + // Interactive mode and test mode in the future. + for stepNumber, step := range steps { + azureCodeBlocks := []environments.AzureCodeBlock{} + for blockNumber, block := range step.CodeBlocks { + azureCodeBlocks = append(azureCodeBlocks, environments.AzureCodeBlock{ + Command: block.Content, + Description: block.Description, + }) + + codeBlockState[totalCodeBlocks] = engine.StatefulCodeBlock{ + StepName: step.Name, + CodeBlock: block, + StepNumber: stepNumber, + CodeBlockNumber: blockNumber, + StdOut: "", + StdErr: "", + Error: nil, + Success: false, + } + + totalCodeBlocks += 1 + } + azureStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name), azureCodeBlocks) + } + + language := codeBlockState[0].CodeBlock.Language + commandLines := []string{ + ui.CommandPrompt(language) + codeBlockState[0].CodeBlock.Content, + } + + return TestModeModel{ + scenarioTitle: title, + commands: TestModeCommands{ + quit: key.NewBinding( + key.WithKeys("q"), + key.WithHelp("q", "Quit the scenario."), + ), + }, + env: env, + resourceGroupName: "", + azureStatus: azureStatus, + codeBlockState: codeBlockState, + executingCommand: false, + currentCodeBlock: 0, + help: help.New(), + environment: engine.Configuration.Environment, + scenarioCompleted: false, + ready: false, + commandLines: commandLines, + }, nil +} From 92dbf0661de6765d13abfc7f62d3093578c70ec8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Jun 2024 12:31:50 -0700 Subject: [PATCH 02/27] [refactor] all of the engine components and modes into separate folders in preparation to unify all ui implementations. --- cmd/ie/commands/execute.go | 4 +- cmd/ie/commands/inspect.go | 5 +- cmd/ie/commands/interactive.go | 4 +- cmd/ie/commands/test.go | 5 +- cmd/ie/commands/to-bash.go | 7 +- internal/engine/{ => common}/codeblock.go | 2 +- internal/engine/{ => common}/commands.go | 16 ++--- internal/engine/common/globals.go | 9 +++ .../engine/{common.go => common/outputs.go} | 4 +- internal/engine/{ => common}/scenario.go | 4 +- internal/engine/{ => common}/scenario_test.go | 2 +- internal/engine/engine.go | 51 +++++++++++---- internal/engine/environments/metadata.go | 50 +++++++++++++++ internal/engine/execution.go | 15 ++--- .../engine/{ => interactive}/interactive.go | 64 ++++++++++--------- internal/engine/test/model.go | 27 ++++---- internal/engine/testing.go | 5 +- 17 files changed, 179 insertions(+), 95 deletions(-) rename internal/engine/{ => common}/codeblock.go (96%) rename internal/engine/{ => common}/commands.go (91%) create mode 100644 internal/engine/common/globals.go rename internal/engine/{common.go => common/outputs.go} (97%) rename internal/engine/{ => common}/scenario.go (99%) rename internal/engine/{ => common}/scenario_test.go (99%) create mode 100644 internal/engine/environments/metadata.go rename internal/engine/{ => interactive}/interactive.go (91%) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index d9890c9b..8f74ada4 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -92,7 +93,7 @@ var executeCommand = &cobra.Command{ } // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown( + scenario, err := common.CreateScenarioFromMarkdown( markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables, @@ -112,7 +113,6 @@ var executeCommand = &cobra.Command{ WorkingDirectory: workingDirectory, RenderValues: renderValues, }) - if err != nil { logging.GlobalLogger.Errorf("Error creating engine: %s", err) fmt.Printf("Error creating engine: %s", err) diff --git a/cmd/ie/commands/inspect.go b/cmd/ie/commands/inspect.go index 05467ca9..1682e2b6 100644 --- a/cmd/ie/commands/inspect.go +++ b/cmd/ie/commands/inspect.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/ui" "github.com/spf13/cobra" @@ -59,7 +59,7 @@ var inspectCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown( + scenario, err := common.CreateScenarioFromMarkdown( markdownFile, []string{"bash", "azurecli", "azurecli-inspect", "terraform"}, cliEnvironmentVariables, @@ -104,6 +104,5 @@ var inspectCommand = &cobra.Command{ fmt.Println() } } - }, } diff --git a/cmd/ie/commands/interactive.go b/cmd/ie/commands/interactive.go index 8d6c8904..8b6e0b97 100644 --- a/cmd/ie/commands/interactive.go +++ b/cmd/ie/commands/interactive.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -69,7 +70,7 @@ var interactiveCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown( + scenario, err := common.CreateScenarioFromMarkdown( markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables, @@ -89,7 +90,6 @@ var interactiveCommand = &cobra.Command{ WorkingDirectory: workingDirectory, RenderValues: renderValues, }) - if err != nil { logging.GlobalLogger.Errorf("Error creating engine: %s", err) fmt.Printf("Error creating engine: %s", err) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 35259bb4..71827fb8 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -5,6 +5,7 @@ import ( "os" "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -25,7 +26,6 @@ var testCommand = &cobra.Command{ Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", Run: func(cmd *cobra.Command, args []string) { - markdownFile := args[0] if markdownFile == "" { cmd.Help() @@ -43,14 +43,13 @@ var testCommand = &cobra.Command{ CorrelationId: "", WorkingDirectory: workingDirectory, }) - if err != nil { logging.GlobalLogger.Errorf("Error creating engine %s", err) fmt.Printf("Error creating engine %s", err) os.Exit(1) } - scenario, err := engine.CreateScenarioFromMarkdown( + scenario, err := common.CreateScenarioFromMarkdown( markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, nil, diff --git a/cmd/ie/commands/to-bash.go b/cmd/ie/commands/to-bash.go index 248e1427..fb675d72 100644 --- a/cmd/ie/commands/to-bash.go +++ b/cmd/ie/commands/to-bash.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" @@ -50,11 +50,10 @@ var toBashCommand = &cobra.Command{ } // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown( + scenario, err := common.CreateScenarioFromMarkdown( markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) - if err != nil { logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) @@ -66,7 +65,6 @@ var toBashCommand = &cobra.Command{ if environments.IsAzureEnvironment(environment) { script := AzureScript{Script: scenario.ToShellScript()} scriptJson, err := json.Marshal(script) - if err != nil { logging.GlobalLogger.Errorf("Error converting to json: %s", err) fmt.Printf("Error converting to json: %s", err) @@ -79,7 +77,6 @@ var toBashCommand = &cobra.Command{ } return nil - }, } diff --git a/internal/engine/codeblock.go b/internal/engine/common/codeblock.go similarity index 96% rename from internal/engine/codeblock.go rename to internal/engine/common/codeblock.go index f726dde7..8f6a0226 100644 --- a/internal/engine/codeblock.go +++ b/internal/engine/common/codeblock.go @@ -1,4 +1,4 @@ -package engine +package common import "github.com/Azure/InnovationEngine/internal/parsers" diff --git a/internal/engine/commands.go b/internal/engine/common/commands.go similarity index 91% rename from internal/engine/commands.go rename to internal/engine/common/commands.go index 38b0a1c7..f74a6fdd 100644 --- a/internal/engine/commands.go +++ b/internal/engine/common/commands.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" @@ -52,7 +52,7 @@ func ExecuteCodeBlockAsync(codeBlock parsers.CodeBlock, env map[string]string) t expectedRegex := codeBlock.ExpectedOutput.ExpectedRegex expectedOutputLanguage := codeBlock.ExpectedOutput.Language - outputComparisonError := compareCommandOutputs( + outputComparisonError := CompareCommandOutputs( actualOutput, expectedOutput, expectedSimilarity, @@ -86,7 +86,7 @@ func ExecuteCodeBlockAsync(codeBlock parsers.CodeBlock, env map[string]string) t // finishes executing. func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) tea.Msg { logging.GlobalLogger.Info("Executing command synchronously: ", codeBlock.Content) - program.ReleaseTerminal() + Program.ReleaseTerminal() output, err := shells.ExecuteBashCommand( codeBlock.Content, @@ -98,7 +98,7 @@ func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) te }, ) - program.RestoreTerminal() + Program.RestoreTerminal() if err != nil { return FailedCommandMessage{ @@ -116,7 +116,7 @@ func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) te } // clearScreen returns a command that clears the terminal screen and positions the cursor at the top-left corner -func clearScreen() tea.Cmd { +func ClearScreen() tea.Cmd { return func() tea.Msg { fmt.Print( "\033[H\033[2J", @@ -127,13 +127,13 @@ func clearScreen() tea.Cmd { // Updates the azure status with the current state of the interactive mode // model. -func updateAzureStatus(model InteractiveModeModel) tea.Cmd { +func UpdateAzureStatus(azureStatus environments.AzureDeploymentStatus, environment string) tea.Cmd { return func() tea.Msg { logging.GlobalLogger.Tracef( "Attempting to update the azure status: %+v", - model.azureStatus, + azureStatus, ) - environments.ReportAzureStatus(model.azureStatus, model.environment) + environments.ReportAzureStatus(azureStatus, environment) return AzureStatusUpdatedMessage{} } } diff --git a/internal/engine/common/globals.go b/internal/engine/common/globals.go new file mode 100644 index 00000000..3b512262 --- /dev/null +++ b/internal/engine/common/globals.go @@ -0,0 +1,9 @@ +package common + +import tea "github.com/charmbracelet/bubbletea" + +// TODO: Ideally we won't need a global program variable. We should +// refactor this in the future such that each tea program is localized to the +// function that creates it and ExecuteCodeBlockSync doesn't mutate the global +// program variable. +var Program *tea.Program = nil diff --git a/internal/engine/common.go b/internal/engine/common/outputs.go similarity index 97% rename from internal/engine/common.go rename to internal/engine/common/outputs.go index b072c44b..4543227c 100644 --- a/internal/engine/common.go +++ b/internal/engine/common/outputs.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" @@ -12,7 +12,7 @@ import ( ) // Compares the actual output of a command to the expected output of a command. -func compareCommandOutputs( +func CompareCommandOutputs( actualOutput string, expectedOutput string, expectedSimilarity float64, diff --git a/internal/engine/scenario.go b/internal/engine/common/scenario.go similarity index 99% rename from internal/engine/scenario.go rename to internal/engine/common/scenario.go index cd05e49f..33a98b41 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/common/scenario.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" @@ -68,7 +68,7 @@ func downloadScenarioMarkdown(url string) ([]byte, error) { return body, nil } -// Given either a local or remote path to a markdown file, resolve the path to +// Given either a local or remote path to a markdown file, resolve the path to // the markdown file and return the contents of the file. func resolveMarkdownSource(path string) ([]byte, error) { if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") { diff --git a/internal/engine/scenario_test.go b/internal/engine/common/scenario_test.go similarity index 99% rename from internal/engine/scenario_test.go rename to internal/engine/common/scenario_test.go index 510b240a..f99f09ee 100644 --- a/internal/engine/scenario_test.go +++ b/internal/engine/common/scenario_test.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" diff --git a/internal/engine/engine.go b/internal/engine/engine.go index a5da2706..e51b40d2 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -5,7 +5,10 @@ import ( "strings" "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/engine/environments" + "github.com/Azure/InnovationEngine/internal/engine/interactive" + "github.com/Azure/InnovationEngine/internal/engine/test" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/lib/fs" "github.com/Azure/InnovationEngine/internal/logging" @@ -37,7 +40,7 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { } // Executes a deployment scenario. -func (e *Engine) ExecuteScenario(scenario *Scenario) error { +func (e *Engine) ExecuteScenario(scenario *common.Scenario) error { return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) @@ -49,28 +52,52 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { } // Validates a deployment scenario. -func (e *Engine) TestScenario(scenario *Scenario) error { +func (e *Engine) TestScenario(scenario *common.Scenario) error { return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) + stepsToExecute := filterDeletionCommands(scenario.Steps, e.Configuration.DoNotDelete) + + model, err := test.NewTestModeModel( + scenario.Name, + e.Configuration.Subscription, + e.Configuration.Environment, + stepsToExecute, + lib.CopyMap(scenario.Environment), + ) + if err != nil { + return err + } + + common.Program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) + + var finalModel tea.Model + finalModel, err = common.Program.Run() + var ok bool + + // TODO(vmarcella): After testing is complete, we should generate a report. + + model, ok = finalModel.(test.TestModeModel) + + if !ok { + return fmt.Errorf("failed to cast tea.Model to TestModeModel") + } - // Test the steps - fmt.Println(ui.ScenarioTitleStyle.Render("Now testing " + scenario.Name)) - err := e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) return err }) } // Executes a Scenario in interactive mode. This mode goes over each codeblock // step by step and allows the user to interact with the codeblock. -func (e *Engine) InteractWithScenario(scenario *Scenario) error { +func (e *Engine) InteractWithScenario(scenario *common.Scenario) error { return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) stepsToExecute := filterDeletionCommands(scenario.Steps, e.Configuration.DoNotDelete) - model, err := NewInteractiveModeModel( + model, err := interactive.NewInteractiveModeModel( scenario.Name, - e, + e.Configuration.Subscription, + e.Configuration.Environment, stepsToExecute, lib.CopyMap(scenario.Environment), ) @@ -78,13 +105,13 @@ func (e *Engine) InteractWithScenario(scenario *Scenario) error { return err } - program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) + common.Program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) var finalModel tea.Model var ok bool - finalModel, err = program.Run() + finalModel, err = common.Program.Run() - model, ok = finalModel.(InteractiveModeModel) + model, ok = finalModel.(interactive.InteractiveModeModel) if environments.EnvironmentsAzure == e.Configuration.Environment { if !ok { @@ -92,7 +119,7 @@ func (e *Engine) InteractWithScenario(scenario *Scenario) error { } logging.GlobalLogger.Info("Writing session output to stdout") - fmt.Println(strings.Join(model.commandLines, "\n")) + fmt.Println(strings.Join(model.CommandLines, "\n")) } switch e.Configuration.Environment { diff --git a/internal/engine/environments/metadata.go b/internal/engine/environments/metadata.go new file mode 100644 index 00000000..fdead660 --- /dev/null +++ b/internal/engine/environments/metadata.go @@ -0,0 +1,50 @@ +package environments + +type ScenarioConfigurations struct { + Permissions []string `json:"permissions"` + // These are not being picked up yet but would contain variables that are + // found within the document and can be configured. + Variables []string `json:"variables"` +} + +type ScenarioMetadata struct { + Key string `json:"key"` + Title string `json:"title"` + Description string `json:"description"` + ExtraDetails string `json:"extraDetails"` + BulletPoints []string `json:"bulletPoints"` + SourceURL string `json:"sourceURL"` + DocumentationURL string `json:"documentationURL"` + Configurations ScenarioConfigurations `json:"configurations"` +} + +type LocalizedScenarioMetadata struct { + Key string `json:"key"` + IsActive bool `json:"isActive"` + Locales map[string]ScenarioMetadata `json:"locales"` +} + +type ScenarioMetadataCollection []LocalizedScenarioMetadata + +// Resulting structure looks like: +// [ +// "key": "scenario-key", +// "isActive": true, +// "locales: { +// "en": { +// "key": "scenario-key", +// "title": "Scenario Title", +// "description": "Scenario Description", +// "extraDetails": "Extra Details", +// "bulletPoints": ["Bullet Point 1", "Bullet Point 2"], +// "sourceURL": "https://source.url", +// "documentationURL": "https://documentation.url", +// "configurations": { +// "permissions": ["permission1", "permission2"], +// "variables": ["variable1", "variable2"] +// } +// } +// } +// } +// ] +// diff --git a/internal/engine/execution.go b/internal/engine/execution.go index f7382eba..f98b0234 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -6,6 +6,7 @@ import ( "time" "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" @@ -25,8 +26,8 @@ const ( // If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. -func filterDeletionCommands(steps []Step, preserveResources bool) []Step { - filteredSteps := []Step{} +func filterDeletionCommands(steps []common.Step, preserveResources bool) []common.Step { + filteredSteps := []common.Step{} if preserveResources { for _, step := range steps { newBlocks := []parsers.CodeBlock{} @@ -38,7 +39,7 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { } } if len(newBlocks) > -1 { - filteredSteps = append(filteredSteps, Step{ + filteredSteps = append(filteredSteps, common.Step{ Name: step.Name, CodeBlocks: newBlocks, }) @@ -68,10 +69,9 @@ func renderCommand(blockContent string) (shells.CommandOutput, error) { } // Executes the steps from a scenario and renders the output to the terminal. -func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) error { - +func (e *Engine) ExecuteAndRenderSteps(steps []common.Step, env map[string]string) error { var resourceGroupName string = "" - var azureStatus = environments.NewAzureDeploymentStatus() + azureStatus := environments.NewAzureDeploymentStatus() err := az.SetSubscription(e.Configuration.Subscription) if err != nil { @@ -183,7 +183,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro expectedRegex := block.ExpectedOutput.ExpectedRegex expectedOutputLanguage := block.ExpectedOutput.Language - outputComparisonError := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedRegex, expectedOutputLanguage) + outputComparisonError := common.CompareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedRegex, expectedOutputLanguage) if outputComparisonError != nil { logging.GlobalLogger.Errorf("Error comparing command outputs: %s", outputComparisonError.Error()) @@ -310,7 +310,6 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro "Cleaning environment variable file located at /tmp/env-vars", ) err := shells.CleanEnvironmentStateFile() - if err != nil { logging.GlobalLogger.Errorf("Error cleaning environment variables: %s", err.Error()) return err diff --git a/internal/engine/interactive.go b/internal/engine/interactive/interactive.go similarity index 91% rename from internal/engine/interactive.go rename to internal/engine/interactive/interactive.go index 2756f7fe..96639d09 100644 --- a/internal/engine/interactive.go +++ b/internal/engine/interactive/interactive.go @@ -1,4 +1,4 @@ -package engine +package interactive import ( "fmt" @@ -6,6 +6,7 @@ import ( "time" "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" @@ -36,7 +37,7 @@ type interactiveModeComponents struct { type InteractiveModeModel struct { azureStatus environments.AzureDeploymentStatus - codeBlockState map[int]StatefulCodeBlock + codeBlockState map[int]common.StatefulCodeBlock commands InteractiveModeCommands currentCodeBlock int env map[string]string @@ -50,13 +51,13 @@ type InteractiveModeModel struct { scenarioCompleted bool components interactiveModeComponents ready bool - commandLines []string + CommandLines []string } // Initialize the intractive mode model func (model InteractiveModeModel) Init() tea.Cmd { environments.ReportAzureStatus(model.azureStatus, model.environment) - return tea.Batch(clearScreen(), tea.Tick(time.Millisecond*10, func(t time.Time) tea.Msg { + return tea.Batch(common.ClearScreen(), tea.Tick(time.Millisecond*10, func(t time.Time) tea.Msg { return tea.KeyMsg{Type: tea.KeyCtrlL} // This is to force a repaint })) } @@ -146,13 +147,13 @@ func handleUserInput( ) commands = append(commands, tea.Sequence( - updateAzureStatus(model), + common.UpdateAzureStatus(model.azureStatus, model.environment), func() tea.Msg { - return ExecuteCodeBlockSync(codeBlock, lib.CopyMap(model.env)) + return common.ExecuteCodeBlockSync(codeBlock, lib.CopyMap(model.env)) })) } else { - commands = append(commands, ExecuteCodeBlockAsync( + commands = append(commands, common.ExecuteCodeBlockAsync( codeBlock, lib.CopyMap(model.env), )) @@ -224,7 +225,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: model, commands = handleUserInput(model, message) - case SuccessfulCommandMessage: + case common.SuccessfulCommandMessage: // Handle successful command executions model.executingCommand = false step := model.currentCodeBlock @@ -248,7 +249,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.resourceGroupName = tmpResourceGroup } } - model.commandLines = append(model.commandLines, codeBlockState.StdOut) + model.CommandLines = append(model.CommandLines, codeBlockState.StdOut) // Increment the codeblock and update the viewport content. model.currentCodeBlock++ @@ -257,7 +258,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { nextCommand := model.codeBlockState[model.currentCodeBlock].CodeBlock.Content nextLanguage := model.codeBlockState[model.currentCodeBlock].CodeBlock.Language - model.commandLines = append(model.commandLines, ui.CommandPrompt(nextLanguage)+nextCommand) + model.CommandLines = append(model.CommandLines, ui.CommandPrompt(nextLanguage)+nextCommand) } // Only increment the step for azure if the step name has changed. @@ -283,15 +284,15 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { commands = append( commands, tea.Sequence( - updateAzureStatus(model), + common.UpdateAzureStatus(model.azureStatus, model.environment), tea.Quit, ), ) } else { - commands = append(commands, updateAzureStatus(model)) + commands = append(commands, common.UpdateAzureStatus(model.azureStatus, model.environment)) } - case FailedCommandMessage: + case common.FailedCommandMessage: // Handle failed command executions // Update the state of the codeblock which finished executing. @@ -302,7 +303,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { codeBlockState.Success = false model.codeBlockState[step] = codeBlockState - model.commandLines = append(model.commandLines, codeBlockState.StdErr) + model.CommandLines = append(model.CommandLines, codeBlockState.StdErr) // Report the error model.executingCommand = false @@ -312,9 +313,15 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.resourceGroupName, model.environment, ) - commands = append(commands, tea.Sequence(updateAzureStatus(model), tea.Quit)) + commands = append( + commands, + tea.Sequence( + common.UpdateAzureStatus(model.azureStatus, model.environment), + tea.Quit, + ), + ) - case AzureStatusUpdatedMessage: + case common.AzureStatusUpdatedMessage: // After the status has been updated, we force a window resize to // render over the status update. For some reason, clearing the screen // manually seems to cause the text produced by View() to not render @@ -383,7 +390,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.components.outputViewport.SetContent(block.StdErr) } - model.components.azureCLIViewport.SetContent(strings.Join(model.commandLines, "\n")) + model.components.azureCLIViewport.SetContent(strings.Join(model.CommandLines, "\n")) // Update all the viewports and append resulting commands. var command tea.Cmd @@ -476,17 +483,12 @@ func (model InteractiveModeModel) View() string { ("\n" + executing) } -// TODO: Ideally we won't need a global program variable. We should -// refactor this in the future such that each tea program is localized to the -// function that creates it and ExecuteCodeBlockSync doesn't mutate the global -// program variable. -var program *tea.Program = nil - // Create a new interactive mode model. func NewInteractiveModeModel( title string, - engine *Engine, - steps []Step, + subscription string, + environment string, + steps []common.Step, env map[string]string, ) (InteractiveModeModel, error) { // TODO: In the future we should just set the current step for the azure status @@ -494,13 +496,13 @@ func NewInteractiveModeModel( azureStatus := environments.NewAzureDeploymentStatus() azureStatus.CurrentStep = 1 totalCodeBlocks := 0 - codeBlockState := make(map[int]StatefulCodeBlock) + codeBlockState := make(map[int]common.StatefulCodeBlock) - err := az.SetSubscription(engine.Configuration.Subscription) + err := az.SetSubscription(subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) azureStatus.SetError(err) - environments.ReportAzureStatus(azureStatus, engine.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, environment) return InteractiveModeModel{}, err } @@ -512,7 +514,7 @@ func NewInteractiveModeModel( Description: block.Description, }) - codeBlockState[totalCodeBlocks] = StatefulCodeBlock{ + codeBlockState[totalCodeBlocks] = common.StatefulCodeBlock{ StepName: step.Name, CodeBlock: block, StepNumber: stepNumber, @@ -560,9 +562,9 @@ func NewInteractiveModeModel( executingCommand: false, currentCodeBlock: 0, help: help.New(), - environment: engine.Configuration.Environment, + environment: environment, scenarioCompleted: false, ready: false, - commandLines: commandLines, + CommandLines: commandLines, }, nil } diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index cd0e13ec..6681d370 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/Azure/InnovationEngine/internal/az" - "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/patterns" @@ -23,7 +23,7 @@ type TestModeCommands struct { // The state required for testing scenarios. type TestModeModel struct { azureStatus environments.AzureDeploymentStatus - codeBlockState map[int]engine.StatefulCodeBlock + codeBlockState map[int]common.StatefulCodeBlock commands TestModeCommands currentCodeBlock int env map[string]string @@ -59,13 +59,13 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.components = initializeComponents(model, message.Width, message.Height) model.ready = true } else { - model.components.updateViewportSizing(message.Height) + model.components.updateViewportSizing(message.Width, message.Height) } case tea.KeyMsg: model, commands = handleUserInput(model, message) - case engine.SuccessfulCommandMessage: + case common.SuccessfulCommandMessage: // Handle successful command executions model.executingCommand = false step := model.currentCodeBlock @@ -124,11 +124,11 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { // If the scenario has not been completed, we need to execute the next command commands = append( commands, - engine.ExecuteCodeBlockAsync(nextCodeBlockState.CodeBlock, model.env), + common.ExecuteCodeBlockAsync(nextCodeBlockState.CodeBlock, model.env), ) } - case engine.FailedCommandMessage: + case common.FailedCommandMessage: // Handle failed command executions // Update the state of the codeblock which finished executing. @@ -171,8 +171,9 @@ func (model TestModeModel) View() string { // Create a new test mode model. func NewTestModeModel( title string, - engine *engine.Engine, - steps []engine.Step, + subscription string, + environment string, + steps []common.Step, env map[string]string, ) (TestModeModel, error) { // TODO: In the future we should just set the current step for the azure status @@ -180,13 +181,13 @@ func NewTestModeModel( azureStatus := environments.NewAzureDeploymentStatus() azureStatus.CurrentStep = 1 totalCodeBlocks := 0 - codeBlockState := make(map[int]engine.StatefulCodeBlock) + codeBlockState := make(map[int]common.StatefulCodeBlock) - err := az.SetSubscription(engine.Configuration.Subscription) + err := az.SetSubscription(subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) azureStatus.SetError(err) - environments.ReportAzureStatus(azureStatus, engine.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, environment) return TestModeModel{}, err } @@ -200,7 +201,7 @@ func NewTestModeModel( Description: block.Description, }) - codeBlockState[totalCodeBlocks] = engine.StatefulCodeBlock{ + codeBlockState[totalCodeBlocks] = common.StatefulCodeBlock{ StepName: step.Name, CodeBlock: block, StepNumber: stepNumber, @@ -236,7 +237,7 @@ func NewTestModeModel( executingCommand: false, currentCodeBlock: 0, help: help.New(), - environment: engine.Configuration.Environment, + environment: environment, scenarioCompleted: false, ready: false, commandLines: commandLines, diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 1b4b928f..3c46c388 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -6,6 +6,7 @@ import ( "time" "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" @@ -15,7 +16,7 @@ import ( "github.com/Azure/InnovationEngine/internal/ui" ) -func (e *Engine) TestSteps(steps []Step, env map[string]string) error { +func (e *Engine) TestSteps(steps []common.Step, env map[string]string) error { var resourceGroupName string stepsToExecute := filterDeletionCommands(steps, true) err := az.SetSubscription(e.Configuration.Subscription) @@ -63,7 +64,7 @@ testRunner: expectedRegex := block.ExpectedOutput.ExpectedRegex expectedOutputLanguage := block.ExpectedOutput.Language - err := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedRegex, expectedOutputLanguage) + err := common.CompareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedRegex, expectedOutputLanguage) if err != nil { logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) fmt.Print(ui.ErrorStyle.Render("Error when comparing the command outputs: %s\n", err.Error())) From 87f87fe0fb6373537084132dc3ca94e1249ded21 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Jun 2024 12:37:07 -0700 Subject: [PATCH 03/27] [update] init to execute the first codeblock and remove azure status updates from ie test. --- internal/engine/test/model.go | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 6681d370..7dcce66c 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -1,12 +1,10 @@ package test import ( - "fmt" "strings" "github.com/Azure/InnovationEngine/internal/az" "github.com/Azure/InnovationEngine/internal/engine/common" - "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/patterns" "github.com/Azure/InnovationEngine/internal/ui" @@ -22,7 +20,6 @@ type TestModeCommands struct { // The state required for testing scenarios. type TestModeModel struct { - azureStatus environments.AzureDeploymentStatus codeBlockState map[int]common.StatefulCodeBlock commands TestModeCommands currentCodeBlock int @@ -42,7 +39,10 @@ type TestModeModel struct { // Init the test mode model. func (model TestModeModel) Init() tea.Cmd { - return nil + return common.ExecuteCodeBlockAsync( + model.codeBlockState[model.currentCodeBlock].CodeBlock, + model.env, + ) } // Update the test mode model. @@ -106,7 +106,6 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { if codeBlockState.StepName != nextCodeBlockState.StepName { logging.GlobalLogger.Debugf("Step name has changed, incrementing step for Azure") - model.azureStatus.CurrentStep++ } else { logging.GlobalLogger.Debugf("Step name has not changed, not incrementing step for Azure") } @@ -115,7 +114,6 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { // status and quit the program. else, if model.currentCodeBlock == len(model.codeBlockState) { model.scenarioCompleted = true - model.azureStatus.Status = "Succeeded" commands = append( commands, tea.Quit, @@ -143,12 +141,6 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { // Report the error model.executingCommand = false - model.azureStatus.SetError(message.Error) - environments.AttachResourceURIsToAzureStatus( - &model.azureStatus, - model.resourceGroupName, - model.environment, - ) commands = append(commands, tea.Quit) } @@ -176,30 +168,19 @@ func NewTestModeModel( steps []common.Step, env map[string]string, ) (TestModeModel, error) { - // TODO: In the future we should just set the current step for the azure status - // to one as the default. - azureStatus := environments.NewAzureDeploymentStatus() - azureStatus.CurrentStep = 1 totalCodeBlocks := 0 codeBlockState := make(map[int]common.StatefulCodeBlock) err := az.SetSubscription(subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) - azureStatus.SetError(err) - environments.ReportAzureStatus(azureStatus, environment) return TestModeModel{}, err } // TODO(vmarcella): The codeblock state building should be reused across // Interactive mode and test mode in the future. for stepNumber, step := range steps { - azureCodeBlocks := []environments.AzureCodeBlock{} for blockNumber, block := range step.CodeBlocks { - azureCodeBlocks = append(azureCodeBlocks, environments.AzureCodeBlock{ - Command: block.Content, - Description: block.Description, - }) codeBlockState[totalCodeBlocks] = common.StatefulCodeBlock{ StepName: step.Name, @@ -214,7 +195,6 @@ func NewTestModeModel( totalCodeBlocks += 1 } - azureStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name), azureCodeBlocks) } language := codeBlockState[0].CodeBlock.Language @@ -232,7 +212,6 @@ func NewTestModeModel( }, env: env, resourceGroupName: "", - azureStatus: azureStatus, codeBlockState: codeBlockState, executingCommand: false, currentCodeBlock: 0, From 4f4c0a5823d8305616669f2181f8e7d2a02e9b1f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Jun 2024 13:03:24 -0700 Subject: [PATCH 04/27] [add] resource group deletion into the testing again. --- internal/engine/engine.go | 1 + internal/engine/test/model.go | 54 +++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index e51b40d2..bd5e4ee7 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -81,6 +81,7 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { if !ok { return fmt.Errorf("failed to cast tea.Model to TestModeModel") } + fmt.Println(strings.Join(model.CommandLines, "\n")) return err }) diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 7dcce66c..0b09e07b 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -1,12 +1,15 @@ package test import ( + "fmt" "strings" "github.com/Azure/InnovationEngine/internal/az" "github.com/Azure/InnovationEngine/internal/engine/common" + "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/patterns" + "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/ui" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -25,7 +28,6 @@ type TestModeModel struct { currentCodeBlock int env map[string]string environment string - executingCommand bool height int help help.Model resourceGroupName string @@ -34,10 +36,10 @@ type TestModeModel struct { scenarioCompleted bool components testModeComponents ready bool - commandLines []string + CommandLines []string } -// Init the test mode model. +// Init the test mode model by executing the first code block. func (model TestModeModel) Init() tea.Cmd { return common.ExecuteCodeBlockAsync( model.codeBlockState[model.currentCodeBlock].CodeBlock, @@ -67,7 +69,6 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { case common.SuccessfulCommandMessage: // Handle successful command executions - model.executingCommand = false step := model.currentCodeBlock // Update the state of the codeblock which finished executing. @@ -89,7 +90,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.resourceGroupName = tmpResourceGroup } } - model.commandLines = append(model.commandLines, codeBlockState.StdOut) + model.CommandLines = append(model.CommandLines, codeBlockState.StdOut) // Increment the codeblock and update the viewport content. model.currentCodeBlock++ @@ -98,18 +99,12 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { nextCommand := model.codeBlockState[model.currentCodeBlock].CodeBlock.Content nextLanguage := model.codeBlockState[model.currentCodeBlock].CodeBlock.Language - model.commandLines = append(model.commandLines, ui.CommandPrompt(nextLanguage)+nextCommand) + model.CommandLines = append(model.CommandLines, ui.CommandPrompt(nextLanguage)+nextCommand) } // Only increment the step for azure if the step name has changed. nextCodeBlockState := model.codeBlockState[model.currentCodeBlock] - if codeBlockState.StepName != nextCodeBlockState.StepName { - logging.GlobalLogger.Debugf("Step name has changed, incrementing step for Azure") - } else { - logging.GlobalLogger.Debugf("Step name has not changed, not incrementing step for Azure") - } - // If the scenario has been completed, we need to update the azure // status and quit the program. else, if model.currentCodeBlock == len(model.codeBlockState) { @@ -137,14 +132,38 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { codeBlockState.Success = false model.codeBlockState[step] = codeBlockState - model.commandLines = append(model.commandLines, codeBlockState.StdErr) + model.CommandLines = append(model.CommandLines, codeBlockState.StdErr) - // Report the error - model.executingCommand = false commands = append(commands, tea.Quit) + + case tea.QuitMsg: + // TODO: Generate test report + + // Delete any found resource groups. + if model.resourceGroupName != "" { + logging.GlobalLogger.Infof("Attempting to delete the deployed resource group with the name: %s", model.resourceGroupName) + command := fmt.Sprintf("az group delete --name %s --yes --no-wait", model.resourceGroupName) + _, err := shells.ExecuteBashCommand( + command, + shells.BashCommandConfiguration{ + EnvironmentVariables: lib.CopyMap(model.env), + InheritEnvironment: true, + InteractiveCommand: false, + WriteToHistory: true, + }, + ) + if err != nil { + model.CommandLines = append(model.CommandLines, ui.ErrorStyle.Render("Error deleting resource group: %s\n", err.Error())) + logging.GlobalLogger.Errorf("Error deleting resource group: %s", err.Error()) + } else { + model.CommandLines = append(model.CommandLines, "Resource group deleted successfully.") + } + + } + } - model.components.commandViewport.SetContent(strings.Join(model.commandLines, "\n")) + model.components.commandViewport.SetContent(strings.Join(model.CommandLines, "\n")) // Update all the viewports and append resulting commands. var command tea.Cmd @@ -213,12 +232,11 @@ func NewTestModeModel( env: env, resourceGroupName: "", codeBlockState: codeBlockState, - executingCommand: false, currentCodeBlock: 0, help: help.New(), environment: environment, scenarioCompleted: false, ready: false, - commandLines: commandLines, + CommandLines: commandLines, }, nil } From da52d23c1554ba2eb9c113a6ef868120fbf1f1a7 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Jun 2024 15:50:42 -0700 Subject: [PATCH 05/27] [add] initial set of tests for the test mode model. --- internal/engine/test/model_test.go | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 internal/engine/test/model_test.go diff --git a/internal/engine/test/model_test.go b/internal/engine/test/model_test.go new file mode 100644 index 00000000..2e594f44 --- /dev/null +++ b/internal/engine/test/model_test.go @@ -0,0 +1,72 @@ +package test + +import ( + "testing" + + "github.com/Azure/InnovationEngine/internal/engine/common" + "github.com/Azure/InnovationEngine/internal/parsers" +) + +func TestTestModeModel(t *testing.T) { + t.Run("Initializing a test model with an invalid subscription fails.", func(t *testing.T) { + // Test the initialization of the test mode model. + _, err := NewTestModeModel("test", "invalid", "test", nil, nil) + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("Creating a test model works.", func(t *testing.T) { + // Test the initialization of the test mode model. + model, err := NewTestModeModel("test", "", "test", nil, nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if model.components.commandViewport.View() != "" { + t.Errorf("Expected view to be empty, got %q", model.components.commandViewport.View()) + } + + if model.scenarioTitle != "test" { + t.Errorf("Expected scenario title to be %q, got %q", "test", model.scenarioTitle) + } + + if model.env != nil { + t.Errorf("Expected env to be nil, got %v", model.env) + } + }) + + t.Run("Creating a test model with steps works.", func(t *testing.T) { + // Test the initialization of the test mode model. + + steps := []common.Step{ + { + Name: "step1", + CodeBlocks: []parsers.CodeBlock{ + { + Header: "header1", + Content: "echo 'hello world'", + Language: "bash", + }, + }, + }, + } + + model, err := NewTestModeModel("test", "", "test", steps, nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if model.currentCodeBlock != 0 { + t.Errorf("Expected current code block to be 0, got %d", model.currentCodeBlock) + } + + if model.codeBlockState[0].CodeBlock.Content != "echo 'hello world'" { + t.Errorf( + "Expected code block content to be %q, got %q", + "echo 'hello world'", + model.codeBlockState[0].CodeBlock.Content, + ) + } + }) +} From 091c17e10d6d5738057d3531a312f358c0e04a8e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Jun 2024 16:34:24 -0700 Subject: [PATCH 06/27] [add] test to assert that the model automatically transitions to running commands after being initialized. --- internal/engine/common/codeblock.go | 6 +++ internal/engine/test/model.go | 5 ++ internal/engine/test/model_test.go | 76 +++++++++++++++++------------ 3 files changed, 57 insertions(+), 30 deletions(-) diff --git a/internal/engine/common/codeblock.go b/internal/engine/common/codeblock.go index 8f6a0226..b1340436 100644 --- a/internal/engine/common/codeblock.go +++ b/internal/engine/common/codeblock.go @@ -14,3 +14,9 @@ type StatefulCodeBlock struct { StepNumber int Success bool } + +// Checks if a codeblock was executed by looking at the +// output, errors, and if success is true. +func (s StatefulCodeBlock) WasExecuted() bool { + return s.StdOut != "" || s.StdErr != "" || s.Error != nil || s.Success +} diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 0b09e07b..c65eba06 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -196,6 +196,11 @@ func NewTestModeModel( return TestModeModel{}, err } + // If the environment variables are not set, set it to an empty map. + if len(env) == 0 || env == nil { + env = make(map[string]string) + } + // TODO(vmarcella): The codeblock state building should be reused across // Interactive mode and test mode in the future. for stepNumber, step := range steps { diff --git a/internal/engine/test/model_test.go b/internal/engine/test/model_test.go index 2e594f44..d7eb5ccc 100644 --- a/internal/engine/test/model_test.go +++ b/internal/engine/test/model_test.go @@ -5,35 +5,27 @@ import ( "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/stretchr/testify/assert" ) +// This suite of tests is responsible for ensuring that the model around test mode +// is well defined and behaves as expected. func TestTestModeModel(t *testing.T) { t.Run("Initializing a test model with an invalid subscription fails.", func(t *testing.T) { // Test the initialization of the test mode model. _, err := NewTestModeModel("test", "invalid", "test", nil, nil) - if err == nil { - t.Errorf("Expected error, got nil") - } + assert.Error(t, err) }) - t.Run("Creating a test model works.", func(t *testing.T) { + t.Run("Creating a valid test model works.", func(t *testing.T) { // Test the initialization of the test mode model. model, err := NewTestModeModel("test", "", "test", nil, nil) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if model.components.commandViewport.View() != "" { - t.Errorf("Expected view to be empty, got %q", model.components.commandViewport.View()) - } + assert.NoError(t, err) - if model.scenarioTitle != "test" { - t.Errorf("Expected scenario title to be %q, got %q", "test", model.scenarioTitle) - } + assert.Equal(t, "test", model.scenarioTitle) - if model.env != nil { - t.Errorf("Expected env to be nil, got %v", model.env) - } + assert.Equal(t, "", model.components.commandViewport.View()) + assert.Equal(t, map[string]string{}, model.env) }) t.Run("Creating a test model with steps works.", func(t *testing.T) { @@ -53,20 +45,44 @@ func TestTestModeModel(t *testing.T) { } model, err := NewTestModeModel("test", "", "test", steps, nil) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } + assert.NoError(t, err) - if model.currentCodeBlock != 0 { - t.Errorf("Expected current code block to be 0, got %d", model.currentCodeBlock) - } + assert.Equal(t, 0, model.currentCodeBlock) + assert.Equal(t, 1, len(model.codeBlockState)) - if model.codeBlockState[0].CodeBlock.Content != "echo 'hello world'" { - t.Errorf( - "Expected code block content to be %q, got %q", - "echo 'hello world'", - model.codeBlockState[0].CodeBlock.Content, - ) - } + state := model.codeBlockState[0] + + assert.Equal(t, "step1", state.StepName) + assert.Equal(t, "bash", state.CodeBlock.Language) + assert.Equal(t, "header1", state.CodeBlock.Header) + assert.Equal(t, "echo 'hello world'", state.CodeBlock.Content) + assert.Equal(t, false, state.Success) }) + + t.Run( + "Initializing the test model invokes the first command to start running tests.", + func(t *testing.T) { + steps := []common.Step{ + { + Name: "step1", + CodeBlocks: []parsers.CodeBlock{ + { + Header: "header1", + Content: "echo 'hello world'", + Language: "bash", + }, + }, + }, + } + + model, err := NewTestModeModel("test", "", "test", steps, nil) + assert.NoError(t, err) + + m, _ := model.Update(model.Init()()) + + if model, ok := m.(TestModeModel); ok { + assert.Equal(t, 1, model.currentCodeBlock) + } + }, + ) } From 4f1545f0fc9f6380e8ed4b30a7f55f22b72229c2 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 7 Jun 2024 13:42:54 -0700 Subject: [PATCH 07/27] [update] test to check the result of executing the first codeblock. --- internal/engine/test/model_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/engine/test/model_test.go b/internal/engine/test/model_test.go index d7eb5ccc..d71fec25 100644 --- a/internal/engine/test/model_test.go +++ b/internal/engine/test/model_test.go @@ -82,6 +82,13 @@ func TestTestModeModel(t *testing.T) { if model, ok := m.(TestModeModel); ok { assert.Equal(t, 1, model.currentCodeBlock) + + executedBlock := model.codeBlockState[0] + + // Assert outputs of the executed block. + assert.Equal(t, "hello world\n", executedBlock.StdOut) + assert.Equal(t, "", executedBlock.StdErr) + assert.Equal(t, true, executedBlock.Success) } }, ) From e1154e77ca59418ee58bbc39a7053d9566604b84 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jun 2024 16:15:55 -0700 Subject: [PATCH 08/27] fix(tests): Add local markdown tests to testing pipeline to see output in github actions, update logic for deleting resource groups after execution, and add more tests to test-mode. --- .github/workflows/scenario-testing.yaml | 3 +- Makefile | 10 + internal/az/group.go | 1 - internal/engine/common/commands.go | 10 + internal/engine/common/scenario_test.go | 11 +- internal/engine/test/model.go | 74 ++++---- internal/engine/test/model_test.go | 126 ++++++++++++- scenarios/testing/CommentTest.md | 9 +- scenarios/testing/brokenMarkdown.md | 3 + scenarios/testing/createRG.md | 29 --- .../testing/e2eAzureTestCommentVariables.md | 159 ---------------- scenarios/testing/fuzzyMatchTest.md | 11 +- scenarios/testing/nonCLI.md | 114 ------------ scenarios/testing/test.md | 172 ------------------ scenarios/testing/variables.md | 32 +++- 15 files changed, 242 insertions(+), 522 deletions(-) delete mode 100644 scenarios/testing/createRG.md delete mode 100644 scenarios/testing/e2eAzureTestCommentVariables.md delete mode 100644 scenarios/testing/nonCLI.md delete mode 100644 scenarios/testing/test.md diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index e0aa5ed0..0cabce02 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -39,10 +39,11 @@ jobs: environment: ScenarioTesting steps: - uses: actions/checkout@v2 - - name: Build all targets. + - name: Build & test all targets. run: | make build-all make test-all WITH_COVERAGE=true + make test-local-scenarios - name: Upload test coverage uses: actions/upload-artifact@v2 if: github.event_name == 'pull_request' diff --git a/Makefile b/Makefile index 82e12250..54408dcb 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,11 @@ SCENARIO ?= ./README.md WORKING_DIRECTORY ?= $(PWD) test-scenario: @echo "Running scenario $(SCENARIO)" +ifeq ($(SUBSCRIPTION), 00000000-0000-0000-0000-000000000000) + $(IE_BINARY) test $(SCENARIO) --working-directory $(WORKING_DIRECTORY) +else $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) +endif test-scenarios: @echo "Testing out the scenarios" @@ -48,6 +52,12 @@ test-scenarios: ($(MAKE) test-scenario SCENARIO="$${dir}README.md" SUBCRIPTION="$(SUBSCRIPTION)") || exit $$?; \ done +test-local-scenarios: + @echo "Testing out the local scenarios" + for file in ./scenarios/testing/*.md; do \ + ($(MAKE) test-scenario SCENARIO="$${file}") || exit $$?; \ + done + test-upstream-scenarios: @echo "Pulling the upstream scenarios" @git config --global --add safe.directory /home/runner/work/InnovationEngine/InnovationEngine diff --git a/internal/az/group.go b/internal/az/group.go index 82cb9b6e..d03a7b9e 100644 --- a/internal/az/group.go +++ b/internal/az/group.go @@ -17,7 +17,6 @@ func FindAllDeployedResourceURIs(resourceGroup string) []string { WriteToHistory: true, }, ) - if err != nil { logging.GlobalLogger.Error("Failed to list deployments", err) } diff --git a/internal/engine/common/commands.go b/internal/engine/common/commands.go index f74a6fdd..2e550883 100644 --- a/internal/engine/common/commands.go +++ b/internal/engine/common/commands.go @@ -23,6 +23,16 @@ type FailedCommandMessage struct { Error error } +type ExitMessage struct { + EncounteredFailure bool +} + +func Exit(encounteredFailure bool) tea.Cmd { + return func() tea.Msg { + return ExitMessage{EncounteredFailure: encounteredFailure} + } +} + // Executes a bash command and returns a tea message with the output. This function // will be executed asycnhronously. func ExecuteCodeBlockAsync(codeBlock parsers.CodeBlock, env map[string]string) tea.Cmd { diff --git a/internal/engine/common/scenario_test.go b/internal/engine/common/scenario_test.go index 8129c1ba..8a06015f 100644 --- a/internal/engine/common/scenario_test.go +++ b/internal/engine/common/scenario_test.go @@ -80,10 +80,11 @@ func TestResolveMarkdownSource(t *testing.T) { } func TestVariableOverrides(t *testing.T) { + variableScenarioPath := "../../../scenarios/testing/variables.md" // Test overriding environment variables t.Run("Override a standard variable declaration", func(t *testing.T) { scenario, err := CreateScenarioFromMarkdown( - "../../scenarios/testing/variables.md", + variableScenarioPath, []string{"bash"}, map[string]string{ "MY_VAR": "my_value", @@ -99,7 +100,7 @@ func TestVariableOverrides(t *testing.T) { "Override a variable that is declared on the same line as another variable, separated by &&", func(t *testing.T) { scenario, err := CreateScenarioFromMarkdown( - "../../scenarios/testing/variables.md", + variableScenarioPath, []string{"bash"}, map[string]string{ "NEXT_VAR": "next_value", @@ -120,7 +121,7 @@ func TestVariableOverrides(t *testing.T) { "Override a variable that is declared on the same line as another variable, separated by ;", func(t *testing.T) { scenario, err := CreateScenarioFromMarkdown( - "../../scenarios/testing/variables.md", + variableScenarioPath, []string{"bash"}, map[string]string{ "THIS_VAR": "this_value", @@ -140,7 +141,7 @@ func TestVariableOverrides(t *testing.T) { t.Run("Override a variable that has a subshell command as it's value", func(t *testing.T) { scenario, err := CreateScenarioFromMarkdown( - "../../scenarios/testing/variables.md", + variableScenarioPath, []string{"bash"}, map[string]string{ "SUBSHELL_VARIABLE": "subshell_value", @@ -158,7 +159,7 @@ func TestVariableOverrides(t *testing.T) { t.Run("Override a variable that references another variable", func(t *testing.T) { scenario, err := CreateScenarioFromMarkdown( - "../../scenarios/testing/variables.md", + variableScenarioPath, []string{"bash"}, map[string]string{ "VAR2": "var2_value", diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index c65eba06..88dd39e6 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -21,29 +21,32 @@ type TestModeCommands struct { quit key.Binding } +// Creating an alias to make it easier to test the execution of bash commands. +// TODO(vmarcella): We should abstract this behind some sort of generic +// interface in the future. +var executeBashCommand = shells.ExecuteBashCommand + // The state required for testing scenarios. type TestModeModel struct { - codeBlockState map[int]common.StatefulCodeBlock - commands TestModeCommands - currentCodeBlock int - env map[string]string - environment string - height int - help help.Model - resourceGroupName string - scenarioTitle string - width int - scenarioCompleted bool - components testModeComponents - ready bool - CommandLines []string + codeBlockState map[int]common.StatefulCodeBlock + commands TestModeCommands + currentCodeBlock int + environmentVariables map[string]string + environment string + help help.Model + resourceGroupName string + scenarioTitle string + scenarioCompleted bool + components testModeComponents + ready bool + CommandLines []string } // Init the test mode model by executing the first code block. func (model TestModeModel) Init() tea.Cmd { return common.ExecuteCodeBlockAsync( model.codeBlockState[model.currentCodeBlock].CodeBlock, - model.env, + model.environmentVariables, ) } @@ -54,8 +57,6 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { switch message := message.(type) { case tea.WindowSizeMsg: - model.width = message.Width - model.height = message.Height logging.GlobalLogger.Debugf("Window size changed to: %d x %d", message.Width, message.Height) if !model.ready { model.components = initializeComponents(model, message.Width, message.Height) @@ -108,16 +109,17 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { // If the scenario has been completed, we need to update the azure // status and quit the program. else, if model.currentCodeBlock == len(model.codeBlockState) { - model.scenarioCompleted = true + logging.GlobalLogger.Infof("The last codeblock was executed. Requesting to exit test mode...") commands = append( commands, - tea.Quit, + common.Exit(false), ) + } else { // If the scenario has not been completed, we need to execute the next command commands = append( commands, - common.ExecuteCodeBlockAsync(nextCodeBlockState.CodeBlock, model.env), + common.ExecuteCodeBlockAsync(nextCodeBlockState.CodeBlock, model.environmentVariables), ) } @@ -134,19 +136,19 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.codeBlockState[step] = codeBlockState model.CommandLines = append(model.CommandLines, codeBlockState.StdErr) - commands = append(commands, tea.Quit) + commands = append(commands, common.Exit(true)) - case tea.QuitMsg: + case common.ExitMessage: // TODO: Generate test report // Delete any found resource groups. if model.resourceGroupName != "" { logging.GlobalLogger.Infof("Attempting to delete the deployed resource group with the name: %s", model.resourceGroupName) command := fmt.Sprintf("az group delete --name %s --yes --no-wait", model.resourceGroupName) - _, err := shells.ExecuteBashCommand( + _, err := executeBashCommand( command, shells.BashCommandConfiguration{ - EnvironmentVariables: lib.CopyMap(model.env), + EnvironmentVariables: lib.CopyMap(model.environmentVariables), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true, @@ -161,6 +163,12 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { } + // If the model didn't encounter a failure, then the scenario was scenario + // was completed successfully. + model.scenarioCompleted = !message.EncounteredFailure + + commands = append(commands, tea.Quit) + } model.components.commandViewport.SetContent(strings.Join(model.CommandLines, "\n")) @@ -234,14 +242,14 @@ func NewTestModeModel( key.WithHelp("q", "Quit the scenario."), ), }, - env: env, - resourceGroupName: "", - codeBlockState: codeBlockState, - currentCodeBlock: 0, - help: help.New(), - environment: environment, - scenarioCompleted: false, - ready: false, - CommandLines: commandLines, + environmentVariables: env, + resourceGroupName: "", + codeBlockState: codeBlockState, + currentCodeBlock: 0, + help: help.New(), + environment: environment, + scenarioCompleted: false, + ready: false, + CommandLines: commandLines, }, nil } diff --git a/internal/engine/test/model_test.go b/internal/engine/test/model_test.go index d71fec25..4836a238 100644 --- a/internal/engine/test/model_test.go +++ b/internal/engine/test/model_test.go @@ -5,6 +5,7 @@ import ( "github.com/Azure/InnovationEngine/internal/engine/common" "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/shells" "github.com/stretchr/testify/assert" ) @@ -25,7 +26,7 @@ func TestTestModeModel(t *testing.T) { assert.Equal(t, "test", model.scenarioTitle) assert.Equal(t, "", model.components.commandViewport.View()) - assert.Equal(t, map[string]string{}, model.env) + assert.Equal(t, map[string]string{}, model.environmentVariables) }) t.Run("Creating a test model with steps works.", func(t *testing.T) { @@ -92,4 +93,127 @@ func TestTestModeModel(t *testing.T) { } }, ) + + t.Run( + "Test mode doesn't try to delete resource group if none was created.", + func(t *testing.T) { + steps := []common.Step{ + { + Name: "step1", + CodeBlocks: []parsers.CodeBlock{ + { + Header: "header1", + Content: "echo 'hello world'", + }, + }, + }, + } + + model, err := NewTestModeModel("test", "", "test", steps, nil) + + assert.NoError(t, err) + + m, _ := model.Update(model.Init()()) + + if model, ok := m.(TestModeModel); ok { + assert.Equal(t, 1, model.currentCodeBlock) + + executedBlock := model.codeBlockState[0] + model.resourceGroupName = "test" + + // Assert outputs of the executed block. + assert.Equal(t, "hello world\n", executedBlock.StdOut) + assert.Equal(t, "", executedBlock.StdErr) + assert.Equal(t, true, executedBlock.Success) + } else { + assert.Fail(t, "Model is not a TestModeModel") + } + + // Assert that the model doesn't try to delete the resource group when + // the resource group name is empty. + m, _ = model.Update(common.Exit(false)()) + counter := 0 + // We create a mock function to replace the shells.ExecuteBashCommand function + // to make sure that the function is not called. + executeBashCommand = func( + command string, + config shells.BashCommandConfiguration, + ) (shells.CommandOutput, error) { + counter += 1 + return shells.CommandOutput{}, nil + } + + if model, ok := m.(TestModeModel); ok { + assert.Equal(t, 0, counter) + assert.Equal(t, true, model.scenarioCompleted) + } else { + assert.Fail(t, "Model is not a TestModeModel") + } + }, + ) + + t.Run( + "Test mode tries to delete resource group if one was created.", + func(t *testing.T) { + steps := []common.Step{ + { + Name: "step1", + CodeBlocks: []parsers.CodeBlock{ + { + Header: "header1", + Content: "echo 'hello world'", + }, + }, + }, + } + + model, err := NewTestModeModel("test", "", "test", steps, nil) + + assert.NoError(t, err) + + m, _ := model.Update(model.Init()()) + + var ok bool + + if model, ok = m.(TestModeModel); ok { + assert.Equal(t, 1, model.currentCodeBlock) + + executedBlock := model.codeBlockState[0] + model.resourceGroupName = "test" + + // Assert outputs of the executed block. + assert.Equal(t, "hello world\n", executedBlock.StdOut) + assert.Equal(t, "", executedBlock.StdErr) + assert.Equal(t, true, executedBlock.Success) + + } else { + assert.Fail(t, "Model is not a TestModeModel") + } + + // Assert that the model tries to delete the resource group when + // the resource group name is not empty. + counter := 0 + recordedCommand := "" + // We create a mock function to replace the shells.ExecuteBashCommand function + // to make sure that the function is called. + executeBashCommand = func( + command string, + config shells.BashCommandConfiguration, + ) (shells.CommandOutput, error) { + recordedCommand = command + counter += 1 + return shells.CommandOutput{}, nil + } + + m, _ = model.Update(common.Exit(false)()) + + if model, ok = m.(TestModeModel); ok { + assert.Equal(t, 1, counter) + assert.Equal(t, "az group delete --name test --yes --no-wait", recordedCommand) + assert.Equal(t, true, model.scenarioCompleted) + } else { + assert.Fail(t, "Model is not a TestModeModel") + } + }, + ) } diff --git a/scenarios/testing/CommentTest.md b/scenarios/testing/CommentTest.md index 05627f9b..89097c38 100644 --- a/scenarios/testing/CommentTest.md +++ b/scenarios/testing/CommentTest.md @@ -1,22 +1,23 @@ - - # Testing multi Line code block -```azurecli-interactive +```bash echo "Hello \ world" ``` # This is what the output should be + + ```text hello world -``` \ No newline at end of file +``` + diff --git a/scenarios/testing/brokenMarkdown.md b/scenarios/testing/brokenMarkdown.md index 2754fdb4..7ce2e7a4 100644 --- a/scenarios/testing/brokenMarkdown.md +++ b/scenarios/testing/brokenMarkdown.md @@ -1,3 +1,5 @@ +# Broken + This is a markdown file which does not pass the requirements... It has a code block which never ends. Innovation Engine should be able to exit the program automatically instead of hanging @@ -5,3 +7,4 @@ Innovation Engine should be able to exit the program automatically instead of ha ```bash echo "hello World" `` + diff --git a/scenarios/testing/createRG.md b/scenarios/testing/createRG.md deleted file mode 100644 index f53b7693..00000000 --- a/scenarios/testing/createRG.md +++ /dev/null @@ -1,29 +0,0 @@ - - - -## Create a resource group - -Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: - -```bash -az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION -``` - - -```Output -{ - "fqdns": "", - "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", - "location": "eastus", - "macAddress": "00-0D-3A-23-9A-49", - "powerState": "VM running", - "privateIpAddress": "10.0.0.4", - "publicIpAddress": "40.68.254.142", - "resourceGroup": "myResourceGroup" -} -``` \ No newline at end of file diff --git a/scenarios/testing/e2eAzureTestCommentVariables.md b/scenarios/testing/e2eAzureTestCommentVariables.md deleted file mode 100644 index 2106218f..00000000 --- a/scenarios/testing/e2eAzureTestCommentVariables.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: 'Quickstart: Use the Azure CLI to create a Linux VM' -description: In this quickstart, you learn how to use the Azure CLI to create a Linux virtual machine -author: cynthn -ms.service: virtual-machines -ms.collection: linux -ms.topic: quickstart -ms.workload: infrastructure -ms.date: 06/01/2022 -ms.author: cynthn -ms.custom: mvc, seo-javascript-september2019, seo-javascript-october2019, seo-python-october2019, devx-track-azurecli, mode-api ---- - - - -# Quickstart: Create a Linux virtual machine with the Azure CLI - -**Applies to:** :heavy_check_mark: Linux VMs - -This quickstart shows you how to use the Azure CLI to deploy a Linux virtual machine (VM) in Azure. The Azure CLI is used to create and manage Azure resources via either the command line or scripts. - -In this tutorial, we will be installing the latest Debian image. To show the VM in action, you'll connect to it using SSH and install the NGINX web server. - -If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. - -## Launch Azure Cloud Shell - -The Azure Cloud Shell is a free interactive shell that you can use to run the steps in this article. It has common Azure tools preinstalled and configured to use with your account. - -To open the Cloud Shell, just select **Try it** from the upper right corner of a code block. You can also open Cloud Shell in a separate browser tab by going to [https://shell.azure.com/bash](https://shell.azure.com/bash). Select **Copy** to copy the blocks of code, paste it into the Cloud Shell, and select **Enter** to run it. - -If you prefer to install and use the CLI locally, this quickstart requires Azure CLI version 2.0.30 or later. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI]( /cli/azure/install-azure-cli). - - - -## Create a resource group - -Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: - -```bash -az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION -``` - -## Create virtual machine - -Create a VM with the [az vm create](/cli/azure/vm) command. - -The following example creates a VM named *myVM* and adds a user account named *azureuser*. The `--generate-ssh-keys` parameter is used to automatically generate an SSH key, and put it in the default key location (*~/.ssh*). To use a specific set of keys instead, use the `--ssh-key-values` option. - -```bash -az vm create \ - --resource-group $MY_RESOURCE_GROUP_NAME \ - --name $MY_VM_NAME \ - --image $MY_VM_IMAGE \ - --admin-username $MY_ADMIN_USERNAME \ - --generate-ssh-keys -``` - -It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. - -```Output -{ - "fqdns": "", - "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", - "location": "eastus", - "macAddress": "00-0D-3A-23-9A-49", - "powerState": "VM running", - "privateIpAddress": "10.0.0.4", - "publicIpAddress": "40.68.254.142", - "resourceGroup": "myResourceGroup" -} -``` - -Make a note of the `publicIpAddress` to use later. - -## Install web server - -To see your VM in action, install the NGINX web server. Update your package sources and then install the latest NGINX package. - -```bash -az vm run-command invoke \ - -g $MY_RESOURCE_GROUP_NAME \ - -n $MY_VM_NAME \ - --command-id RunShellScript \ - --scripts "sudo apt-get update && sudo apt-get install -y nginx" -``` - -## Open port 80 for web traffic - -By default, only SSH connections are opened when you create a Linux VM in Azure. Use [az vm open-port](/cli/azure/vm) to open TCP port 80 for use with the NGINX web server: - -```bash -az vm open-port --port 80 --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME -``` - -## View the web server in action - -Use a web browser of your choice to view the default NGINX welcome page. Use the public IP address of your VM as the web address. The following example shows the default NGINX web site: - -![Screenshot showing the N G I N X default web page.](./media/quick-create-cli/nginix-welcome-page-debian.png) - -Or Run the following command to see the NGINX welcome page in terminal - -```bash - curl $(az vm show -d -g $MY_RESOURCE_GROUP_NAME -n $MY_VM_NAME --query "publicIps" -o tsv) -``` - - -```HTML - - - -Welcome to nginx! - - - -

Welcome to nginx!

-

If you see this page, the nginx web server is successfully installed and -working. Further configuration is required.

- -

For online documentation and support please refer to -nginx.org.
-Commercial support is available at -nginx.com.

- -

Thank you for using nginx.

- - -``` - -## Clean up resources - -When no longer needed, you can use the [az group delete](/cli/azure/group) command to remove the resource group, VM, and all related resources. - -```bash -az group delete --name $MY_RESOURCE_GROUP_NAME --no-wait --yes --verbose -``` - -## Next steps - -In this quickstart, you deployed a simple virtual machine, opened a network port for web traffic, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. - - -> [!div class="nextstepaction"] -> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/scenarios/testing/fuzzyMatchTest.md b/scenarios/testing/fuzzyMatchTest.md index f58e37c3..d0e84e84 100644 --- a/scenarios/testing/fuzzyMatchTest.md +++ b/scenarios/testing/fuzzyMatchTest.md @@ -3,13 +3,15 @@ ```azurecli-interactive echo "Hello World" ``` + This is what the expected output should be + + ```text Hello world ``` - # Testing multi Line code block ```azurecli-interactive @@ -18,7 +20,9 @@ world" ``` # Output Should Fail + + ```text world Hello ``` @@ -31,7 +35,9 @@ world" ``` # Output Should Pass + + ```text Hello world ``` @@ -44,8 +50,9 @@ world" ``` # Bad similarity - should fail + + ```text Hello world ``` - diff --git a/scenarios/testing/nonCLI.md b/scenarios/testing/nonCLI.md deleted file mode 100644 index 9d16b6fb..00000000 --- a/scenarios/testing/nonCLI.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Quickstart - Create a Linux VM in the Azure portal -description: In this quickstart, you learn how to use the Azure portal to create a Linux virtual machine. -author: cynthn -ms.service: virtual-machines -ms.collection: linux -ms.topic: quickstart -ms.workload: infrastructure -ms.date: 08/01/2022 -ms.author: cynthn -ms.custom: mvc, mode-ui ---- -This document will not be a CLI document. I am curious what innovation engine will do in this case. - -# Quickstart: Create a Linux virtual machine in the Azure portal - -**Applies to:** :heavy_check_mark: Linux VMs - -Azure virtual machines (VMs) can be created through the Azure portal. The Azure portal is a browser-based user interface to create Azure resources. This quickstart shows you how to use the Azure portal to deploy a Linux virtual machine (VM) running Ubuntu 18.04 LTS. To see your VM in action, you also SSH to the VM and install the NGINX web server. - -If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. - -## Sign in to Azure - -Sign in to the [Azure portal](https://portal.azure.com). - -## Create virtual machine - -1. Enter *virtual machines* in the search. -1. Under **Services**, select **Virtual machines**. -1. In the **Virtual machines** page, select **Create** and then **Virtual machine**. The **Create a virtual machine** page opens. - -1. In the **Basics** tab, under **Project details**, make sure the correct subscription is selected and then choose to **Create new** resource group. Enter *myResourceGroup* for the name.*. - - ![Screenshot of the Project details section showing where you select the Azure subscription and the resource group for the virtual machine](./media/quick-create-portal/project-details.png) - -1. Under **Instance details**, enter *myVM* for the **Virtual machine name**, and choose *Ubuntu 18.04 LTS - Gen2* for your **Image**. Leave the other defaults. The default size and pricing is only shown as an example. Size availability and pricing are dependent on your region and subscription. - - :::image type="content" source="media/quick-create-portal/instance-details.png" alt-text="Screenshot of the Instance details section where you provide a name for the virtual machine and select its region, image, and size."::: - - > [!NOTE] - > Some users will now see the option to create VMs in multiple zones. To learn more about this new capability, see [Create virtual machines in an availability zone](../create-portal-availability-zone.md). - > :::image type="content" source="../media/create-portal-availability-zone/preview.png" alt-text="Screenshot showing that you have the option to create virtual machines in multiple availability zones."::: - - -1. Under **Administrator account**, select **SSH public key**. - -1. In **Username** enter *azureuser*. - -1. For **SSH public key source**, leave the default of **Generate new key pair**, and then enter *myKey* for the **Key pair name**. - - ![Screenshot of the Administrator account section where you select an authentication type and provide the administrator credentials](./media/quick-create-portal/administrator-account.png) - -1. Under **Inbound port rules** > **Public inbound ports**, choose **Allow selected ports** and then select **SSH (22)** and **HTTP (80)** from the drop-down. - - ![Screenshot of the inbound port rules section where you select what ports inbound connections are allowed on](./media/quick-create-portal/inbound-port-rules.png) - -1. Leave the remaining defaults and then select the **Review + create** button at the bottom of the page. - -1. On the **Create a virtual machine** page, you can see the details about the VM you are about to create. When you are ready, select **Create**. - -1. When the **Generate new key pair** window opens, select **Download private key and create resource**. Your key file will be download as **myKey.pem**. Make sure you know where the `.pem` file was downloaded; you will need the path to it in the next step. - -1. When the deployment is finished, select **Go to resource**. - -1. On the page for your new VM, select the public IP address and copy it to your clipboard. - - - ![Screenshot showing how to copy the IP address for the virtual machine](./media/quick-create-portal/ip-address.png) - - -## Connect to virtual machine - -Create an SSH connection with the VM. - -1. If you are on a Mac or Linux machine, open a Bash prompt and set read-only permission on the .pem file using `chmod 400 ~/Downloads/myKey.pem`. If you are on a Windows machine, open a PowerShell prompt. - -1. At your prompt, open an SSH connection to your virtual machine. Replace the IP address with the one from your VM, and replace the path to the `.pem` with the path to where the key file was downloaded. - -```console -ssh -i ~/Downloads/myKey.pem azureuser@10.111.12.123 -``` - -> [!TIP] -> The SSH key you created can be used the next time your create a VM in Azure. Just select the **Use a key stored in Azure** for **SSH public key source** the next time you create a VM. You already have the private key on your computer, so you won't need to download anything. - -## Install web server - -To see your VM in action, install the NGINX web server. From your SSH session, update your package sources and then install the latest NGINX package. - -```bash -sudo apt-get -y update -sudo apt-get -y install nginx -``` - -When done, type `exit` to leave the SSH session. - - -## View the web server in action - -Use a web browser of your choice to view the default NGINX welcome page. Type the public IP address of the VM as the web address. The public IP address can be found on the VM overview page or as part of the SSH connection string you used earlier. - -![Screenshot showing the NGINX default site in a browser](./media/quick-create-portal/nginx.png) - -## Clean up resources - -When no longer needed, you can delete the resource group, virtual machine, and all related resources. To do so, select the resource group for the virtual machine, select **Delete**, then confirm the name of the resource group to delete. - -## Next steps - -In this quickstart, you deployed a simple virtual machine, created a Network Security Group and rule, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. - -> [!div class="nextstepaction"] -> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/scenarios/testing/test.md b/scenarios/testing/test.md deleted file mode 100644 index 164f31b3..00000000 --- a/scenarios/testing/test.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: 'Quickstart: Use the Azure CLI to create a Linux VM' ---- - -# Prerequisites - -Innovation Engine can process prerequisites for documents. This code section tests that the pre requisites functionality works in Innovation Engine. -It will run the following real prerequisites along with a look for and fail to run a fake prerequisite. - -You must have completed [Fuzzy Matching Test](testScripts/fuzzyMatchTest.md) and you must have completed [Comment Test](testScripts/CommentTest.md) - -You also need to have completed [This is a fake file](testScripts/fakefile.md) - -And there are going to be additional \ and ( to throw off the algorithm... - -# Running simple bash commands - -Innovation engine can execute bash commands. For example - - -```bash -echo "Hello World" -``` - -# Test Code block with expected output - -```azurecli-interactive -echo "Hello \ -world" -``` - -It also can test the output to make sure everything ran as planned. - -``` -Hello world -``` - -# Test non-executable code blocks -If a code block does not have an executable tag it will simply render the codeblock as text - -For example: - -```YAML -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-back -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-back - template: - metadata: - labels: - app: azure-vote-back - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-back - image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 - env: - - name: ALLOW_EMPTY_PASSWORD - value: "yes" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 6379 - name: redis ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-back -spec: - ports: - - port: 6379 - selector: - app: azure-vote-back ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-front -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-front - template: - metadata: - labels: - app: azure-vote-front - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-front - image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 80 - env: - - name: REDIS - value: "azure-vote-back" ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-front -spec: - type: LoadBalancer - ports: - - port: 80 - selector: - app: azure-vote-front - -``` - -# Testing regular comments - -Innovation engine is able to handle comments and actual do fancy things with special comments. - -There are comments you can't see here. - - - - - -# Testing Declaring Environment Variables from Comments -Innovation Engine can declare environment variables via hidden inline comments. This feature is useful for running documents E2E as part of CI/CD - - - - -```azurecli-interactive -echo $MY_VARIABLE -``` - - -# Test Running an Azure Command -```azurecli-interactive -az group exists --name MyResourceGroup -``` - -# Next Steps - -These are the next steps... at some point we need to do something here \ No newline at end of file diff --git a/scenarios/testing/variables.md b/scenarios/testing/variables.md index 5877fd59..81ad004f 100644 --- a/scenarios/testing/variables.md +++ b/scenarios/testing/variables.md @@ -7,6 +7,12 @@ export MY_VAR="Hello, World!" echo $MY_VAR ``` + + +```text +Hello, World! +``` + ## Double variable declaration ```bash @@ -14,11 +20,23 @@ export NEXT_VAR="Hello" && export OTHER_VAR="Hello, World!" echo $NEXT_VAR ``` + + +```text +Hello +``` + ## Double declaration with semicolon ```bash export THIS_VAR="Hello"; export THAT_VAR="Hello, World!" -echo $OTHER_VAR +echo $THAT_VAR +``` + + + +```text +Hello, World! ``` ## Declaration with subshell value @@ -28,6 +46,12 @@ export SUBSHELL_VARIABLE=$(echo "Hello, World!") echo $SUBSHELL_VARIABLE ``` + + +```text +Hello, World! +``` + ## Declaration with other variable in value ```bash @@ -35,3 +59,9 @@ export VAR1="Hello" export VAR2="$VAR1, World!" echo $VAR2 ``` + + + +```text +Hello, World! +``` From 8381867560b27ad4f459510b698f59052b458763 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jun 2024 16:24:23 -0700 Subject: [PATCH 09/27] fix(pipeline): Specify the shell as bash explicitly. --- .github/workflows/scenario-testing.yaml | 1 + scenarios/testing/CommentTest.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 0cabce02..3fcaee30 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -40,6 +40,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build & test all targets. + shell: bash run: | make build-all make test-all WITH_COVERAGE=true diff --git a/scenarios/testing/CommentTest.md b/scenarios/testing/CommentTest.md index 89097c38..eb14ba2f 100644 --- a/scenarios/testing/CommentTest.md +++ b/scenarios/testing/CommentTest.md @@ -20,4 +20,3 @@ world" ```text hello world ``` - From 10ea029a37ab827ce3f57c55241e684f1e7cb526 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jun 2024 16:26:31 -0700 Subject: [PATCH 10/27] fix(pipeline): Specify the shell as bash explicitly. --- .github/workflows/scenario-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 3fcaee30..1a791b4b 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build & test all targets. - shell: bash + shell: 'script -q -e -c "bash --noprofile --norc -eo pipefail {0}"' run: | make build-all make test-all WITH_COVERAGE=true From 99bf280cca9303970beebec5786e15766a0af3d8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jun 2024 16:27:50 -0700 Subject: [PATCH 11/27] fix(pipeline): Specify the shell as bash explicitly. --- .github/workflows/scenario-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 1a791b4b..978e7063 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build & test all targets. - shell: 'script -q -e -c "bash --noprofile --norc -eo pipefail {0}"' + shell: 'script --return --quiet --log-out /dev/null --command "bash -e {0}"' run: | make build-all make test-all WITH_COVERAGE=true From c69a7a6b35d81cbed5e214743c593a4f9c5b0430 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 18 Jun 2024 15:34:58 -0700 Subject: [PATCH 12/27] fix(test): Update regex used to locate resource groups to correctly match the resource group name, update ExecuteBashCommand to be a variable exported from shells/bash.go for easier mocking, and update model tests. --- internal/az/group_test.go | 46 ++++++++++++++++++++++++++++++ internal/engine/test/model.go | 7 +---- internal/engine/test/model_test.go | 12 ++++++-- internal/patterns/regex.go | 2 +- internal/shells/bash.go | 13 +++++---- 5 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 internal/az/group_test.go diff --git a/internal/az/group_test.go b/internal/az/group_test.go new file mode 100644 index 00000000..18bbf826 --- /dev/null +++ b/internal/az/group_test.go @@ -0,0 +1,46 @@ +package az + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindingResourceGroups(t *testing.T) { + testCases := []struct { + resourceGroupString string + expectedResourceGroupName string + }{ + // RG string that ends with a slash + { + resourceGroupString: "resourceGroups/rg1/", + expectedResourceGroupName: "rg1", + }, + // RG string that ends with a space and starts new text + { + resourceGroupString: "resourceGroups/rg1 /subscriptions/", + expectedResourceGroupName: "rg1", + }, + + // RG string that includes nested resources and extraneous text. + { + resourceGroupString: "/subscriptions/9b70acd9-975f-44ba-bad6-255a2c8bda37/resourceGroups/myResourceGroup-rg/providers/Microsoft.ContainerRegistry/registries/mydnsrandomnamebbbhe ffc55a9e-ed2a-4b60-b034-45228dfe7db5 2024-06-11T09:41:36.631310+00:00", + expectedResourceGroupName: "myResourceGroup-rg", + }, + // RG string that is surrounded by quotes. + { + resourceGroupString: `"id": "/subscriptions/0a2c89a7-a44e-4cd0-b6ec-868432ad1d13/resourceGroups/myResourceGroup"`, + expectedResourceGroupName: "myResourceGroup", + }, + // RG string that has no match. + { + resourceGroupString: "NoMatch", + expectedResourceGroupName: "", + }, + } + + for _, tc := range testCases { + resourceGroupName := FindResourceGroupName(tc.resourceGroupString) + assert.Equal(t, tc.expectedResourceGroupName, resourceGroupName) + } +} diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 88dd39e6..075309bf 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -21,11 +21,6 @@ type TestModeCommands struct { quit key.Binding } -// Creating an alias to make it easier to test the execution of bash commands. -// TODO(vmarcella): We should abstract this behind some sort of generic -// interface in the future. -var executeBashCommand = shells.ExecuteBashCommand - // The state required for testing scenarios. type TestModeModel struct { codeBlockState map[int]common.StatefulCodeBlock @@ -145,7 +140,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { if model.resourceGroupName != "" { logging.GlobalLogger.Infof("Attempting to delete the deployed resource group with the name: %s", model.resourceGroupName) command := fmt.Sprintf("az group delete --name %s --yes --no-wait", model.resourceGroupName) - _, err := executeBashCommand( + _, err := shells.ExecuteBashCommand( command, shells.BashCommandConfiguration{ EnvironmentVariables: lib.CopyMap(model.environmentVariables), diff --git a/internal/engine/test/model_test.go b/internal/engine/test/model_test.go index 4836a238..6bd025ff 100644 --- a/internal/engine/test/model_test.go +++ b/internal/engine/test/model_test.go @@ -133,9 +133,13 @@ func TestTestModeModel(t *testing.T) { // the resource group name is empty. m, _ = model.Update(common.Exit(false)()) counter := 0 + // We create a mock function to replace the shells.ExecuteBashCommand function // to make sure that the function is not called. - executeBashCommand = func( + original := shells.ExecuteBashCommand + defer func() { shells.ExecuteBashCommand = original }() + + shells.ExecuteBashCommand = func( command string, config shells.BashCommandConfiguration, ) (shells.CommandOutput, error) { @@ -194,9 +198,13 @@ func TestTestModeModel(t *testing.T) { // the resource group name is not empty. counter := 0 recordedCommand := "" + // We create a mock function to replace the shells.ExecuteBashCommand function // to make sure that the function is called. - executeBashCommand = func( + original := shells.ExecuteBashCommand + defer func() { shells.ExecuteBashCommand = original }() + + shells.ExecuteBashCommand = func( command string, config shells.BashCommandConfiguration, ) (shells.CommandOutput, error) { diff --git a/internal/patterns/regex.go b/internal/patterns/regex.go index 67e5db8c..a67b7241 100644 --- a/internal/patterns/regex.go +++ b/internal/patterns/regex.go @@ -17,5 +17,5 @@ var ( // ARM regex AzResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) - AzResourceGroupName = regexp.MustCompile(`resourceGroups/([^\"]+)`) + AzResourceGroupName = regexp.MustCompile(`resourceGroups/([^\"\\/\ ]+)`) ) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 9dd9d9af..e49b0cec 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -70,7 +70,6 @@ func appendToBashHistory(command string, filePath string) error { } return nil - } // Resets the stored environment variables file. @@ -92,7 +91,6 @@ func filterInvalidKeys(envMap map[string]string) map[string]string { func CleanEnvironmentStateFile() error { env, err := loadEnvFile(environmentStateFile) - if err != nil { return err } @@ -126,9 +124,14 @@ type BashCommandConfiguration struct { WriteToHistory bool } +var ExecuteBashCommand = executeBashCommandImpl + // Executes a bash command and returns the output or error. -func ExecuteBashCommand(command string, config BashCommandConfiguration) (CommandOutput, error) { - var commandWithStateSaved = []string{ +func executeBashCommandImpl( + command string, + config BashCommandConfiguration, +) (CommandOutput, error) { + commandWithStateSaved := []string{ "set -e", command, "IE_LAST_COMMAND_EXIT_CODE=\"$?\"", @@ -176,13 +179,11 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman if config.WriteToHistory { homeDir, err := lib.GetHomeDirectory() - if err != nil { return CommandOutput{}, fmt.Errorf("failed to get home directory: %w", err) } err = appendToBashHistory(command, homeDir+"/.bash_history") - if err != nil { return CommandOutput{}, fmt.Errorf("failed to write command to history: %w", err) } From 925e8a3112f825e37a3f5154d6cd23ceeb9f6880 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Jun 2024 10:56:24 -0700 Subject: [PATCH 13/27] fix(test): Update test mode to include the Error object returned from the FailedCommandMessage to be part of the error output and update CompareCommandOutputs to return better error messages to describe what happened. --- internal/engine/common/outputs.go | 23 ++++++++++++++++------- internal/engine/engine.go | 4 ++-- internal/engine/test/model.go | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/internal/engine/common/outputs.go b/internal/engine/common/outputs.go index 4543227c..22ea96a0 100644 --- a/internal/engine/common/outputs.go +++ b/internal/engine/common/outputs.go @@ -42,18 +42,21 @@ func CompareCommandOutputs( return err } - if !results.AboveThreshold { - return fmt.Errorf( - ui.ErrorMessageStyle.Render("Expected output does not match actual output."), - ) - } - logging.GlobalLogger.Debugf( "Expected Similarity: %f, Actual Similarity: %f", expectedSimilarity, results.Score, ) + if !results.AboveThreshold { + return fmt.Errorf( + ui.ErrorMessageStyle.Render( + "Expected output does not match actual output. Got: %s\n Expected: %s"), + actualOutput, + expectedOutput, + ) + } + return nil } @@ -62,7 +65,13 @@ func CompareCommandOutputs( if expectedSimilarity > score { return fmt.Errorf( - ui.ErrorMessageStyle.Render("Expected output does not match actual output."), + ui.ErrorMessageStyle.Render( + "Expected output does not match actual output.\nGot:\n%s\nExpected:\n%s\nExpected Score:%s\nActualScore:%s", + ), + ui.VerboseStyle.Render(actualOutput), + ui.VerboseStyle.Render(expectedOutput), + ui.VerboseStyle.Render(fmt.Sprintf("%f", expectedSimilarity)), + ui.VerboseStyle.Render(fmt.Sprintf("%f", score)), ) } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index bd5e4ee7..7707a37c 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -72,15 +72,15 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { var finalModel tea.Model finalModel, err = common.Program.Run() - var ok bool // TODO(vmarcella): After testing is complete, we should generate a report. - model, ok = finalModel.(test.TestModeModel) + model, ok := finalModel.(test.TestModeModel) if !ok { return fmt.Errorf("failed to cast tea.Model to TestModeModel") } + fmt.Println(strings.Join(model.CommandLines, "\n")) return err diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 075309bf..73f72faa 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -129,7 +129,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { codeBlockState.Success = false model.codeBlockState[step] = codeBlockState - model.CommandLines = append(model.CommandLines, codeBlockState.StdErr) + model.CommandLines = append(model.CommandLines, codeBlockState.StdErr+message.Error.Error()) commands = append(commands, common.Exit(true)) From 8240599b58f4541e6263f3cb02d7c5b5ba6e39a1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Jun 2024 13:48:43 -0700 Subject: [PATCH 14/27] feat(test): Update test coverage to include all modules as part of the cover profile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 54408dcb..ff079776 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ test-all: @go clean -testcache ifeq ($(WITH_COVERAGE), true) @echo "Running all tests with coverage..." - @go test -v -coverprofile=coverage.out ./... + @go test -v -coverpkg=./... -coverprofile=coverage.out ./... @go tool cover -html=coverage.out -o coverage.html else @echo "Running all tests..." From aed8dc96b220e7a48eec1d15eabbc502f180e3f0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Jun 2024 15:05:50 -0700 Subject: [PATCH 15/27] fix(test): Update the viewport to go to the bottom every time a command finishes executing. --- internal/engine/test/model.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 73f72faa..8c7f2647 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -49,6 +49,8 @@ func (model TestModeModel) Init() tea.Cmd { func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { var commands []tea.Cmd + viewportContentUpdated := false + switch message := message.(type) { case tea.WindowSizeMsg: @@ -87,6 +89,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { } } model.CommandLines = append(model.CommandLines, codeBlockState.StdOut) + viewportContentUpdated = true // Increment the codeblock and update the viewport content. model.currentCodeBlock++ @@ -130,6 +133,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.codeBlockState[step] = codeBlockState model.CommandLines = append(model.CommandLines, codeBlockState.StdErr+message.Error.Error()) + viewportContentUpdated = true commands = append(commands, common.Exit(true)) @@ -168,6 +172,10 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.components.commandViewport.SetContent(strings.Join(model.CommandLines, "\n")) + if viewportContentUpdated { + model.components.commandViewport.GotoBottom() + } + // Update all the viewports and append resulting commands. var command tea.Cmd From f4b831c4f3a418fff1f1e8122462fd3834f0f245 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 15:32:19 -0700 Subject: [PATCH 16/27] [update] ie test to take in an environment flag and only use altscreen when not rendering in CI. --- cmd/ie/commands/test.go | 2 ++ internal/engine/engine.go | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 71827fb8..c022a7ca 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -35,6 +35,7 @@ var testCommand = &cobra.Command{ verbose, _ := cmd.Flags().GetBool("verbose") subscription, _ := cmd.Flags().GetString("subscription") workingDirectory, _ := cmd.Flags().GetString("working-directory") + environment, _ := cmd.Flags().GetString("environment") innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, @@ -42,6 +43,7 @@ var testCommand = &cobra.Command{ Subscription: subscription, CorrelationId: "", WorkingDirectory: workingDirectory, + Environment: environment, }) if err != nil { logging.GlobalLogger.Errorf("Error creating engine %s", err) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 7707a37c..74fb3bb7 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -39,7 +39,7 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { }, nil } -// Executes a deployment scenario. +// Executes a markdown scenario. func (e *Engine) ExecuteScenario(scenario *common.Scenario) error { return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) @@ -51,7 +51,8 @@ func (e *Engine) ExecuteScenario(scenario *common.Scenario) error { }) } -// Validates a deployment scenario. +// Executes a scenario in testing moe. This mode goes over each code block +// and executes it without user interaction. func (e *Engine) TestScenario(scenario *common.Scenario) error { return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) @@ -68,7 +69,17 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { return err } - common.Program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) + var flags []tea.ProgramOption + if environments.EnvironmentsCI == e.Configuration.Environment { + flags = append(flags, tea.WithoutRenderer()) + } else { + flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion()) + } + + fmt.Println(e.Configuration.Environment) + fmt.Println(flags) + + common.Program = tea.NewProgram(model, flags...) var finalModel tea.Model finalModel, err = common.Program.Run() From 7a2a4c2a86dfa4f14b17fb24a5300b6a4b8239bf Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 15:36:47 -0700 Subject: [PATCH 17/27] [update] makefile to accept environment parameter and specify it in the pipeline. --- .github/workflows/scenario-testing.yaml | 3 +-- Makefile | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 978e7063..a3d9da04 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -40,11 +40,10 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build & test all targets. - shell: 'script --return --quiet --log-out /dev/null --command "bash -e {0}"' run: | make build-all make test-all WITH_COVERAGE=true - make test-local-scenarios + make test-local-scenarios ENVIRONMENT=ci - name: Upload test coverage uses: actions/upload-artifact@v2 if: github.event_name == 'pull_request' diff --git a/Makefile b/Makefile index ff079776..4bb3416d 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,6 @@ install-ie: # ------------------------------ Test targets ---------------------------------- WITH_COVERAGE := false - test-all: @go clean -testcache ifeq ($(WITH_COVERAGE), true) @@ -38,12 +37,13 @@ endif SUBSCRIPTION ?= 00000000-0000-0000-0000-000000000000 SCENARIO ?= ./README.md WORKING_DIRECTORY ?= $(PWD) +ENVIRONMENT ?= local test-scenario: @echo "Running scenario $(SCENARIO)" ifeq ($(SUBSCRIPTION), 00000000-0000-0000-0000-000000000000) - $(IE_BINARY) test $(SCENARIO) --working-directory $(WORKING_DIRECTORY) + $(IE_BINARY) test $(SCENARIO) --working-directory $(WORKING_DIRECTORY) --environment $(ENVIRONMENT) else - $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) + $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) --enviroment $(ENVIRONMENT) endif test-scenarios: From 6d852f5088d123d5a4caf3b5c9f8c9e89f685295 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 15:58:30 -0700 Subject: [PATCH 18/27] [update] stdin to be nil when the command isn't interactive. --- cmd/ie/commands/test.go | 2 +- internal/shells/bash.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index c022a7ca..87fe2f8e 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -24,7 +24,7 @@ func init() { var testCommand = &cobra.Command{ Use: "test", Args: cobra.MinimumNArgs(1), - Short: "Test document commands against it's expected outputs.", + Short: "Test document commands against their expected outputs.", Run: func(cmd *cobra.Command, args []string) { markdownFile := args[0] if markdownFile == "" { diff --git a/internal/shells/bash.go b/internal/shells/bash.go index e49b0cec..f412e64e 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -153,6 +153,7 @@ func executeBashCommandImpl( } else { commandToExecute.Stdout = &stdoutBuffer commandToExecute.Stderr = &stderrBuffer + commandToExecute.Stdin = nil } if config.InheritEnvironment { From 1e4a073e72d9d9b0817186445abf66e72779928c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:01:32 -0700 Subject: [PATCH 19/27] [remove] check. --- .github/workflows/scenario-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index a3d9da04..4c8477f2 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -66,6 +66,6 @@ jobs: apk add --no-cache make git openssh openssl helm curl jq make test-upstream-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} - name: Display ie.log file - if: (success() || failure()) && github.event_name != 'pull_request' + if: (success() || failure()) run: | cat ie.log From bfb82fb7c1cd78f066aab11ce9685c6c60e4a452 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:12:31 -0700 Subject: [PATCH 20/27] [update] tea flags when the environment is CI. --- internal/engine/engine.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 74fb3bb7..b345f5e2 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "os" "strings" "github.com/Azure/InnovationEngine/internal/az" @@ -71,7 +72,7 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { var flags []tea.ProgramOption if environments.EnvironmentsCI == e.Configuration.Environment { - flags = append(flags, tea.WithoutRenderer()) + flags = append(flags, tea.WithOutput(os.Stdout), tea.WithInput(os.Stdin)) } else { flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion()) } From c5e4fb3b6cbb1af41f22283730ecffb4241639c0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:14:37 -0700 Subject: [PATCH 21/27] [remove] the renderer when in CI. --- internal/engine/engine.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index b345f5e2..c6cef451 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -72,7 +72,12 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { var flags []tea.ProgramOption if environments.EnvironmentsCI == e.Configuration.Environment { - flags = append(flags, tea.WithOutput(os.Stdout), tea.WithInput(os.Stdin)) + flags = append( + flags, + tea.WithoutRenderer(), + tea.WithOutput(os.Stdout), + tea.WithInput(os.Stdin), + ) } else { flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion()) } From 2772487ec6834cf8f58f69df7e00ca821120f128 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:31:55 -0700 Subject: [PATCH 22/27] [update] the failure reporting so that how the scenario failed is made clear. --- internal/engine/engine.go | 9 +++++---- internal/engine/environments/environments.go | 15 ++++++++++----- internal/engine/test/model.go | 17 +++++++++++++++++ internal/shells/bash.go | 1 - 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index c6cef451..651197a1 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,6 +1,7 @@ package engine import ( + "errors" "fmt" "os" "strings" @@ -82,9 +83,6 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion()) } - fmt.Println(e.Configuration.Environment) - fmt.Println(flags) - common.Program = tea.NewProgram(model, flags...) var finalModel tea.Model @@ -95,8 +93,11 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { model, ok := finalModel.(test.TestModeModel) if !ok { - return fmt.Errorf("failed to cast tea.Model to TestModeModel") + err = errors.Join(err, fmt.Errorf("failed to cast tea.Model to TestModeModel")) + return err + } + err = errors.Join(err, model.GetFailure()) fmt.Println(strings.Join(model.CommandLines, "\n")) diff --git a/internal/engine/environments/environments.go b/internal/engine/environments/environments.go index c6c9dc45..504ae033 100644 --- a/internal/engine/environments/environments.go +++ b/internal/engine/environments/environments.go @@ -1,16 +1,21 @@ package environments const ( - EnvironmentsLocal = "local" - EnvironmentsCI = "ci" - EnvironmentsOCD = "ocd" - EnvironmentsAzure = "azure" + EnvironmentsLocal = "local" + EnvironmentsCI = "ci" + EnvironmentsGithubActions = "github-actions" + EnvironmentsOCD = "ocd" + EnvironmentsAzure = "azure" ) // Check if the environment is valid. func IsValidEnvironment(environment string) bool { switch environment { - case EnvironmentsLocal, EnvironmentsCI, EnvironmentsOCD, EnvironmentsAzure: + case EnvironmentsLocal, + EnvironmentsCI, + EnvironmentsGithubActions, + EnvironmentsOCD, + EnvironmentsAzure: return true default: return false diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 8c7f2647..35bbe0ea 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -37,6 +37,23 @@ type TestModeModel struct { CommandLines []string } +// Obtains the last codeblock that the scenario was on before it failed. +// If the scenario was completed successfully, then it returns nil. +func (model TestModeModel) GetFailure() error { + if model.scenarioCompleted { + return nil + } + + failedCodeBlock := model.codeBlockState[model.currentCodeBlock] + return fmt.Errorf( + "failed to execute code block %d on step %d.\nError: %s\nStdErr: %s", + failedCodeBlock.CodeBlockNumber, + failedCodeBlock.StepNumber, + failedCodeBlock.StdErr, + failedCodeBlock.Error, + ) +} + // Init the test mode model by executing the first code block. func (model TestModeModel) Init() tea.Cmd { return common.ExecuteCodeBlockAsync( diff --git a/internal/shells/bash.go b/internal/shells/bash.go index f412e64e..e49b0cec 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -153,7 +153,6 @@ func executeBashCommandImpl( } else { commandToExecute.Stdout = &stdoutBuffer commandToExecute.Stderr = &stderrBuffer - commandToExecute.Stdin = nil } if config.InheritEnvironment { From 41d5701b954e7298b5f2699e9e7bddfde2a93dd1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:35:37 -0700 Subject: [PATCH 23/27] [fix] the codeblocks to set the error when they've failed. --- internal/engine/test/model.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/engine/test/model.go b/internal/engine/test/model.go index 35bbe0ea..57b97183 100644 --- a/internal/engine/test/model.go +++ b/internal/engine/test/model.go @@ -49,8 +49,8 @@ func (model TestModeModel) GetFailure() error { "failed to execute code block %d on step %d.\nError: %s\nStdErr: %s", failedCodeBlock.CodeBlockNumber, failedCodeBlock.StepNumber, - failedCodeBlock.StdErr, failedCodeBlock.Error, + failedCodeBlock.StdErr, ) } @@ -146,6 +146,7 @@ func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { codeBlockState := model.codeBlockState[step] codeBlockState.StdOut = message.StdOut codeBlockState.StdErr = message.StdErr + codeBlockState.Error = message.Error codeBlockState.Success = false model.codeBlockState[step] = codeBlockState From d0703c0d90173066615f2b554cb3fc7dc4d8be74 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:37:44 -0700 Subject: [PATCH 24/27] [remove] ci from being an environment in favor of github-action, as we can't assume that all cis will not have ttys. --- .github/workflows/scenario-testing.yaml | 2 +- internal/engine/engine.go | 2 +- internal/engine/environments/environments.go | 12 +++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 4c8477f2..7943d0eb 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -43,7 +43,7 @@ jobs: run: | make build-all make test-all WITH_COVERAGE=true - make test-local-scenarios ENVIRONMENT=ci + make test-local-scenarios ENVIRONMENT=github-action - name: Upload test coverage uses: actions/upload-artifact@v2 if: github.event_name == 'pull_request' diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 651197a1..04c9a7fd 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -72,7 +72,7 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { } var flags []tea.ProgramOption - if environments.EnvironmentsCI == e.Configuration.Environment { + if environments.EnvironmentsGithubActions == e.Configuration.Environment { flags = append( flags, tea.WithoutRenderer(), diff --git a/internal/engine/environments/environments.go b/internal/engine/environments/environments.go index 504ae033..6baa9cbf 100644 --- a/internal/engine/environments/environments.go +++ b/internal/engine/environments/environments.go @@ -1,19 +1,17 @@ package environments const ( - EnvironmentsLocal = "local" - EnvironmentsCI = "ci" - EnvironmentsGithubActions = "github-actions" - EnvironmentsOCD = "ocd" - EnvironmentsAzure = "azure" + EnvironmentsLocal = "local" + EnvironmentsGithubAction = "github-action" + EnvironmentsOCD = "ocd" + EnvironmentsAzure = "azure" ) // Check if the environment is valid. func IsValidEnvironment(environment string) bool { switch environment { case EnvironmentsLocal, - EnvironmentsCI, - EnvironmentsGithubActions, + EnvironmentsGithubAction, EnvironmentsOCD, EnvironmentsAzure: return true From f22f2e332ee778d948c9a5fd0103408916d638f2 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 21 Jun 2024 16:39:38 -0700 Subject: [PATCH 25/27] [fix] reference. --- internal/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 04c9a7fd..a376fc13 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -72,7 +72,7 @@ func (e *Engine) TestScenario(scenario *common.Scenario) error { } var flags []tea.ProgramOption - if environments.EnvironmentsGithubActions == e.Configuration.Environment { + if environments.EnvironmentsGithubAction == e.Configuration.Environment { flags = append( flags, tea.WithoutRenderer(), From e044a667c129113eefe1034ed151f0136d564770 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Jun 2024 09:42:47 -0700 Subject: [PATCH 26/27] [update] test to pass. --- scenarios/testing/fuzzyMatchTest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/testing/fuzzyMatchTest.md b/scenarios/testing/fuzzyMatchTest.md index d0e84e84..1a488bc1 100644 --- a/scenarios/testing/fuzzyMatchTest.md +++ b/scenarios/testing/fuzzyMatchTest.md @@ -24,7 +24,7 @@ world" ```text -world Hello +Hello world ``` # Code block From d7ded386174843501727d72d95f233d58460f761 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Jun 2024 13:36:41 -0700 Subject: [PATCH 27/27] [remove] bad similarity for now. Will track in a future unit test. --- .../testing/.null-ls_369210_fuzzyMatchTest.md | 58 +++++++++++++++++++ scenarios/testing/fuzzyMatchTest.md | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 scenarios/testing/.null-ls_369210_fuzzyMatchTest.md diff --git a/scenarios/testing/.null-ls_369210_fuzzyMatchTest.md b/scenarios/testing/.null-ls_369210_fuzzyMatchTest.md new file mode 100644 index 00000000..fbae0352 --- /dev/null +++ b/scenarios/testing/.null-ls_369210_fuzzyMatchTest.md @@ -0,0 +1,58 @@ +# Testing multi Line code block + +```azurecli-interactive +echo "Hello World" +``` + +This is what the expected output should be + + + +```text +Hello world +``` + +# Testing multi Line code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Output Should Fail + + + +```text +Hello world +``` + +# Code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Output Should Pass + + + +```text +Hello world +``` + +# Code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Bad similarity - should fail + + + +```text +Hello world +``` diff --git a/scenarios/testing/fuzzyMatchTest.md b/scenarios/testing/fuzzyMatchTest.md index 1a488bc1..fbae0352 100644 --- a/scenarios/testing/fuzzyMatchTest.md +++ b/scenarios/testing/fuzzyMatchTest.md @@ -51,7 +51,7 @@ world" # Bad similarity - should fail - + ```text Hello world