Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ jobs:
go-version: ${{ env.GOVERSION }}
- run: git config --global url.https://${{ secrets.PRIVATE_REPO }}@github.com.insteadOf https://github.com
- run: go mod download
- run: go test -cover ./...
- run: make test-cover
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ REGISTRY ?= 714918108619.dkr.ecr.us-west-2.amazonaws.com
DISPATCH = $(BUILD)/dispatch
IMAGE = $(REGISTRY)/dispatch:$(TAG)

test:
test: dispatch
$(GO) test ./...

test-cover: dispatch
$(GO) test -cover ./...

lint:
golangci-lint run ./...

Expand Down
5 changes: 2 additions & 3 deletions cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ a pristine environment in which function calls can be dispatched and
handled by the local application. To start the command using a previous
session, use the --session option to specify a session ID from a
previous run.`, defaultEndpoint),
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
GroupID: "dispatch",
Args: cobra.MinimumNArgs(1),
GroupID: "dispatch",
PreRunE: func(cmd *cobra.Command, args []string) error {
return runConfigFlow()
},
Expand Down
189 changes: 189 additions & 0 deletions cli/run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package cli

import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

var dispatchBinary = filepath.Join("../build", runtime.GOOS, runtime.GOARCH, "dispatch")

func TestRunCommand(t *testing.T) {
t.Run("Run with non-existent env file", func(t *testing.T) {
t.Parallel()

buff, err := execRunCommand(&[]string{}, "run", "--env-file", "non-existent.env", "--", "echo", "hello")
if err != nil {
t.Fatal(err.Error())
}

assert.Regexp(t, "Error: failed to load env file from .+/dispatch/cli/non-existent.env: open non-existent.env: no such file or directory\n", buff.String())
})

t.Run("Run with env file", func(t *testing.T) {
t.Parallel()

envFile, err := createEnvFile(t.TempDir(), []byte("CHARACTER=rick_sanchez"))
defer os.Remove(envFile)
if err != nil {
t.Fatalf("Failed to write env file: %v", err)
}

buff, err := execRunCommand(&[]string{}, "run", "--env-file", envFile, "--", "printenv", "CHARACTER")
if err != nil {
t.Fatal(err.Error())
}

result, found := findEnvVariableInLogs(&buff)
if !found {
t.Fatalf("Expected printenv in the output: %s", buff.String())
}
assert.Equal(t, "rick_sanchez", result, fmt.Sprintf("Expected 'printenv | rick_sanchez' in the output, got 'printenv | %s'", result))
})

t.Run("Run with env variable", func(t *testing.T) {
t.Parallel()

// Set environment variables
envVars := []string{"CHARACTER=morty_smith"}

buff, err := execRunCommand(&envVars, "run", "--", "printenv", "CHARACTER")
if err != nil {
t.Fatal(err.Error())
}

result, found := findEnvVariableInLogs(&buff)
if !found {
t.Fatalf("Expected printenv in the output: %s", buff.String())
}
assert.Equal(t, "morty_smith", result, fmt.Sprintf("Expected 'printenv | morty_smith' in the output, got 'printenv | %s'", result))
})

t.Run("Run with env variable in command line has priority over the one in the env file", func(t *testing.T) {
t.Parallel()

envFile, err := createEnvFile(t.TempDir(), []byte("CHARACTER=rick_sanchez"))
defer os.Remove(envFile)
if err != nil {
t.Fatalf("Failed to write env file: %v", err)
}

// Set environment variables
envVars := []string{"CHARACTER=morty_smith"}
buff, err := execRunCommand(&envVars, "run", "--env-file", envFile, "--", "printenv", "CHARACTER")
if err != nil {
t.Fatal(err.Error())
}

result, found := findEnvVariableInLogs(&buff)
if !found {
t.Fatalf("Expected printenv in the output: %s", buff.String())
}
assert.Equal(t, "morty_smith", result, fmt.Sprintf("Expected 'printenv | morty_smith' in the output, got 'printenv | %s'", result))
})

t.Run("Run with env variable in local env vars has priority over the one in the env file", func(t *testing.T) {
// Do not use t.Parallel() here as we are manipulating the environment!

// Set environment variables
os.Setenv("CHARACTER", "morty_smith")
defer os.Unsetenv("CHARACTER")

envFile, err := createEnvFile(t.TempDir(), []byte("CHARACTER=rick_sanchez"))
defer os.Remove(envFile)

if err != nil {
t.Fatalf("Failed to write env file: %v", err)
}

buff, err := execRunCommand(&[]string{}, "run", "--env-file", envFile, "--", "printenv", "CHARACTER")
if err != nil {
t.Fatal(err.Error())
}

result, found := findEnvVariableInLogs(&buff)
if !found {
t.Fatalf("Expected printenv in the output: %s\n\n", buff.String())
}
assert.Equal(t, "morty_smith", result, fmt.Sprintf("Expected 'printenv | morty_smith' in the output, got 'printenv | %s'", result))
})
}

func execRunCommand(envVars *[]string, arg ...string) (bytes.Buffer, error) {
// Create a context with a timeout to ensure the process doesn't run indefinitely
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// add the api key to the arguments so the command can run without `dispatch login` being run first
arg = append(arg[:1], append([]string{"--api-key", "00000000"}, arg[1:]...)...)

// Set up the command
cmd := exec.CommandContext(ctx, dispatchBinary, arg...)

if len(*envVars) != 0 {
// Set environment variables
cmd.Env = append(os.Environ(), *envVars...)
}

// Capture the standard error
var errBuf bytes.Buffer
cmd.Stderr = &errBuf

// Start the command
if err := cmd.Start(); err != nil {
return errBuf, fmt.Errorf("Failed to start command: %w", err)
}

// Wait for the command to finish or for the context to timeout
// We use Wait() instead of Run() so that we can capture the error
// For example:
// FOO=bar ./build/darwin/amd64/dispatch run -- printenv FOO
// This will exit with
// Error: command 'printenv FOO' exited unexpectedly
// but also it will print...
// printenv | bar
// to the logs and that is exactly what we want to test
// If context timeout occurs, than something went wrong
// and `dispatch run` is running indefinitely.
if err := cmd.Wait(); err != nil {
// Check if the error is due to context timeout (command running too long)
if ctx.Err() == context.DeadlineExceeded {
Comment on lines +158 to +159
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context error will always carry information about this being a timeout, could we simplify the code by removing this special case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will do exit 1 on printenv ENV_VARIABLE, so I've added this check to be sure that this command exited properly and we can read stderr now to make sure ENV_VARIABLE has correct value (when err is nil).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I'm not sure I understand the condition in which we would get a timeout, but if you know it's important here it's fine (maybe worth a comment to record that context).

return errBuf, fmt.Errorf("Command timed out: %w", err)
}
}

return errBuf, nil
}

func createEnvFile(path string, content []byte) (string, error) {
envFile := filepath.Join(path, "test.env")
err := os.WriteFile(envFile, content, 0600)
return envFile, err
}

func findEnvVariableInLogs(buf *bytes.Buffer) (string, bool) {
var result string
found := false

// Split the log into lines
lines := strings.Split(buf.String(), "\n")

// Iterate over each line and check for the condition
for _, line := range lines {
if strings.Contains(line, "printenv | ") {
result = strings.Split(line, "printenv | ")[1]
found = true
break
}
}
return result, found
}