diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index e0aa5ed0..7943d0eb 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 ENVIRONMENT=github-action - name: Upload test coverage uses: actions/upload-artifact@v2 if: github.event_name == 'pull_request' @@ -65,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 diff --git a/Makefile b/Makefile index 82e12250..4bb3416d 100644 --- a/Makefile +++ b/Makefile @@ -22,12 +22,11 @@ install-ie: # ------------------------------ Test targets ---------------------------------- WITH_COVERAGE := false - 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..." @@ -38,9 +37,14 @@ endif SUBSCRIPTION ?= 00000000-0000-0000-0000-000000000000 SCENARIO ?= ./README.md WORKING_DIRECTORY ?= $(PWD) +ENVIRONMENT ?= local test-scenario: @echo "Running scenario $(SCENARIO)" - $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) +ifeq ($(SUBSCRIPTION), 00000000-0000-0000-0000-000000000000) + $(IE_BINARY) test $(SCENARIO) --working-directory $(WORKING_DIRECTORY) --environment $(ENVIRONMENT) +else + $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) --enviroment $(ENVIRONMENT) +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/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..87fe2f8e 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" ) @@ -23,9 +24,8 @@ 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 == "" { cmd.Help() @@ -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,15 +43,15 @@ var testCommand = &cobra.Command{ Subscription: subscription, CorrelationId: "", WorkingDirectory: workingDirectory, + Environment: environment, }) - 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/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/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/common/codeblock.go b/internal/engine/common/codeblock.go new file mode 100644 index 00000000..b1340436 --- /dev/null +++ b/internal/engine/common/codeblock.go @@ -0,0 +1,22 @@ +package common + +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 +} + +// 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/commands.go b/internal/engine/common/commands.go similarity index 82% rename from internal/engine/commands.go rename to internal/engine/common/commands.go index 52db6ab9..2e550883 100644 --- a/internal/engine/commands.go +++ b/internal/engine/common/commands.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" @@ -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 { @@ -52,7 +62,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,16 +96,19 @@ 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, 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() + Program.RestoreTerminal() if err != nil { return FailedCommandMessage{ @@ -113,7 +126,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", @@ -124,13 +137,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 72% rename from internal/engine/common.go rename to internal/engine/common/outputs.go index b072c44b..22ea96a0 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, @@ -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/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 149bde77..db01dfe9 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/common/scenario.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" diff --git a/internal/engine/scenario_test.go b/internal/engine/common/scenario_test.go similarity index 95% rename from internal/engine/scenario_test.go rename to internal/engine/common/scenario_test.go index d38a8f76..8a06015f 100644 --- a/internal/engine/scenario_test.go +++ b/internal/engine/common/scenario_test.go @@ -1,4 +1,4 @@ -package engine +package common import ( "fmt" @@ -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/engine.go b/internal/engine/engine.go index caf71753..a376fc13 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,11 +1,16 @@ package engine import ( + "errors" "fmt" + "os" "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" @@ -36,8 +41,8 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { }, nil } -// Executes a deployment scenario. -func (e *Engine) ExecuteScenario(scenario *Scenario) error { +// 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) @@ -48,29 +53,70 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { }) } -// Validates a deployment scenario. -func (e *Engine) TestScenario(scenario *Scenario) error { +// 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) + 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 + } + + var flags []tea.ProgramOption + if environments.EnvironmentsGithubAction == e.Configuration.Environment { + flags = append( + flags, + tea.WithoutRenderer(), + tea.WithOutput(os.Stdout), + tea.WithInput(os.Stdin), + ) + } else { + flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion()) + } + + common.Program = tea.NewProgram(model, flags...) + + var finalModel tea.Model + finalModel, err = common.Program.Run() + + // TODO(vmarcella): After testing is complete, we should generate a report. + + model, ok := finalModel.(test.TestModeModel) + + if !ok { + 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")) - // Test the steps - fmt.Println(ui.ScenarioTitleStyle.Render(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 +124,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 +138,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/environments.go b/internal/engine/environments/environments.go index c6c9dc45..6baa9cbf 100644 --- a/internal/engine/environments/environments.go +++ b/internal/engine/environments/environments.go @@ -1,16 +1,19 @@ package environments const ( - EnvironmentsLocal = "local" - EnvironmentsCI = "ci" - 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, EnvironmentsOCD, EnvironmentsAzure: + case EnvironmentsLocal, + EnvironmentsGithubAction, + EnvironmentsOCD, + EnvironmentsAzure: return true default: return false 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 89% rename from internal/engine/interactive.go rename to internal/engine/interactive/interactive.go index 156615d1..96639d09 100644 --- a/internal/engine/interactive.go +++ b/internal/engine/interactive/interactive.go @@ -1,4 +1,4 @@ -package engine +package interactive import ( "fmt" @@ -6,10 +6,10 @@ 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" - "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 +28,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 +37,7 @@ type interactiveModeComponents struct { type InteractiveModeModel struct { azureStatus environments.AzureDeploymentStatus - codeBlockState map[int]CodeBlockState + codeBlockState map[int]common.StatefulCodeBlock commands InteractiveModeCommands currentCodeBlock int env map[string]string @@ -64,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 })) } @@ -160,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), )) @@ -238,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 @@ -262,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++ @@ -271,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. @@ -297,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. @@ -316,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 @@ -326,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 @@ -397,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 @@ -490,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 @@ -508,13 +496,13 @@ func NewInteractiveModeModel( azureStatus := environments.NewAzureDeploymentStatus() azureStatus.CurrentStep = 1 totalCodeBlocks := 0 - codeBlockState := make(map[int]CodeBlockState) + 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 } @@ -526,7 +514,7 @@ func NewInteractiveModeModel( Description: block.Description, }) - codeBlockState[totalCodeBlocks] = CodeBlockState{ + codeBlockState[totalCodeBlocks] = common.StatefulCodeBlock{ StepName: step.Name, CodeBlock: block, StepNumber: stepNumber, @@ -574,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/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..57b97183 --- /dev/null +++ b/internal/engine/test/model.go @@ -0,0 +1,276 @@ +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" + 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 { + 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 +} + +// 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.Error, + failedCodeBlock.StdErr, + ) +} + +// 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.environmentVariables, + ) +} + +// Update the test mode model. +func (model TestModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { + var commands []tea.Cmd + + viewportContentUpdated := false + + switch message := message.(type) { + + case tea.WindowSizeMsg: + 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.Width, message.Height) + } + + case tea.KeyMsg: + model, commands = handleUserInput(model, message) + + case common.SuccessfulCommandMessage: + // Handle successful command executions + 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) + viewportContentUpdated = true + + // 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 the scenario has been completed, we need to update the azure + // status and quit the program. else, + if model.currentCodeBlock == len(model.codeBlockState) { + logging.GlobalLogger.Infof("The last codeblock was executed. Requesting to exit test mode...") + commands = append( + commands, + 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.environmentVariables), + ) + } + + case common.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.Error = message.Error + codeBlockState.Success = false + + model.codeBlockState[step] = codeBlockState + model.CommandLines = append(model.CommandLines, codeBlockState.StdErr+message.Error.Error()) + viewportContentUpdated = true + + commands = append(commands, common.Exit(true)) + + 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( + command, + shells.BashCommandConfiguration{ + EnvironmentVariables: lib.CopyMap(model.environmentVariables), + 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.") + } + + } + + // 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")) + + if viewportContentUpdated { + model.components.commandViewport.GotoBottom() + } + + // 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, + subscription string, + environment string, + steps []common.Step, + env map[string]string, +) (TestModeModel, error) { + 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) + 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 { + for blockNumber, block := range step.CodeBlocks { + + codeBlockState[totalCodeBlocks] = common.StatefulCodeBlock{ + StepName: step.Name, + CodeBlock: block, + StepNumber: stepNumber, + CodeBlockNumber: blockNumber, + StdOut: "", + StdErr: "", + Error: nil, + Success: false, + } + + totalCodeBlocks += 1 + } + } + + 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."), + ), + }, + 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 new file mode 100644 index 00000000..6bd025ff --- /dev/null +++ b/internal/engine/test/model_test.go @@ -0,0 +1,227 @@ +package test + +import ( + "testing" + + "github.com/Azure/InnovationEngine/internal/engine/common" + "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/shells" + "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) + assert.Error(t, err) + }) + + 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) + assert.NoError(t, err) + + assert.Equal(t, "test", model.scenarioTitle) + + assert.Equal(t, "", model.components.commandViewport.View()) + assert.Equal(t, map[string]string{}, model.environmentVariables) + }) + + 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) + assert.NoError(t, err) + + assert.Equal(t, 0, model.currentCodeBlock) + assert.Equal(t, 1, len(model.codeBlockState)) + + 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) + + 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) + } + }, + ) + + 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. + original := shells.ExecuteBashCommand + defer func() { shells.ExecuteBashCommand = original }() + + shells.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. + original := shells.ExecuteBashCommand + defer func() { shells.ExecuteBashCommand = original }() + + shells.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/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())) 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) } 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/CommentTest.md b/scenarios/testing/CommentTest.md index 05627f9b..eb14ba2f 100644 --- a/scenarios/testing/CommentTest.md +++ b/scenarios/testing/CommentTest.md @@ -1,22 +1,22 @@ - - # 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..fbae0352 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,9 +20,11 @@ world" ``` # Output Should Fail + + ```text -world Hello +Hello world ``` # Code block @@ -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! +```