feat: arbitrary MIRO_ envs to shell script

This commit is contained in:
2026-03-19 18:31:55 +00:00
parent 96b0ce57d9
commit c3823ee9b0
11 changed files with 337 additions and 75 deletions
+12 -4
View File
@@ -50,8 +50,8 @@ func TestRunInit(t *testing.T) {
}
})
if got := mustReadFile(t, filepath.Join(root, "miro.toml")); got != "[miro]\n test_dir = \"e2e\"\n" {
t.Fatalf("config = %q, want %q", got, "[miro]\n test_dir = \"e2e\"\n")
if got := mustReadFile(t, filepath.Join(root, "miro.toml")); got != defaultWrittenConfig("e2e") {
t.Fatalf("config = %q, want %q", got, defaultWrittenConfig("e2e"))
}
info, err := os.Stat(filepath.Join(root, "e2e", "shell.sh"))
if err != nil {
@@ -152,7 +152,7 @@ func TestRunRecordRejectsAbsolutePathOutsideTestDir(t *testing.T) {
if err := os.MkdirAll(wantDir, 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
outside := filepath.Join(root, "outside", "spec")
withWorkingDir(t, root, func() {
@@ -206,7 +206,7 @@ func TestRunRecordFailsWhenDependencyMissing(t *testing.T) {
if err := os.MkdirAll(wantDir, 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
for _, tc := range []struct {
name string
@@ -446,3 +446,11 @@ func withStdin(t *testing.T, input string, fn func()) {
fn()
}
func defaultWrittenConfig(testDir string) string {
return "[miro]\n test_dir = \"" + testDir + "\"\n\n[sandbox]\n visible_home = \"/home/test\"\n"
}
func validConfigContent(testDir string) string {
return "[miro]\ntest_dir = \"" + testDir + "\"\n\n[sandbox]\nvisible_home = \"/home/test\"\n"
}
+80 -7
View File
@@ -5,22 +5,40 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"github.com/BurntSushi/toml"
)
const DefaultVisibleHome = "/home/test"
var (
lowerSnakeCasePattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$`)
requiredSandboxDefaults = map[string]string{
"visible_home": DefaultVisibleHome,
}
)
type Config struct {
TestDir string
Sandbox map[string]string
}
type tomlConfig struct {
Miro tomlMiroConfig `toml:"miro"`
Miro tomlMiroConfig `toml:"miro"`
Sandbox map[string]string `toml:"sandbox"`
}
type tomlMiroConfig struct {
TestDir string `toml:"test_dir"`
}
func DefaultSandboxConfig() map[string]string {
return cloneSandbox(requiredSandboxDefaults)
}
// ReadConfig reads miro.toml.
func ReadConfig(path string) (Config, error) {
var raw tomlConfig
@@ -41,9 +59,18 @@ func ReadConfig(path string) (Config, error) {
if raw.Miro.TestDir == "" {
return Config{}, fmt.Errorf("failed to read %s: empty miro.test_dir", path)
}
if !meta.IsDefined("sandbox") {
return Config{}, fmt.Errorf("failed to read %s: missing [sandbox] config", path)
}
sandbox, err := validateSandbox(path, raw.Sandbox)
if err != nil {
return Config{}, err
}
return Config{
TestDir: raw.Miro.TestDir,
Sandbox: sandbox,
}, nil
}
@@ -52,15 +79,61 @@ func WriteConfig(path string, cfg Config) error {
if cfg.TestDir == "" {
return errors.New("empty miro.test_dir")
}
sandbox, err := validateSandbox(path, cfg.Sandbox)
if err != nil {
return err
}
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(tomlConfig{
Miro: tomlMiroConfig{
TestDir: cfg.TestDir,
},
}); err != nil {
return fmt.Errorf("failed to encode %s: %v", path, err)
fmt.Fprintf(&buf, "[miro]\n test_dir = %q\n\n[sandbox]\n", cfg.TestDir)
keys := make([]string, 0, len(sandbox))
for key := range sandbox {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Fprintf(&buf, " %s = %q\n", key, sandbox[key])
}
return os.WriteFile(path, buf.Bytes(), 0o644)
}
func validateSandbox(path string, sandbox map[string]string) (map[string]string, error) {
validated := cloneSandbox(sandbox)
for key := range validated {
if !lowerSnakeCasePattern.MatchString(key) {
return nil, fmt.Errorf("failed to read %s: invalid sandbox key %q: must be lower_snake_case", path, key)
}
}
for key := range requiredSandboxDefaults {
value, ok := validated[key]
if !ok {
return nil, fmt.Errorf("failed to read %s: missing required sandbox.%s", path, key)
}
if value == "" {
return nil, fmt.Errorf("failed to read %s: empty sandbox.%s", path, key)
}
}
if !filepath.IsAbs(validated["visible_home"]) {
return nil, fmt.Errorf("failed to read %s: sandbox.visible_home must be an absolute path", path)
}
return validated, nil
}
func cloneSandbox(sandbox map[string]string) map[string]string {
if len(sandbox) == 0 {
return map[string]string{}
}
cloned := make(map[string]string, len(sandbox))
for key, value := range sandbox {
cloned[key] = value
}
return cloned
}
+66 -5
View File
@@ -13,13 +13,18 @@ func TestReadConfig(t *testing.T) {
name string
content string
wantDir string
wantSandbox map[string]string
wantErr string
wantMissing bool
}{
{
name: "with test dir",
content: "[miro]\ntest_dir = \"custom/suite\"\n",
name: "with test dir and sandbox",
content: "[miro]\ntest_dir = \"custom/suite\"\n\n[sandbox]\nvisible_home = \"/home/test\"\nkey_word = \"value\"\n",
wantDir: "custom/suite",
wantSandbox: map[string]string{
"visible_home": "/home/test",
"key_word": "value",
},
},
{
name: "legacy top level key",
@@ -41,6 +46,31 @@ func TestReadConfig(t *testing.T) {
content: "[miro]\ntest_dir = \"\"\n",
wantErr: "empty miro.test_dir",
},
{
name: "without sandbox table",
content: "[miro]\ntest_dir = \"custom/suite\"\n",
wantErr: "missing [sandbox] config",
},
{
name: "without required visible home",
content: "[miro]\ntest_dir = \"custom/suite\"\n\n[sandbox]\n",
wantErr: "missing required sandbox.visible_home",
},
{
name: "empty required visible home",
content: "[miro]\ntest_dir = \"custom/suite\"\n\n[sandbox]\nvisible_home = \"\"\n",
wantErr: "empty sandbox.visible_home",
},
{
name: "relative visible home",
content: "[miro]\ntest_dir = \"custom/suite\"\n\n[sandbox]\nvisible_home = \"home/test\"\n",
wantErr: "sandbox.visible_home must be an absolute path",
},
{
name: "invalid sandbox key",
content: "[miro]\ntest_dir = \"custom/suite\"\n\n[sandbox]\nvisible_home = \"/home/test\"\nKeyWord = \"value\"\n",
wantErr: "invalid sandbox key",
},
{
name: "invalid toml",
content: "[miro]\ntest_dir = [\n",
@@ -83,6 +113,14 @@ func TestReadConfig(t *testing.T) {
if got.TestDir != tt.wantDir {
t.Fatalf("ReadConfig() TestDir = %q, want %q", got.TestDir, tt.wantDir)
}
if len(got.Sandbox) != len(tt.wantSandbox) {
t.Fatalf("ReadConfig() Sandbox = %#v, want %#v", got.Sandbox, tt.wantSandbox)
}
for key, want := range tt.wantSandbox {
if got.Sandbox[key] != want {
t.Fatalf("ReadConfig() Sandbox[%q] = %q, want %q", key, got.Sandbox[key], want)
}
}
})
}
}
@@ -90,7 +128,14 @@ func TestReadConfig(t *testing.T) {
func TestWriteConfig(t *testing.T) {
path := filepath.Join(t.TempDir(), "miro.toml")
if err := WriteConfig(path, Config{TestDir: "e2e"}); err != nil {
if err := WriteConfig(path, Config{
TestDir: "e2e",
Sandbox: map[string]string{
"visible_home": "/home/test",
"alpha_key": "a",
"zulu_key": "z",
},
}); err != nil {
t.Fatalf("WriteConfig() error = %v", err)
}
@@ -98,8 +143,9 @@ func TestWriteConfig(t *testing.T) {
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(got) != "[miro]\n test_dir = \"e2e\"\n" {
t.Fatalf("config = %q, want %q", string(got), "[miro]\n test_dir = \"e2e\"\n")
want := "[miro]\n test_dir = \"e2e\"\n\n[sandbox]\n alpha_key = \"a\"\n visible_home = \"/home/test\"\n zulu_key = \"z\"\n"
if string(got) != want {
t.Fatalf("config = %q, want %q", string(got), want)
}
}
@@ -114,3 +160,18 @@ func TestWriteConfigEmptyTestDirFails(t *testing.T) {
t.Fatalf("WriteConfig() error = %q, want %q", err.Error(), "empty miro.test_dir")
}
}
func TestWriteConfigMissingRequiredSandboxFails(t *testing.T) {
path := filepath.Join(t.TempDir(), "miro.toml")
err := WriteConfig(path, Config{
TestDir: "e2e",
Sandbox: map[string]string{},
})
if err == nil {
t.Fatal("WriteConfig() error = nil, want error")
}
if !strings.Contains(err.Error(), "missing required sandbox.visible_home") {
t.Fatalf("WriteConfig() error = %q, want visible_home error", err.Error())
}
}
+2 -1
View File
@@ -28,6 +28,7 @@ func Init() error {
} else {
if err := miroconfig.WriteConfig(configPath, miroconfig.Config{
TestDir: defaultTestDir,
Sandbox: miroconfig.DefaultSandboxConfig(),
}); err != nil {
return fmt.Errorf("failed to write %s: %v", configPath, err)
}
@@ -38,5 +39,5 @@ func Init() error {
return err
}
return writeRecordShell(testDir)
return ensureRecordShell(testDir)
}
+12 -2
View File
@@ -10,7 +10,17 @@ import (
// Record creates the requested scenario path under the resolved test directory
// and records an interactive shell session into in/out fixtures when saved.
func Record(path string) (string, error) {
testDir, err := ResolveTestDir()
root, err := currentProjectRoot()
if err != nil {
return "", err
}
cfg, err := readConfigFromRoot(root)
if err != nil {
return "", fmt.Errorf("failed to resolve test directory: %v", err)
}
testDir, err := resolveTestDirFromConfig(root, cfg)
if err != nil {
return "", fmt.Errorf("failed to resolve test directory: %v", err)
}
@@ -33,7 +43,7 @@ func Record(path string) (string, error) {
in: os.Stdin,
out: os.Stdout,
err: os.Stderr,
}); err != nil {
}, cfg.Sandbox); err != nil {
return "", err
}
+6 -6
View File
@@ -12,8 +12,7 @@ import (
)
const (
recordVisibleHome = "/home/test"
recordGitDate = "2024-01-01T00:00:00Z"
recordGitDate = "2024-01-01T00:00:00Z"
)
type recordIO struct {
@@ -22,7 +21,7 @@ type recordIO struct {
err io.Writer
}
func recordScenario(target, shellPath string, rio recordIO) error {
func recordScenario(target, shellPath string, rio recordIO, sandboxConfig map[string]string) error {
rawIn, rawOut, cleanup, err := newRecordFiles()
if err != nil {
return err
@@ -45,7 +44,7 @@ func recordScenario(target, shellPath string, rio recordIO) error {
output.Fprintln(rio.err, "Run commands in the recorder shell, then type exit to finish.")
if err := runRecordSession(target, rawIn, rawOut, shellPath, sandbox, rio); err != nil {
if err := runRecordSession(target, rawIn, rawOut, shellPath, sandbox, rio, sandboxConfig); err != nil {
return err
}
@@ -123,10 +122,11 @@ func newRecordSandboxForPathEnv(pathEnv string) (recordSandbox, func(), error) {
return sandbox, cleanup, nil
}
func runRecordSession(dir, rawIn, rawOut, shellPath string, sandbox recordSandbox, rio recordIO) error {
func runRecordSession(dir, rawIn, rawOut, shellPath string, sandbox recordSandbox, rio recordIO, sandboxConfig map[string]string) error {
cmd := exec.Command("script", "-q", "-E", "always", "-I", rawIn, "-O", rawOut, "-c", shellPath)
cmd.Dir = dir
cmd.Env = recordSessionEnv(sandbox)
cmd.Env = recordSessionEnv(sandbox, sandboxConfig)
cmd.Stdin = rio.in
cmd.Stdout = rio.out
cmd.Stderr = rio.err
+34 -8
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
)
@@ -24,6 +25,17 @@ func recordShellPath(testDir string) string {
return filepath.Join(testDir, recordShellName)
}
func ensureRecordShell(testDir string) error {
path := recordShellPath(testDir)
if _, err := os.Stat(path); err == nil {
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to check recorder shell %q: %v", path, err)
}
return writeRecordShell(testDir)
}
func writeRecordShell(testDir string) error {
if err := os.MkdirAll(testDir, 0o755); err != nil {
return fmt.Errorf("failed to create test directory %q: %v", testDir, err)
@@ -59,11 +71,9 @@ func resolveRecordShell(testDir string) (string, error) {
func buildRecordShellScript() string {
var body bytes.Buffer
if err := recordShellTemplate.Execute(&body, struct {
VisibleHome string
GitDate string
GitDate string
}{
VisibleHome: shQuote(recordVisibleHome),
GitDate: shQuote(recordGitDate),
GitDate: shQuote(recordGitDate),
}); err != nil {
panic(fmt.Sprintf("render record shell template: %v", err))
}
@@ -71,17 +81,33 @@ func buildRecordShellScript() string {
return body.String()
}
func recordSessionEnv(sandbox recordSandbox) []string {
func recordSessionEnv(sandbox recordSandbox, sandboxConfig map[string]string) []string {
env := append([]string{}, os.Environ()...)
env = append(env,
"MIRO_RECORD_HOST_HOME="+sandbox.hostHome,
"MIRO_RECORD_HOST_TMP="+sandbox.hostTmp,
"MIRO_RECORD_PATH_ENV="+sandbox.pathEnv,
"MIRO_HOST_HOME="+sandbox.hostHome,
"MIRO_HOST_TMP="+sandbox.hostTmp,
"MIRO_PATH_ENV="+sandbox.pathEnv,
)
for _, key := range sortedSandboxKeys(sandboxConfig) {
env = append(env, sandboxEnvName(key)+"="+sandboxConfig[key])
}
return env
}
func sandboxEnvName(key string) string {
return "MIRO_" + strings.ToUpper(key)
}
func sortedSandboxKeys(sandboxConfig map[string]string) []string {
keys := make([]string, 0, len(sandboxConfig))
for key := range sandboxConfig {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func shQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}
+10 -8
View File
@@ -2,11 +2,12 @@
set -eu
# Host directory mounted as the sandboxed HOME.
host_home=${MIRO_RECORD_HOST_HOME:?}
host_home=${MIRO_HOST_HOME:?}
# Host temp directory mounted read-write inside the sandbox.
host_tmp=${MIRO_RECORD_HOST_TMP:?}
host_tmp=${MIRO_HOST_TMP:?}
# PATH value forwarded into the sandbox so required tools stay available.
path_env=${MIRO_RECORD_PATH_ENV:?}
path_env=${MIRO_PATH_ENV:?}
visible_home=${MIRO_VISIBLE_HOME:?}
if command -v git >/dev/null 2>&1; then
HOME="$host_home" GIT_CONFIG_NOSYSTEM=1 git config --global user.name 'Miro Test' >/dev/null 2>&1 || :
@@ -15,10 +16,10 @@ if command -v git >/dev/null 2>&1; then
HOME="$host_home" GIT_CONFIG_NOSYSTEM=1 git config --global advice.defaultBranchName false >/dev/null 2>&1 || :
fi
exec bwrap \
set -- \
--ro-bind / / \
--tmpfs /home \
--bind "$host_home" {{ .VisibleHome }} \
--bind "$host_home" "$visible_home" \
--bind "$host_tmp" '/tmp' \
--dev /dev \
--proc /proc \
@@ -29,7 +30,7 @@ exec bwrap \
--setenv GIT_CONFIG_NOSYSTEM '1' \
--setenv GIT_PAGER 'cat' \
--setenv HISTFILE '/dev/null' \
--setenv HOME {{ .VisibleHome }} \
--setenv HOME "$visible_home" \
--setenv LANG 'C' \
--setenv LC_ALL 'C' \
--setenv PAGER 'cat' \
@@ -38,5 +39,6 @@ exec bwrap \
--setenv TERM 'xterm-256color' \
--setenv TMPDIR '/tmp' \
--setenv TZ 'UTC' \
--chdir {{ .VisibleHome }} \
bash --noprofile --norc -i
--chdir "$visible_home"
exec bwrap "$@" bash --noprofile --norc -i
+84 -21
View File
@@ -15,7 +15,7 @@ func TestRecordCreatesRelativePath(t *testing.T) {
root := t.TempDir()
testDir := filepath.Join(root, "e2e")
mustMkdirAll(t, testDir)
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
mustWriteRecordShell(t, testDir)
addFakeRecordDependencies(t, "script")
@@ -51,7 +51,7 @@ func TestRecordAcceptsExplicitTestDirPrefix(t *testing.T) {
root := t.TempDir()
testDir := filepath.Join(root, "e2e")
mustMkdirAll(t, testDir)
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
mustWriteRecordShell(t, testDir)
addFakeRecordDependencies(t, "script")
@@ -83,7 +83,7 @@ func TestRecordAcceptsExplicitTestDirPrefix(t *testing.T) {
func TestRecordRejectsAbsolutePathOutsideTestDir(t *testing.T) {
root := t.TempDir()
mustMkdirAll(t, filepath.Join(root, "e2e"))
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
outside := filepath.Join(root, "outside", "a", "b", "c")
err := withWorkingDir(t, root, func() error {
@@ -110,7 +110,7 @@ func TestRecordReturnsDiscardedErrorWhenSaveDeclined(t *testing.T) {
target := filepath.Join(testDir, "a", "b", "c")
mustMkdirAll(t, target)
return withRecordStreams(t, "n\n", func(rio recordIO) error {
return recordScenario(target, recordShellPath(testDir), rio)
return recordScenario(target, recordShellPath(testDir), rio, defaultSandboxConfig())
})
})
@@ -138,7 +138,7 @@ func TestRecordReturnsDiscardedErrorWhenOverwriteDeclined(t *testing.T) {
err := withWorkingDir(t, root, func() error {
return withRecordStreams(t, "n\n", func(rio recordIO) error {
return recordScenario(target, recordShellPath(testDir), rio)
return recordScenario(target, recordShellPath(testDir), rio, defaultSandboxConfig())
})
})
@@ -167,24 +167,62 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
body := buildRecordShellScript()
for _, want := range []string{
"host_home=${MIRO_RECORD_HOST_HOME:?}",
"host_tmp=${MIRO_RECORD_HOST_TMP:?}",
"path_env=${MIRO_RECORD_PATH_ENV:?}",
"host_home=${MIRO_HOST_HOME:?}",
"host_tmp=${MIRO_HOST_TMP:?}",
"path_env=${MIRO_PATH_ENV:?}",
"visible_home=${MIRO_VISIBLE_HOME:?}",
"HOME=\"$host_home\" GIT_CONFIG_NOSYSTEM=1 git config --global user.name 'Miro Test'",
"--bind \"$host_home\" " + shQuote(recordVisibleHome),
"--bind \"$host_home\" \"$visible_home\"",
"--bind \"$host_tmp\" '/tmp'",
"--setenv HOME " + shQuote(recordVisibleHome),
"--setenv HOME \"$visible_home\"",
"--setenv PATH \"$path_env\"",
"--setenv PS1 '$ '",
"--setenv TERM 'xterm-256color'",
"--setenv TZ 'UTC'",
"--chdir " + shQuote(recordVisibleHome),
"bash --noprofile --norc -i",
"--chdir \"$visible_home\"",
"exec bwrap \"$@\" bash --noprofile --norc -i",
} {
if !strings.Contains(body, want) {
t.Fatalf("wrapper = %q, want substring %q", body, want)
}
}
for _, unwanted := range []string{
"MIRO_SANDBOX_ENVS",
"set -- \"$@\" --setenv \"$env_name\" \"$env_value\"",
} {
if strings.Contains(body, unwanted) {
t.Fatalf("wrapper = %q, want no substring %q", body, unwanted)
}
}
if strings.Contains(body, "/home/test") {
t.Fatalf("wrapper = %q, want no hardcoded visible home", body)
}
}
func TestRecordSessionEnvIncludesConfiguredSandboxEnv(t *testing.T) {
env := recordSessionEnv(recordSandbox{
hostHome: "/tmp/host-home",
hostTmp: "/tmp/host-tmp",
pathEnv: "/tmp/bin",
}, map[string]string{
"visible_home": "/sandbox/home",
"key_word": "value",
})
for _, want := range []string{
"MIRO_HOST_HOME=/tmp/host-home",
"MIRO_HOST_TMP=/tmp/host-tmp",
"MIRO_PATH_ENV=/tmp/bin",
"MIRO_KEY_WORD=value",
"MIRO_VISIBLE_HOME=/sandbox/home",
} {
if !containsEnvEntry(env, want) {
t.Fatalf("env = %#v, want entry %q", env, want)
}
}
if containsEnvEntry(env, "MIRO_SANDBOX_ENVS=MIRO_KEY_WORD:MIRO_VISIBLE_HOME") {
t.Fatalf("env = %#v, want MIRO_SANDBOX_ENVS to be absent", env)
}
}
func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) {
@@ -205,7 +243,7 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) {
shellPath := recordShellPath(testDir)
err = withRecordStreams(t, "", func(rio recordIO) error {
return runRecordSession(t.TempDir(), filepath.Join(t.TempDir(), "raw.in"), filepath.Join(t.TempDir(), "raw.out"), shellPath, sandbox, rio)
return runRecordSession(t.TempDir(), filepath.Join(t.TempDir(), "raw.in"), filepath.Join(t.TempDir(), "raw.out"), shellPath, sandbox, rio, defaultSandboxConfig())
})
if err != nil {
t.Fatalf("runRecordSession() error = %v", err)
@@ -230,17 +268,21 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) {
body := readFile(t, commandBodyPath)
for _, want := range []string{
"host_home=${MIRO_RECORD_HOST_HOME:?}",
"host_home=${MIRO_HOST_HOME:?}",
"visible_home=${MIRO_VISIBLE_HOME:?}",
"--ro-bind / /",
"--tmpfs /home",
"--setenv HOME " + shQuote(recordVisibleHome),
"--setenv HOME \"$visible_home\"",
"--setenv TMPDIR '/tmp'",
"bash --noprofile --norc -i",
"exec bwrap \"$@\" bash --noprofile --norc -i",
} {
if !strings.Contains(body, want) {
t.Fatalf("wrapper = %q, want substring %q", body, want)
}
}
if strings.Contains(body, "MIRO_SANDBOX_ENVS") {
t.Fatalf("wrapper = %q, want no MIRO_SANDBOX_ENVS reference", body)
}
}
func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
@@ -251,6 +293,11 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
target := filepath.Join(testDir, "suite", "spec")
mustMkdirAll(t, target)
mustWriteRecordShell(t, testDir)
sandboxConfig := map[string]string{
"visible_home": "/home/test",
"key_word": "value",
}
visibleHome := sandboxConfig["visible_home"]
reader, writer, err := os.Pipe()
if err != nil {
@@ -267,7 +314,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
defer close(writeDone)
defer writer.Close()
if _, err := writer.Write([]byte("pwd\necho \"$HOME\"\nif [ -e /home/test/repo ]; then echo FOUND; else echo MISSING; fi\npwd\nexit\n")); err != nil {
if _, err := writer.Write([]byte("pwd\necho \"$HOME\"\necho \"$MIRO_KEY_WORD\"\nif [ -e \"$HOME/repo\" ]; then echo FOUND; else echo MISSING; fi\npwd\nexit\n")); err != nil {
writeDone <- err
return
}
@@ -287,7 +334,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
in: reader,
out: ioDiscard{},
err: &bytes.Buffer{},
})
}, sandboxConfig)
})
if err != nil {
t.Fatalf("recordScenario() error = %v", err)
@@ -300,7 +347,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
if strings.Contains(recordedIn, "Script started on ") {
t.Fatalf("saved in = %q, want stripped script wrapper", recordedIn)
}
for _, want := range []string{"pwd\n", "echo \"$HOME\"\n", "if [ -e /home/test/repo ]; then echo FOUND; else echo MISSING; fi\n", "exit\n"} {
for _, want := range []string{"pwd\n", "echo \"$HOME\"\n", "echo \"$MIRO_KEY_WORD\"\n", "if [ -e \"$HOME/repo\" ]; then echo FOUND; else echo MISSING; fi\n", "exit\n"} {
if !strings.Contains(recordedIn, want) {
t.Fatalf("saved in = %q, want substring %q", recordedIn, want)
}
@@ -310,7 +357,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
if strings.Contains(recordedOut, "Script started on ") {
t.Fatalf("saved out = %q, want stripped script wrapper", recordedOut)
}
for _, want := range []string{recordVisibleHome} {
for _, want := range []string{visibleHome, "value"} {
if !strings.Contains(recordedOut, want) {
t.Fatalf("saved out = %q, want substring %q", recordedOut, want)
}
@@ -326,7 +373,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
func TestRecordFailsWhenRecorderShellMissing(t *testing.T) {
root := t.TempDir()
testDir := filepath.Join(root, "e2e")
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
mustMkdirAll(t, testDir)
addFakeRecordDependencies(t, "script")
@@ -449,3 +496,19 @@ func mustWriteRecordShell(t *testing.T, testDir string) {
t.Fatalf("writeRecordShell(%q) error = %v", testDir, err)
}
}
func defaultSandboxConfig() map[string]string {
return map[string]string{
"visible_home": "/home/test",
}
}
func containsEnvEntry(env []string, want string) bool {
for _, entry := range env {
if entry == want {
return true
}
}
return false
}
+10 -2
View File
@@ -22,12 +22,20 @@ func ResolveTestDir() (string, error) {
}
func resolveTestDirFromRoot(root string) (string, error) {
configPath := filepath.Join(root, "miro.toml")
cfg, err := miroconfig.ReadConfig(configPath)
cfg, err := readConfigFromRoot(root)
if err != nil {
return "", err
}
return resolveTestDirFromConfig(root, cfg)
}
func readConfigFromRoot(root string) (miroconfig.Config, error) {
configPath := filepath.Join(root, "miro.toml")
return miroconfig.ReadConfig(configPath)
}
func resolveTestDirFromConfig(root string, cfg miroconfig.Config) (string, error) {
testDir := filepath.Join(root, cfg.TestDir)
info, err := os.Stat(testDir)
if err == nil {
+21 -11
View File
@@ -19,8 +19,8 @@ func TestInitCreatesConfigAtProjectRoot(t *testing.T) {
})
got := readFile(t, filepath.Join(root, "miro.toml"))
if got != "[miro]\n test_dir = \"e2e\"\n" {
t.Fatalf("config = %q, want %q", got, "[miro]\n test_dir = \"e2e\"\n")
if got != defaultWrittenConfig("e2e") {
t.Fatalf("config = %q, want %q", got, defaultWrittenConfig("e2e"))
}
assertRecordShell(t, filepath.Join(root, "e2e", recordShellName))
}
@@ -46,7 +46,7 @@ func TestInitUsesGitRoot(t *testing.T) {
func TestInitLeavesExistingValidConfigUntouched(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"custom/suite\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite"))
writeFile(t, filepath.Join(root, "custom", "suite", recordShellName), "outdated\n")
withWorkingDir(t, root, func() struct{} {
@@ -57,15 +57,17 @@ func TestInitLeavesExistingValidConfigUntouched(t *testing.T) {
})
got := readFile(t, filepath.Join(root, "miro.toml"))
if got != "[miro]\ntest_dir = \"custom/suite\"\n" {
t.Fatalf("config = %q, want %q", got, "[miro]\ntest_dir = \"custom/suite\"\n")
if got != validConfigContent("custom/suite") {
t.Fatalf("config = %q, want %q", got, validConfigContent("custom/suite"))
}
if got := readFile(t, filepath.Join(root, "custom", "suite", recordShellName)); got != "outdated\n" {
t.Fatalf("shell = %q, want existing shell to stay untouched", got)
}
assertRecordShell(t, filepath.Join(root, "custom", "suite", recordShellName))
}
func TestInitCreatesMissingConfiguredTestDir(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"custom/suite\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite"))
withWorkingDir(t, root, func() struct{} {
if err := Init(); err != nil {
@@ -96,7 +98,7 @@ func TestResolveTestDirFromConfig(t *testing.T) {
root := t.TempDir()
configured := filepath.Join(root, "custom", "suite")
mustMkdirAll(t, configured)
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"custom/suite\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite"))
got := withWorkingDir(t, root, func() string {
path, err := ResolveTestDir()
@@ -181,7 +183,7 @@ func TestResolveTestDirMalformedConfigFails(t *testing.T) {
func TestResolveTestDirConfiguredMissingDirectoryReturnsConfiguredPath(t *testing.T) {
root := t.TempDir()
want := filepath.Join(root, "missing")
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"missing\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("missing"))
got := withWorkingDir(t, root, func() string {
path, err := ResolveTestDir()
@@ -199,7 +201,7 @@ func TestResolveTestDirConfiguredMissingDirectoryReturnsConfiguredPath(t *testin
func TestResolveTestDirConfiguredFileFails(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "case.txt"), "hello\n")
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"case.txt\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("case.txt"))
err := withWorkingDir(t, root, func() error {
_, err := ResolveTestDir()
@@ -218,7 +220,7 @@ func TestResolveTestDirUsesGitRoot(t *testing.T) {
root := t.TempDir()
mustGitInit(t, root)
want := filepath.Join(root, "e2e")
writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"e2e\"\n")
writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e"))
subdir := filepath.Join(root, "nested", "dir")
mustMkdirAll(t, subdir)
@@ -297,3 +299,11 @@ func assertRecordShell(t *testing.T, path string) {
t.Fatalf("shell = %q, want generated recorder shell", got)
}
}
func defaultWrittenConfig(testDir string) string {
return "[miro]\n test_dir = \"" + testDir + "\"\n\n[sandbox]\n visible_home = \"/home/test\"\n"
}
func validConfigContent(testDir string) string {
return "[miro]\ntest_dir = \"" + testDir + "\"\n\n[sandbox]\nvisible_home = \"/home/test\"\n"
}