diff --git a/cmd/root_test.go b/cmd/root_test.go index 389b796..83bf770 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -33,6 +33,7 @@ func TestRunShowsHelpWhenNoArgs(t *testing.T) { func TestRunInit(t *testing.T) { root := t.TempDir() + addFakeRecordDependencies(t, "script", "bwrap", "bash") withWorkingDir(t, root, func() { stdout, stderr := captureOutput(t, func() { @@ -52,6 +53,13 @@ func TestRunInit(t *testing.T) { if got := mustReadFile(t, filepath.Join(root, "miro.toml")); got != "test_dir = \"e2e\"\n" { t.Fatalf("config = %q, want %q", got, "test_dir = \"e2e\"\n") } + info, err := os.Stat(filepath.Join(root, "e2e", "shell.sh")) + if err != nil { + t.Fatalf("Stat(%q) error = %v", filepath.Join(root, "e2e", "shell.sh"), err) + } + if info.Mode().Perm()&0o111 == 0 { + t.Fatalf("shell.sh mode = %o, want executable", info.Mode().Perm()) + } } func TestRunRecord(t *testing.T) { @@ -59,12 +67,20 @@ func TestRunRecord(t *testing.T) { root := t.TempDir() wantDir := filepath.Join(root, "e2e") - if err := os.MkdirAll(wantDir, 0o755); err != nil { - t.Fatalf("MkdirAll() error = %v", err) - } - writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") withWorkingDir(t, root, func() { + initStdout, initStderr := captureOutput(t, func() { + if got := Run([]string{"init"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + if initStdout != prefixed("Done initialising...\n") { + t.Fatalf("stdout = %q, want %q", initStdout, prefixed("Done initialising...\n")) + } + if initStderr != "" { + t.Fatalf("stderr = %q, want empty", initStderr) + } + withStdin(t, "y\n", func() {}) stdout, stderr := captureOutput(t, func() { if got := Run([]string{"record", "suite/spec"}); got != 0 { @@ -93,12 +109,20 @@ func TestRunRecordWithExplicitTestDirPath(t *testing.T) { root := t.TempDir() wantDir := filepath.Join(root, "e2e") - if err := os.MkdirAll(wantDir, 0o755); err != nil { - t.Fatalf("MkdirAll() error = %v", err) - } - writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") withWorkingDir(t, root, func() { + initStdout, initStderr := captureOutput(t, func() { + if got := Run([]string{"init"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + if initStdout != prefixed("Done initialising...\n") { + t.Fatalf("stdout = %q, want %q", initStdout, prefixed("Done initialising...\n")) + } + if initStderr != "" { + t.Fatalf("stderr = %q, want empty", initStderr) + } + withStdin(t, "y\n", func() {}) stdout, stderr := captureOutput(t, func() { if got := Run([]string{"record", filepath.Join("e2e", "suite", "spec")}); got != 0 { @@ -170,6 +194,9 @@ func TestRunInitFailsWhenDependenciesMissing(t *testing.T) { if _, err := os.Stat(filepath.Join(root, "miro.toml")); !os.IsNotExist(err) { t.Fatalf("Stat(%q) error = %v, want not exists", filepath.Join(root, "miro.toml"), err) } + if _, err := os.Stat(filepath.Join(root, "e2e", "shell.sh")); !os.IsNotExist(err) { + t.Fatalf("Stat(%q) error = %v, want not exists", filepath.Join(root, "e2e", "shell.sh"), err) + } }) } diff --git a/internal/miro/init.go b/internal/miro/init.go index b7b1e2d..67a5d0b 100644 --- a/internal/miro/init.go +++ b/internal/miro/init.go @@ -23,16 +23,20 @@ func Init() error { if _, err := miroconfig.ReadConfig(configPath); err != nil { return err } - return nil } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to check %s: %v", configPath, err) + } else { + if err := miroconfig.WriteConfig(configPath, miroconfig.Config{ + TestDir: defaultTestDir, + }); err != nil { + return fmt.Errorf("failed to write %s: %v", configPath, err) + } } - if err := miroconfig.WriteConfig(configPath, miroconfig.Config{ - TestDir: defaultTestDir, - }); err != nil { - return fmt.Errorf("failed to write %s: %v", configPath, err) + testDir, err := resolveTestDirFromRoot(root) + if err != nil { + return err } - return nil + return writeRecordShell(testDir) } diff --git a/internal/miro/record.go b/internal/miro/record.go index 7994661..3b6523e 100644 --- a/internal/miro/record.go +++ b/internal/miro/record.go @@ -20,11 +20,16 @@ func Record(path string) (string, error) { return "", err } + shellPath, err := resolveRecordShell(testDir) + if err != nil { + return "", err + } + if err := os.MkdirAll(target, 0o755); err != nil { return "", err } - if err := recordScenario(target, recordIO{ + if err := recordScenario(target, shellPath, recordIO{ in: os.Stdin, out: os.Stdout, err: os.Stderr, diff --git a/internal/miro/record_session.go b/internal/miro/record_session.go index 32f12e6..8c450ea 100644 --- a/internal/miro/record_session.go +++ b/internal/miro/record_session.go @@ -2,7 +2,6 @@ package miro import ( "bufio" - "fmt" "io" "os" "os/exec" @@ -25,7 +24,7 @@ type recordIO struct { err io.Writer } -func recordScenario(target string, rio recordIO) error { +func recordScenario(target, shellPath string, rio recordIO) error { rawIn, rawOut, cleanup, err := newRecordFiles() if err != nil { return err @@ -48,7 +47,7 @@ func recordScenario(target 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, sandbox, rio); err != nil { + if err := runRecordSession(target, rawIn, rawOut, shellPath, sandbox, rio); err != nil { return err } @@ -92,7 +91,6 @@ type recordSandbox struct { hostHome string hostTmp string projectRoot string - wrapperPath string pathEnv string } @@ -119,7 +117,6 @@ func newRecordSandboxForProjectRoot(root, pathEnv string) (recordSandbox, func() hostHome: filepath.Join(dir, "home"), hostTmp: filepath.Join(dir, "tmp"), projectRoot: root, - wrapperPath: filepath.Join(dir, "sandbox.sh"), pathEnv: pathEnv, } @@ -134,63 +131,12 @@ func newRecordSandboxForProjectRoot(root, pathEnv string) (recordSandbox, func() } } - body := buildRecordWrapperScript(sandbox) - if err := os.WriteFile(sandbox.wrapperPath, []byte(body), 0o755); err != nil { - cleanup() - return recordSandbox{}, nil, err - } - return sandbox, cleanup, nil } - -func buildRecordWrapperScript(sandbox recordSandbox) string { - var body strings.Builder - - body.WriteString("#!/bin/sh\n") - body.WriteString("set -eu\n\n") - body.WriteString("if command -v git >/dev/null 2>&1; then\n") - body.WriteString(fmt.Sprintf(" HOME=%s GIT_CONFIG_NOSYSTEM=1 git config --global user.name 'Miro Test' >/dev/null 2>&1 || :\n", shQuote(sandbox.hostHome))) - body.WriteString(fmt.Sprintf(" HOME=%s GIT_CONFIG_NOSYSTEM=1 git config --global user.email 'miro-test@example.com' >/dev/null 2>&1 || :\n", shQuote(sandbox.hostHome))) - body.WriteString(fmt.Sprintf(" HOME=%s GIT_CONFIG_NOSYSTEM=1 git config --global init.defaultBranch main >/dev/null 2>&1 || :\n", shQuote(sandbox.hostHome))) - body.WriteString(fmt.Sprintf(" HOME=%s GIT_CONFIG_NOSYSTEM=1 git config --global advice.defaultBranchName false >/dev/null 2>&1 || :\n", shQuote(sandbox.hostHome))) - body.WriteString("fi\n\n") - body.WriteString("exec bwrap \\\n") - body.WriteString(" --ro-bind / / \\\n") - body.WriteString(" --tmpfs /home \\\n") - body.WriteString(fmt.Sprintf(" --bind %s %s \\\n", shQuote(sandbox.hostHome), shQuote(recordVisibleHome))) - body.WriteString(fmt.Sprintf(" --ro-bind %s %s \\\n", shQuote(sandbox.projectRoot), shQuote(recordVisibleRepo))) - body.WriteString(fmt.Sprintf(" --bind %s %s \\\n", shQuote(sandbox.hostTmp), shQuote(recordVisibleTmp))) - body.WriteString(" --dev /dev \\\n") - body.WriteString(" --proc /proc \\\n") - body.WriteString(" --unshare-pid \\\n") - body.WriteString(" --die-with-parent \\\n") - body.WriteString(fmt.Sprintf(" --setenv GIT_AUTHOR_DATE %s \\\n", shQuote(recordGitDate))) - body.WriteString(fmt.Sprintf(" --setenv GIT_COMMITTER_DATE %s \\\n", shQuote(recordGitDate))) - body.WriteString(" --setenv GIT_CONFIG_NOSYSTEM '1' \\\n") - body.WriteString(" --setenv GIT_PAGER 'cat' \\\n") - body.WriteString(" --setenv HISTFILE '/dev/null' \\\n") - body.WriteString(fmt.Sprintf(" --setenv HOME %s \\\n", shQuote(recordVisibleHome))) - body.WriteString(" --setenv LANG 'C' \\\n") - body.WriteString(" --setenv LC_ALL 'C' \\\n") - body.WriteString(" --setenv PAGER 'cat' \\\n") - body.WriteString(fmt.Sprintf(" --setenv PATH %s \\\n", shQuote(sandbox.pathEnv))) - body.WriteString(" --setenv PS1 '$ ' \\\n") - body.WriteString(" --setenv TERM 'xterm-256color' \\\n") - body.WriteString(fmt.Sprintf(" --setenv TMPDIR %s \\\n", shQuote(recordVisibleTmp))) - body.WriteString(" --setenv TZ 'UTC' \\\n") - body.WriteString(fmt.Sprintf(" --chdir %s \\\n", shQuote(recordVisibleHome))) - body.WriteString(" bash --noprofile --norc -i\n") - - return body.String() -} - -func shQuote(value string) string { - return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" -} - -func runRecordSession(dir, rawIn, rawOut string, sandbox recordSandbox, rio recordIO) error { - cmd := exec.Command("script", "-q", "-E", "always", "-I", rawIn, "-O", rawOut, "-c", sandbox.wrapperPath) +func runRecordSession(dir, rawIn, rawOut, shellPath string, sandbox recordSandbox, rio recordIO) error { + cmd := exec.Command("script", "-q", "-E", "always", "-I", rawIn, "-O", rawOut, "-c", shellPath) cmd.Dir = dir + cmd.Env = recordSessionEnv(sandbox) cmd.Stdin = rio.in cmd.Stdout = rio.out cmd.Stderr = rio.err diff --git a/internal/miro/record_shell.go b/internal/miro/record_shell.go new file mode 100644 index 0000000..a01f827 --- /dev/null +++ b/internal/miro/record_shell.go @@ -0,0 +1,92 @@ +package miro + +import ( + "bytes" + "embed" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" +) + +const recordShellName = "shell.sh" + +//go:embed record_shell.sh.tmpl +var recordShellTemplateFS embed.FS + +var recordShellTemplate = template.Must( + template.New("record_shell.sh.tmpl").ParseFS(recordShellTemplateFS, "record_shell.sh.tmpl"), +) + +func recordShellPath(testDir string) string { + return filepath.Join(testDir, recordShellName) +} + +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) + } + + path := recordShellPath(testDir) + if err := os.WriteFile(path, []byte(buildRecordShellScript()), 0o644); err != nil { + return fmt.Errorf("failed to write recorder shell %q: %v", path, err) + } + if err := os.Chmod(path, 0o755); err != nil { + return fmt.Errorf("failed to make recorder shell executable %q: %v", path, err) + } + + return nil +} + +func resolveRecordShell(testDir string) (string, error) { + path := recordShellPath(testDir) + info, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("missing recorder shell %q; rerun `miro init` or restore the file", path) + } + return "", fmt.Errorf("failed to check recorder shell %q: %v", path, err) + } + if info.IsDir() { + return "", fmt.Errorf("recorder shell %q is a directory; rerun `miro init` or restore the file", path) + } + + return path, nil +} + +func buildRecordShellScript() string { + var body bytes.Buffer + if err := recordShellTemplate.Execute(&body, struct { + VisibleHome string + VisibleRepo string + VisibleTmp string + GitDate string + }{ + VisibleHome: shQuote(recordVisibleHome), + VisibleRepo: shQuote(recordVisibleRepo), + VisibleTmp: shQuote(recordVisibleTmp), + GitDate: shQuote(recordGitDate), + }); err != nil { + panic(fmt.Sprintf("render record shell template: %v", err)) + } + + return body.String() +} + +func recordSessionEnv(sandbox recordSandbox) []string { + env := append([]string{}, os.Environ()...) + env = append(env, + "MIRO_RECORD_HOST_HOME="+sandbox.hostHome, + "MIRO_RECORD_HOST_TMP="+sandbox.hostTmp, + "MIRO_RECORD_PROJECT_ROOT="+sandbox.projectRoot, + "MIRO_RECORD_PATH_ENV="+sandbox.pathEnv, + ) + + return env +} + +func shQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" +} diff --git a/internal/miro/record_shell.sh.tmpl b/internal/miro/record_shell.sh.tmpl new file mode 100644 index 0000000..ebc7574 --- /dev/null +++ b/internal/miro/record_shell.sh.tmpl @@ -0,0 +1,41 @@ +#!/bin/sh +set -eu + +host_home=${MIRO_RECORD_HOST_HOME:?} +host_tmp=${MIRO_RECORD_HOST_TMP:?} +project_root=${MIRO_RECORD_PROJECT_ROOT:?} +path_env=${MIRO_RECORD_PATH_ENV:?} + +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 || : + HOME="$host_home" GIT_CONFIG_NOSYSTEM=1 git config --global user.email 'miro-test@example.com' >/dev/null 2>&1 || : + HOME="$host_home" GIT_CONFIG_NOSYSTEM=1 git config --global init.defaultBranch main >/dev/null 2>&1 || : + HOME="$host_home" GIT_CONFIG_NOSYSTEM=1 git config --global advice.defaultBranchName false >/dev/null 2>&1 || : +fi + +exec bwrap \ + --ro-bind / / \ + --tmpfs /home \ + --bind "$host_home" {{ .VisibleHome }} \ + --ro-bind "$project_root" {{ .VisibleRepo }} \ + --bind "$host_tmp" {{ .VisibleTmp }} \ + --dev /dev \ + --proc /proc \ + --unshare-pid \ + --die-with-parent \ + --setenv GIT_AUTHOR_DATE {{ .GitDate }} \ + --setenv GIT_COMMITTER_DATE {{ .GitDate }} \ + --setenv GIT_CONFIG_NOSYSTEM '1' \ + --setenv GIT_PAGER 'cat' \ + --setenv HISTFILE '/dev/null' \ + --setenv HOME {{ .VisibleHome }} \ + --setenv LANG 'C' \ + --setenv LC_ALL 'C' \ + --setenv PAGER 'cat' \ + --setenv PATH "$path_env" \ + --setenv PS1 '$ ' \ + --setenv TERM 'xterm-256color' \ + --setenv TMPDIR {{ .VisibleTmp }} \ + --setenv TZ 'UTC' \ + --chdir {{ .VisibleHome }} \ + bash --noprofile --norc -i diff --git a/internal/miro/record_test.go b/internal/miro/record_test.go index c31b69a..11ffb37 100644 --- a/internal/miro/record_test.go +++ b/internal/miro/record_test.go @@ -16,6 +16,7 @@ func TestRecordCreatesRelativePath(t *testing.T) { testDir := filepath.Join(root, "e2e") mustMkdirAll(t, testDir) writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") + mustWriteRecordShell(t, testDir) addFakeRecordDependencies(t, "script") got := withWorkingDir(t, root, func() string { @@ -51,6 +52,7 @@ func TestRecordAcceptsExplicitTestDirPrefix(t *testing.T) { testDir := filepath.Join(root, "e2e") mustMkdirAll(t, testDir) writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") + mustWriteRecordShell(t, testDir) addFakeRecordDependencies(t, "script") got := withWorkingDir(t, root, func() string { @@ -101,13 +103,14 @@ func TestRecordReturnsDiscardedErrorWhenSaveDeclined(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") mustMkdirAll(t, testDir) + mustWriteRecordShell(t, testDir) addFakeRecordDependencies(t, "script") err := withWorkingDir(t, root, func() error { target := filepath.Join(testDir, "a", "b", "c") mustMkdirAll(t, target) return withRecordStreams(t, "n\n", func(rio recordIO) error { - return recordScenario(target, rio) + return recordScenario(target, recordShellPath(testDir), rio) }) }) @@ -128,13 +131,14 @@ func TestRecordReturnsDiscardedErrorWhenOverwriteDeclined(t *testing.T) { testDir := filepath.Join(root, "e2e") target := filepath.Join(testDir, "a", "b", "c") mustMkdirAll(t, target) + mustWriteRecordShell(t, testDir) addFakeRecordDependencies(t, "script") writeFile(t, filepath.Join(target, "in"), "existing in\n") writeFile(t, filepath.Join(target, "out"), "existing out\n") err := withWorkingDir(t, root, func() error { return withRecordStreams(t, "n\n", func(rio recordIO) error { - return recordScenario(target, rio) + return recordScenario(target, recordShellPath(testDir), rio) }) }) @@ -159,23 +163,20 @@ func TestRecordReturnsDiscardedErrorWhenOverwriteDeclined(t *testing.T) { } } -func TestNewRecordSandboxBuildsDeterministicWrapper(t *testing.T) { - projectRoot := filepath.Join(t.TempDir(), "project root") - sandbox, cleanup, err := newRecordSandboxForProjectRoot(projectRoot, "/custom/bin:/usr/bin") - if err != nil { - t.Fatalf("newRecordSandboxForProjectRoot() error = %v", err) - } - defer cleanup() - - body := readFile(t, sandbox.wrapperPath) +func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) { + body := buildRecordShellScript() for _, want := range []string{ - "HOME=" + shQuote(sandbox.hostHome) + " GIT_CONFIG_NOSYSTEM=1 git config --global user.name 'Miro Test'", - "--bind " + shQuote(sandbox.hostHome) + " " + shQuote(recordVisibleHome), - "--ro-bind " + shQuote(projectRoot) + " " + shQuote(recordVisibleRepo), - "--bind " + shQuote(sandbox.hostTmp) + " " + shQuote(recordVisibleTmp), + "host_home=${MIRO_RECORD_HOST_HOME:?}", + "host_tmp=${MIRO_RECORD_HOST_TMP:?}", + "project_root=${MIRO_RECORD_PROJECT_ROOT:?}", + "path_env=${MIRO_RECORD_PATH_ENV:?}", + "HOME=\"$host_home\" GIT_CONFIG_NOSYSTEM=1 git config --global user.name 'Miro Test'", + "--bind \"$host_home\" " + shQuote(recordVisibleHome), + "--ro-bind \"$project_root\" " + shQuote(recordVisibleRepo), + "--bind \"$host_tmp\" " + shQuote(recordVisibleTmp), "--setenv HOME " + shQuote(recordVisibleHome), - "--setenv PATH " + shQuote("/custom/bin:/usr/bin"), + "--setenv PATH \"$path_env\"", "--setenv PS1 '$ '", "--setenv TERM 'xterm-256color'", "--setenv TZ 'UTC'", @@ -190,6 +191,8 @@ func TestNewRecordSandboxBuildsDeterministicWrapper(t *testing.T) { func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { root := t.TempDir() + testDir := filepath.Join(root, "e2e") + mustWriteRecordShell(t, testDir) addFakeRecordDependencies(t, "script") argsPath := filepath.Join(t.TempDir(), "script.args") @@ -203,8 +206,9 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { } defer cleanup() + shellPath := recordShellPath(testDir) err = withRecordStreams(t, "", func(rio recordIO) error { - return runRecordSession(root, filepath.Join(t.TempDir(), "raw.in"), filepath.Join(t.TempDir(), "raw.out"), sandbox, rio) + return runRecordSession(root, filepath.Join(t.TempDir(), "raw.in"), filepath.Join(t.TempDir(), "raw.out"), shellPath, sandbox, rio) }) if err != nil { t.Fatalf("runRecordSession() error = %v", err) @@ -223,15 +227,16 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { if args[7] != "-c" { t.Fatalf("script args[7] = %q, want %q", args[7], "-c") } - if args[8] != sandbox.wrapperPath { - t.Fatalf("script args[8] = %q, want %q", args[8], sandbox.wrapperPath) + if args[8] != shellPath { + t.Fatalf("script args[8] = %q, want %q", args[8], shellPath) } body := readFile(t, commandBodyPath) for _, want := range []string{ + "host_home=${MIRO_RECORD_HOST_HOME:?}", "--ro-bind / /", "--tmpfs /home", - "--ro-bind " + shQuote(root) + " " + shQuote(recordVisibleRepo), + "--ro-bind \"$project_root\" " + shQuote(recordVisibleRepo), "--setenv HOME " + shQuote(recordVisibleHome), "--setenv TMPDIR " + shQuote(recordVisibleTmp), "bash --noprofile --norc -i", @@ -249,6 +254,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { testDir := filepath.Join(root, "e2e") target := filepath.Join(testDir, "suite", "spec") mustMkdirAll(t, target) + mustWriteRecordShell(t, testDir) reader, writer, err := os.Pipe() if err != nil { @@ -281,7 +287,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { }() err = withWorkingDir(t, root, func() error { - return recordScenario(target, recordIO{ + return recordScenario(target, recordShellPath(testDir), recordIO{ in: reader, out: ioDiscard{}, err: &bytes.Buffer{}, @@ -315,6 +321,29 @@ 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"), "test_dir = \"e2e\"\n") + mustMkdirAll(t, testDir) + addFakeRecordDependencies(t, "script") + + target := filepath.Join(testDir, "suite", "spec") + err := withWorkingDir(t, root, func() error { + _, err := Record(filepath.Join("suite", "spec")) + return err + }) + if err == nil { + t.Fatal("Record() error = nil, want error") + } + if !strings.Contains(err.Error(), "rerun `miro init`") { + t.Fatalf("Record() error = %q, want rerun init hint", err.Error()) + } + if _, statErr := os.Stat(target); !os.IsNotExist(statErr) { + t.Fatalf("Stat(%q) error = %v, want not exists", target, statErr) + } +} + func withRecordStreams[T any](t *testing.T, input string, fn func(recordIO) T) T { t.Helper() @@ -410,3 +439,11 @@ func requireCommands(t *testing.T, names ...string) { } } } + +func mustWriteRecordShell(t *testing.T, testDir string) { + t.Helper() + + if err := writeRecordShell(testDir); err != nil { + t.Fatalf("writeRecordShell(%q) error = %v", testDir, err) + } +} diff --git a/internal/miro/testdir_test.go b/internal/miro/testdir_test.go index 53c5223..95e6b6f 100644 --- a/internal/miro/testdir_test.go +++ b/internal/miro/testdir_test.go @@ -22,6 +22,7 @@ func TestInitCreatesConfigAtProjectRoot(t *testing.T) { if got != "test_dir = \"e2e\"\n" { t.Fatalf("config = %q, want %q", got, "test_dir = \"e2e\"\n") } + assertRecordShell(t, filepath.Join(root, "e2e", recordShellName)) } func TestInitUsesGitRoot(t *testing.T) { @@ -40,11 +41,13 @@ func TestInitUsesGitRoot(t *testing.T) { if _, err := os.Stat(filepath.Join(root, "miro.toml")); err != nil { t.Fatalf("Stat(%q) error = %v", filepath.Join(root, "miro.toml"), err) } + assertRecordShell(t, filepath.Join(root, "e2e", recordShellName)) } func TestInitLeavesExistingValidConfigUntouched(t *testing.T) { root := t.TempDir() writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"custom/suite\"\n") + writeFile(t, filepath.Join(root, "custom", "suite", recordShellName), "outdated\n") withWorkingDir(t, root, func() struct{} { if err := Init(); err != nil { @@ -57,6 +60,21 @@ func TestInitLeavesExistingValidConfigUntouched(t *testing.T) { if got != "test_dir = \"custom/suite\"\n" { t.Fatalf("config = %q, want %q", got, "test_dir = \"custom/suite\"\n") } + assertRecordShell(t, filepath.Join(root, "custom", "suite", recordShellName)) +} + +func TestInitCreatesMissingConfiguredTestDir(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"custom/suite\"\n") + + withWorkingDir(t, root, func() struct{} { + if err := Init(); err != nil { + t.Fatalf("Init() error = %v", err) + } + return struct{}{} + }) + + assertRecordShell(t, filepath.Join(root, "custom", "suite", recordShellName)) } func TestInitFailsWhenExistingConfigInvalid(t *testing.T) { @@ -263,3 +281,19 @@ func mustGitInit(t *testing.T, dir string) { t.Fatalf("git init: %v\n%s", err, out) } } + +func assertRecordShell(t *testing.T, path string) { + t.Helper() + + info, err := os.Stat(path) + if err != nil { + t.Fatalf("Stat(%q) error = %v", path, err) + } + if info.Mode().Perm()&0o111 == 0 { + t.Fatalf("%q mode = %o, want executable", path, info.Mode().Perm()) + } + + if got := readFile(t, path); got != buildRecordShellScript() { + t.Fatalf("shell = %q, want generated recorder shell", got) + } +}