Skip to content

Commit 37862b0

Browse files
authored
feat(test): New UI implementation for ie test & refactor various parts of the engine (#198)
This PR: * Updates `ie test` to use bubbletea for rendering it's output, similar to `ie interactive` and soon `ie execute`. * Refactors `ie interactive` and `ie test` implementations to exist in their own folders inside of `engine` (Which will be renamed to `ie` in the future). * Refactors code commonly used across execute, interactive, and test modes into a new package found in `internal/engine/common`. * Decoupled models from requiring a pointer to `Engine` as a parameter in preparation for when it will be deprecated in the future. * Reimplements `shells.ExecuteCodeBlock` to be a variable alias of `shells.executeCodeBlockImpl`. This allows for us to easily mock the the `ExecuteCodeBlock` implementation easily in tests. This allows us to do things like track calls to commands, record what commands were failed, mimic failures from executing commands, etc without having to actually execute commands. This is particularly useful in cases where we want to test the behavior of executing an Azure CLI command without actually executing the command itself. * Fixed an issue with the regex used to locate resource groups inside of command outputs and added tests to ensure that it works. * Adds a new environment `github-action`, specifically for running `ie test` inside of github actions runners. If this flag is not supplied, `ie test` will crash due to bubbletea attempting to open tty when there are no ttys available. (See actions/runner#241 for more information on GH actions not providing a TTY). *
1 parent a3f6cb8 commit 37862b0

36 files changed

+985
-616
lines changed

.github/workflows/scenario-testing.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ jobs:
3939
environment: ScenarioTesting
4040
steps:
4141
- uses: actions/checkout@v2
42-
- name: Build all targets.
42+
- name: Build & test all targets.
4343
run: |
4444
make build-all
4545
make test-all WITH_COVERAGE=true
46+
make test-local-scenarios ENVIRONMENT=github-action
4647
- name: Upload test coverage
4748
uses: actions/upload-artifact@v2
4849
if: github.event_name == 'pull_request'
@@ -65,6 +66,6 @@ jobs:
6566
apk add --no-cache make git openssh openssl helm curl jq
6667
make test-upstream-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }}
6768
- name: Display ie.log file
68-
if: (success() || failure()) && github.event_name != 'pull_request'
69+
if: (success() || failure())
6970
run: |
7071
cat ie.log

Makefile

+13-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ install-ie:
2222
# ------------------------------ Test targets ----------------------------------
2323

2424
WITH_COVERAGE := false
25-
2625
test-all:
2726
@go clean -testcache
2827
ifeq ($(WITH_COVERAGE), true)
2928
@echo "Running all tests with coverage..."
30-
@go test -v -coverprofile=coverage.out ./...
29+
@go test -v -coverpkg=./... -coverprofile=coverage.out ./...
3130
@go tool cover -html=coverage.out -o coverage.html
3231
else
3332
@echo "Running all tests..."
@@ -38,16 +37,27 @@ endif
3837
SUBSCRIPTION ?= 00000000-0000-0000-0000-000000000000
3938
SCENARIO ?= ./README.md
4039
WORKING_DIRECTORY ?= $(PWD)
40+
ENVIRONMENT ?= local
4141
test-scenario:
4242
@echo "Running scenario $(SCENARIO)"
43-
$(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY)
43+
ifeq ($(SUBSCRIPTION), 00000000-0000-0000-0000-000000000000)
44+
$(IE_BINARY) test $(SCENARIO) --working-directory $(WORKING_DIRECTORY) --environment $(ENVIRONMENT)
45+
else
46+
$(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) --working-directory $(WORKING_DIRECTORY) --enviroment $(ENVIRONMENT)
47+
endif
4448

4549
test-scenarios:
4650
@echo "Testing out the scenarios"
4751
for dir in ./scenarios/ocd/*/; do \
4852
($(MAKE) test-scenario SCENARIO="$${dir}README.md" SUBCRIPTION="$(SUBSCRIPTION)") || exit $$?; \
4953
done
5054

55+
test-local-scenarios:
56+
@echo "Testing out the local scenarios"
57+
for file in ./scenarios/testing/*.md; do \
58+
($(MAKE) test-scenario SCENARIO="$${file}") || exit $$?; \
59+
done
60+
5161
test-upstream-scenarios:
5262
@echo "Pulling the upstream scenarios"
5363
@git config --global --add safe.directory /home/runner/work/InnovationEngine/InnovationEngine

cmd/ie/commands/execute.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/Azure/InnovationEngine/internal/engine"
9+
"github.com/Azure/InnovationEngine/internal/engine/common"
910
"github.com/Azure/InnovationEngine/internal/logging"
1011
"github.com/spf13/cobra"
1112
)
@@ -92,7 +93,7 @@ var executeCommand = &cobra.Command{
9293
}
9394

9495
// Parse the markdown file and create a scenario
95-
scenario, err := engine.CreateScenarioFromMarkdown(
96+
scenario, err := common.CreateScenarioFromMarkdown(
9697
markdownFile,
9798
[]string{"bash", "azurecli", "azurecli-interactive", "terraform"},
9899
cliEnvironmentVariables,
@@ -112,7 +113,6 @@ var executeCommand = &cobra.Command{
112113
WorkingDirectory: workingDirectory,
113114
RenderValues: renderValues,
114115
})
115-
116116
if err != nil {
117117
logging.GlobalLogger.Errorf("Error creating engine: %s", err)
118118
fmt.Printf("Error creating engine: %s", err)

cmd/ie/commands/inspect.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"os"
66
"strings"
77

8-
"github.com/Azure/InnovationEngine/internal/engine"
8+
"github.com/Azure/InnovationEngine/internal/engine/common"
99
"github.com/Azure/InnovationEngine/internal/logging"
1010
"github.com/Azure/InnovationEngine/internal/ui"
1111
"github.com/spf13/cobra"
@@ -59,7 +59,7 @@ var inspectCommand = &cobra.Command{
5959
cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1]
6060
}
6161
// Parse the markdown file and create a scenario
62-
scenario, err := engine.CreateScenarioFromMarkdown(
62+
scenario, err := common.CreateScenarioFromMarkdown(
6363
markdownFile,
6464
[]string{"bash", "azurecli", "azurecli-inspect", "terraform"},
6565
cliEnvironmentVariables,
@@ -104,6 +104,5 @@ var inspectCommand = &cobra.Command{
104104
fmt.Println()
105105
}
106106
}
107-
108107
},
109108
}

cmd/ie/commands/interactive.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/Azure/InnovationEngine/internal/engine"
9+
"github.com/Azure/InnovationEngine/internal/engine/common"
910
"github.com/Azure/InnovationEngine/internal/logging"
1011
"github.com/spf13/cobra"
1112
)
@@ -69,7 +70,7 @@ var interactiveCommand = &cobra.Command{
6970
cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1]
7071
}
7172
// Parse the markdown file and create a scenario
72-
scenario, err := engine.CreateScenarioFromMarkdown(
73+
scenario, err := common.CreateScenarioFromMarkdown(
7374
markdownFile,
7475
[]string{"bash", "azurecli", "azurecli-interactive", "terraform"},
7576
cliEnvironmentVariables,
@@ -89,7 +90,6 @@ var interactiveCommand = &cobra.Command{
8990
WorkingDirectory: workingDirectory,
9091
RenderValues: renderValues,
9192
})
92-
9393
if err != nil {
9494
logging.GlobalLogger.Errorf("Error creating engine: %s", err)
9595
fmt.Printf("Error creating engine: %s", err)

cmd/ie/commands/test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/Azure/InnovationEngine/internal/engine"
8+
"github.com/Azure/InnovationEngine/internal/engine/common"
89
"github.com/Azure/InnovationEngine/internal/logging"
910
"github.com/spf13/cobra"
1011
)
@@ -23,9 +24,8 @@ func init() {
2324
var testCommand = &cobra.Command{
2425
Use: "test",
2526
Args: cobra.MinimumNArgs(1),
26-
Short: "Test document commands against it's expected outputs.",
27+
Short: "Test document commands against their expected outputs.",
2728
Run: func(cmd *cobra.Command, args []string) {
28-
2929
markdownFile := args[0]
3030
if markdownFile == "" {
3131
cmd.Help()
@@ -35,22 +35,23 @@ var testCommand = &cobra.Command{
3535
verbose, _ := cmd.Flags().GetBool("verbose")
3636
subscription, _ := cmd.Flags().GetString("subscription")
3737
workingDirectory, _ := cmd.Flags().GetString("working-directory")
38+
environment, _ := cmd.Flags().GetString("environment")
3839

3940
innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{
4041
Verbose: verbose,
4142
DoNotDelete: false,
4243
Subscription: subscription,
4344
CorrelationId: "",
4445
WorkingDirectory: workingDirectory,
46+
Environment: environment,
4547
})
46-
4748
if err != nil {
4849
logging.GlobalLogger.Errorf("Error creating engine %s", err)
4950
fmt.Printf("Error creating engine %s", err)
5051
os.Exit(1)
5152
}
5253

53-
scenario, err := engine.CreateScenarioFromMarkdown(
54+
scenario, err := common.CreateScenarioFromMarkdown(
5455
markdownFile,
5556
[]string{"bash", "azurecli", "azurecli-interactive", "terraform"},
5657
nil,

cmd/ie/commands/to-bash.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"fmt"
77
"strings"
88

9-
"github.com/Azure/InnovationEngine/internal/engine"
9+
"github.com/Azure/InnovationEngine/internal/engine/common"
1010
"github.com/Azure/InnovationEngine/internal/engine/environments"
1111
"github.com/Azure/InnovationEngine/internal/logging"
1212
"github.com/spf13/cobra"
@@ -50,11 +50,10 @@ var toBashCommand = &cobra.Command{
5050
}
5151

5252
// Parse the markdown file and create a scenario
53-
scenario, err := engine.CreateScenarioFromMarkdown(
53+
scenario, err := common.CreateScenarioFromMarkdown(
5454
markdownFile,
5555
[]string{"bash", "azurecli", "azurecli-interactive", "terraform"},
5656
cliEnvironmentVariables)
57-
5857
if err != nil {
5958
logging.GlobalLogger.Errorf("Error creating scenario: %s", err)
6059
fmt.Printf("Error creating scenario: %s", err)
@@ -66,7 +65,6 @@ var toBashCommand = &cobra.Command{
6665
if environments.IsAzureEnvironment(environment) {
6766
script := AzureScript{Script: scenario.ToShellScript()}
6867
scriptJson, err := json.Marshal(script)
69-
7068
if err != nil {
7169
logging.GlobalLogger.Errorf("Error converting to json: %s", err)
7270
fmt.Printf("Error converting to json: %s", err)
@@ -79,7 +77,6 @@ var toBashCommand = &cobra.Command{
7977
}
8078

8179
return nil
82-
8380
},
8481
}
8582

internal/az/group.go

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func FindAllDeployedResourceURIs(resourceGroup string) []string {
1717
WriteToHistory: true,
1818
},
1919
)
20-
2120
if err != nil {
2221
logging.GlobalLogger.Error("Failed to list deployments", err)
2322
}

internal/az/group_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package az
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestFindingResourceGroups(t *testing.T) {
10+
testCases := []struct {
11+
resourceGroupString string
12+
expectedResourceGroupName string
13+
}{
14+
// RG string that ends with a slash
15+
{
16+
resourceGroupString: "resourceGroups/rg1/",
17+
expectedResourceGroupName: "rg1",
18+
},
19+
// RG string that ends with a space and starts new text
20+
{
21+
resourceGroupString: "resourceGroups/rg1 /subscriptions/",
22+
expectedResourceGroupName: "rg1",
23+
},
24+
25+
// RG string that includes nested resources and extraneous text.
26+
{
27+
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",
28+
expectedResourceGroupName: "myResourceGroup-rg",
29+
},
30+
// RG string that is surrounded by quotes.
31+
{
32+
resourceGroupString: `"id": "/subscriptions/0a2c89a7-a44e-4cd0-b6ec-868432ad1d13/resourceGroups/myResourceGroup"`,
33+
expectedResourceGroupName: "myResourceGroup",
34+
},
35+
// RG string that has no match.
36+
{
37+
resourceGroupString: "NoMatch",
38+
expectedResourceGroupName: "",
39+
},
40+
}
41+
42+
for _, tc := range testCases {
43+
resourceGroupName := FindResourceGroupName(tc.resourceGroupString)
44+
assert.Equal(t, tc.expectedResourceGroupName, resourceGroupName)
45+
}
46+
}

internal/engine/common/codeblock.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package common
2+
3+
import "github.com/Azure/InnovationEngine/internal/parsers"
4+
5+
// State for the codeblock in interactive mode. Used to keep track of the
6+
// state of each codeblock.
7+
type StatefulCodeBlock struct {
8+
CodeBlock parsers.CodeBlock
9+
CodeBlockNumber int
10+
Error error
11+
StdErr string
12+
StdOut string
13+
StepName string
14+
StepNumber int
15+
Success bool
16+
}
17+
18+
// Checks if a codeblock was executed by looking at the
19+
// output, errors, and if success is true.
20+
func (s StatefulCodeBlock) WasExecuted() bool {
21+
return s.StdOut != "" || s.StdErr != "" || s.Error != nil || s.Success
22+
}

internal/engine/commands.go internal/engine/common/commands.go

+27-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package engine
1+
package common
22

33
import (
44
"fmt"
@@ -23,6 +23,16 @@ type FailedCommandMessage struct {
2323
Error error
2424
}
2525

26+
type ExitMessage struct {
27+
EncounteredFailure bool
28+
}
29+
30+
func Exit(encounteredFailure bool) tea.Cmd {
31+
return func() tea.Msg {
32+
return ExitMessage{EncounteredFailure: encounteredFailure}
33+
}
34+
}
35+
2636
// Executes a bash command and returns a tea message with the output. This function
2737
// will be executed asycnhronously.
2838
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
5262
expectedRegex := codeBlock.ExpectedOutput.ExpectedRegex
5363
expectedOutputLanguage := codeBlock.ExpectedOutput.Language
5464

55-
outputComparisonError := compareCommandOutputs(
65+
outputComparisonError := CompareCommandOutputs(
5666
actualOutput,
5767
expectedOutput,
5868
expectedSimilarity,
@@ -86,16 +96,19 @@ func ExecuteCodeBlockAsync(codeBlock parsers.CodeBlock, env map[string]string) t
8696
// finishes executing.
8797
func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) tea.Msg {
8898
logging.GlobalLogger.Info("Executing command synchronously: ", codeBlock.Content)
89-
program.ReleaseTerminal()
99+
Program.ReleaseTerminal()
90100

91-
output, err := shells.ExecuteBashCommand(codeBlock.Content, shells.BashCommandConfiguration{
92-
EnvironmentVariables: env,
93-
InheritEnvironment: true,
94-
InteractiveCommand: true,
95-
WriteToHistory: true,
96-
})
101+
output, err := shells.ExecuteBashCommand(
102+
codeBlock.Content,
103+
shells.BashCommandConfiguration{
104+
EnvironmentVariables: env,
105+
InheritEnvironment: true,
106+
InteractiveCommand: true,
107+
WriteToHistory: true,
108+
},
109+
)
97110

98-
program.RestoreTerminal()
111+
Program.RestoreTerminal()
99112

100113
if err != nil {
101114
return FailedCommandMessage{
@@ -113,7 +126,7 @@ func ExecuteCodeBlockSync(codeBlock parsers.CodeBlock, env map[string]string) te
113126
}
114127

115128
// clearScreen returns a command that clears the terminal screen and positions the cursor at the top-left corner
116-
func clearScreen() tea.Cmd {
129+
func ClearScreen() tea.Cmd {
117130
return func() tea.Msg {
118131
fmt.Print(
119132
"\033[H\033[2J",
@@ -124,13 +137,13 @@ func clearScreen() tea.Cmd {
124137

125138
// Updates the azure status with the current state of the interactive mode
126139
// model.
127-
func updateAzureStatus(model InteractiveModeModel) tea.Cmd {
140+
func UpdateAzureStatus(azureStatus environments.AzureDeploymentStatus, environment string) tea.Cmd {
128141
return func() tea.Msg {
129142
logging.GlobalLogger.Tracef(
130143
"Attempting to update the azure status: %+v",
131-
model.azureStatus,
144+
azureStatus,
132145
)
133-
environments.ReportAzureStatus(model.azureStatus, model.environment)
146+
environments.ReportAzureStatus(azureStatus, environment)
134147
return AzureStatusUpdatedMessage{}
135148
}
136149
}

internal/engine/common/globals.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package common
2+
3+
import tea "github.com/charmbracelet/bubbletea"
4+
5+
// TODO: Ideally we won't need a global program variable. We should
6+
// refactor this in the future such that each tea program is localized to the
7+
// function that creates it and ExecuteCodeBlockSync doesn't mutate the global
8+
// program variable.
9+
var Program *tea.Program = nil

0 commit comments

Comments
 (0)