feat: arbitrary MIRO_ envs to shell script
This commit is contained in:
+12
-4
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
recordVisibleHome = "/home/test"
|
||||
recordGitDate = "2024-01-01T00:00:00Z"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +71,8 @@ func resolveRecordShell(testDir string) (string, error) {
|
||||
func buildRecordShellScript() string {
|
||||
var body bytes.Buffer
|
||||
if err := recordShellTemplate.Execute(&body, struct {
|
||||
VisibleHome string
|
||||
GitDate string
|
||||
}{
|
||||
VisibleHome: shQuote(recordVisibleHome),
|
||||
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, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user