diff --git a/cmd/root_test.go b/cmd/root_test.go index dc9e752..b3159d4 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,18 +1,18 @@ package cmd import ( - "bytes" - "io" "os" "path/filepath" "strings" "testing" + + "miro/internal/testutil" ) func TestRunShowsHelpWhenNoArgs(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - stdout, stderr := captureOutput(t, func() { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run(nil); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -31,10 +31,10 @@ func TestRunShowsHelpWhenNoArgs(t *testing.T) { func TestRunInit(t *testing.T) { root := t.TempDir() - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { + testutil.WithWorkingDir(t, root, func() struct{} { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"init"}); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -46,10 +46,11 @@ func TestRunInit(t *testing.T) { if stderr != "" { t.Fatalf("stderr = %q, want empty", stderr) } + return struct{}{} }) - if got := mustReadFile(t, filepath.Join(root, "miro.toml")); got != defaultWrittenConfig("e2e") { - t.Fatalf("config = %q, want %q", got, defaultWrittenConfig("e2e")) + if got := testutil.ReadFile(t, filepath.Join(root, "miro.toml")); got != testutil.DefaultWrittenConfig("e2e") { + t.Fatalf("config = %q, want %q", got, testutil.DefaultWrittenConfig("e2e")) } info, err := os.Stat(filepath.Join(root, "e2e", "shell.sh")) if err != nil { @@ -61,13 +62,13 @@ func TestRunInit(t *testing.T) { } func TestRunRecord(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") root := t.TempDir() wantDir := filepath.Join(root, "e2e") - withWorkingDir(t, root, func() { - initStdout, initStderr := captureOutput(t, func() { + testutil.WithWorkingDir(t, root, func() struct{} { + initStdout, initStderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"init"}); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -79,8 +80,8 @@ func TestRunRecord(t *testing.T) { t.Fatalf("stderr = %q, want empty", initStderr) } - withStdin(t, "y\n", func() {}) - stdout, stderr := captureOutput(t, func() { + testutil.WithStdin(t, "y\n", func() {}) + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"record", "suite/spec"}); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -99,6 +100,7 @@ func TestRunRecord(t *testing.T) { t.Fatalf("Stat(%q) error = %v", filepath.Join(createdPath, name), err) } } + return struct{}{} }) } @@ -106,8 +108,8 @@ func TestRunInitFailsWhenDependenciesMissing(t *testing.T) { root := t.TempDir() t.Setenv("PATH", "") - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { + testutil.WithWorkingDir(t, root, func() struct{} { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"init"}); got != 1 { t.Fatalf("Run() code = %d, want %d", got, 1) } @@ -125,82 +127,40 @@ func TestRunInitFailsWhenDependenciesMissing(t *testing.T) { 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) } + return struct{}{} }) } -func TestRunRecordFailsWhenDependencyMissing(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"), validConfigContent("e2e")) +func TestRunRecordMissingPath(t *testing.T) { + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - for _, tc := range []struct { - name string - deps []string - missing string - }{ - {name: "script", deps: []string{"bwrap", "bash"}, missing: "script"}, - {name: "bwrap", deps: []string{"script", "bash"}, missing: "bwrap"}, - {name: "bash", deps: []string{"script", "bwrap"}, missing: "bash"}, - } { - t.Run(tc.name, func(t *testing.T) { - addFakeRecordDependencies(t, tc.deps...) - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"record", "suite/spec"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - want := `required command "` + tc.missing + `" not found in PATH` - if !strings.Contains(stderr, want) { - t.Fatalf("stderr = %q, want missing dependency error %q", stderr, want) - } - if _, err := os.Stat(filepath.Join(wantDir, "suite", "spec", "in")); !os.IsNotExist(err) { - t.Fatalf("Stat() error = %v, want not exists", err) - } - }) - }) - } -} - -func TestRunRecordMissingConfigFails(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - - root := t.TempDir() - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"record", "suite/spec"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - if !strings.Contains(stderr, "failed to resolve test directory") || !strings.Contains(stderr, "miro.toml") { - t.Fatalf("stderr = %q, want missing config error", stderr) + stdout, stderr := testutil.CaptureOutput(t, func() { + if got := Run([]string{"record"}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) } }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "accepts 1 arg(s), received 0") { + t.Fatalf("stderr = %q, want argument error", stderr) + } + if !strings.Contains(stderr, "Usage:") || !strings.Contains(stderr, "miro record ") { + t.Fatalf("stderr = %q, want record usage", stderr) + } } func TestRunTest(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") root := t.TempDir() - writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\n") + testutil.WriteScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") + testutil.WriteScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\n") - withWorkingDir(t, root, func() { - initStdout, initStderr := captureOutput(t, func() { + testutil.WithWorkingDir(t, root, func() struct{} { + initStdout, initStderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"init"}); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -212,7 +172,7 @@ func TestRunTest(t *testing.T) { t.Fatalf("stderr = %q, want empty", initStderr) } - stdout, stderr := captureOutput(t, func() { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"test"}); got != 0 { t.Fatalf("Run() code = %d, want %d", got, 0) } @@ -233,270 +193,14 @@ func TestRunTest(t *testing.T) { if stderr != "" { t.Fatalf("stderr = %q, want empty", stderr) } + return struct{}{} }) } -func TestRunTestScopedDirectory(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") - - root := t.TempDir() - writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "c"), "echo three\n", "echo three\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) - } - - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test", "nested"}); got != 0 { - t.Fatalf("Run() code = %d, want %d", got, 0) - } - }) - - for _, want := range []string{ - "RUN nested/b", - "PASS nested/b (", - "RUN nested/c", - "PASS nested/c (", - "Summary: total=2 passed=2 failed=0", - } { - if !strings.Contains(stdout, want) { - t.Fatalf("stdout = %q, want substring %q", stdout, want) - } - } - if strings.Contains(stdout, "RUN a") { - t.Fatalf("stdout = %q, want scoped run to exclude scenario outside nested", stdout) - } - if stderr != "" { - t.Fatalf("stderr = %q, want empty", stderr) - } - }) -} - -func TestRunTestScopedLeafDirectory(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") - - root := t.TempDir() - writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\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) - } - - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test", filepath.Join("nested", "b")}); got != 0 { - t.Fatalf("Run() code = %d, want %d", got, 0) - } - }) - - for _, want := range []string{ - "RUN nested/b", - "PASS nested/b (", - "Summary: total=1 passed=1 failed=0", - } { - if !strings.Contains(stdout, want) { - t.Fatalf("stdout = %q, want substring %q", stdout, want) - } - } - if strings.Contains(stdout, "RUN a") { - t.Fatalf("stdout = %q, want scoped run to exclude scenario outside nested/b", stdout) - } - if stderr != "" { - t.Fatalf("stderr = %q, want empty", stderr) - } - }) -} - -func TestRunTestReturnsZeroWhenScenarioMismatches(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") - - root := t.TempDir() - writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "b"), "echo two\n", "different output\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) - } - - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test"}); got != 0 { - t.Fatalf("Run() code = %d, want %d", got, 0) - } - }) - - for _, want := range []string{ - "RUN a", - "PASS a (", - "RUN b", - "FAIL b (", - "output differed", - "Summary: total=2 passed=1 failed=1", - } { - if !strings.Contains(stdout, want) { - t.Fatalf("stdout = %q, want substring %q", stdout, want) - } - } - if stderr != "" { - t.Fatalf("stderr = %q, want empty", stderr) - } - }) -} - -func TestRunTestMissingConfigFails(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - - root := t.TempDir() - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - if !strings.Contains(stderr, "failed to resolve test directory") || !strings.Contains(stderr, "miro.toml") { - t.Fatalf("stderr = %q, want missing config error", stderr) - } - }) -} - -func TestRunTestFailsWhenRecorderShellMissing(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - - root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - if !strings.Contains(stderr, "rerun `miro init`") { - t.Fatalf("stderr = %q, want rerun init hint", stderr) - } - }) -} - -func TestRunTestFailsWhenFixtureMalformed(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - - root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) - writeFile(t, filepath.Join(root, "e2e", "shell.sh"), "#!/bin/sh\n") - writeFile(t, filepath.Join(root, "e2e", "broken", "in"), "echo broken\n") - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - if !strings.Contains(stderr, `malformed scenario "broken": missing out fixture`) { - t.Fatalf("stderr = %q, want malformed fixture error", stderr) - } - }) -} - -func TestRunTestReturnsZeroWhenCompareMarkerMissing(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") - - root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) - writeFile(t, filepath.Join(root, "e2e", "shell.sh"), "#!/bin/sh\n") - writeScenarioFixtures(t, filepath.Join(root, "e2e", "some"), "echo one\n", "echo one\n") - - withWorkingDir(t, root, func() { - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"test"}); got != 0 { - t.Fatalf("Run() code = %d, want %d", got, 0) - } - }) - - for _, want := range []string{ - "RUN some", - "FAIL some (", - "missing compare marker", - "Summary: total=1 passed=0 failed=1", - } { - if !strings.Contains(stdout, want) { - t.Fatalf("stdout = %q, want substring %q", stdout, want) - } - } - if stderr != "" { - t.Fatalf("stderr = %q, want empty", stderr) - } - }) -} - -func TestRunRecordMissingPath(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") - - stdout, stderr := captureOutput(t, func() { - if got := Run([]string{"record"}); got != 1 { - t.Fatalf("Run() code = %d, want %d", got, 1) - } - }) - - if stdout != "" { - t.Fatalf("stdout = %q, want empty", stdout) - } - if !strings.Contains(stderr, "accepts 1 arg(s), received 0") { - t.Fatalf("stderr = %q, want argument error", stderr) - } - if !strings.Contains(stderr, "Usage:") || !strings.Contains(stderr, "miro record ") { - t.Fatalf("stderr = %q, want record usage", stderr) - } -} - func TestRunTestExtraArgs(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - stdout, stderr := captureOutput(t, func() { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"test", "a", "b"}); got != 1 { t.Fatalf("Run() code = %d, want %d", got, 1) } @@ -514,9 +218,9 @@ func TestRunTestExtraArgs(t *testing.T) { } func TestRunInitExtraArgs(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - stdout, stderr := captureOutput(t, func() { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"init", "extra"}); got != 1 { t.Fatalf("Run() code = %d, want %d", got, 1) } @@ -534,9 +238,9 @@ func TestRunInitExtraArgs(t *testing.T) { } func TestRunUnknownCommand(t *testing.T) { - addFakeRecordDependencies(t, "script", "bwrap", "bash") + testutil.AddFakeRecordDependencies(t, "script", "bwrap", "bash") - stdout, stderr := captureOutput(t, func() { + stdout, stderr := testutil.CaptureOutput(t, func() { if got := Run([]string{"wat"}); got != 1 { t.Fatalf("Run() code = %d, want %d", got, 1) } @@ -550,242 +254,6 @@ func TestRunUnknownCommand(t *testing.T) { } } -func captureOutput(t *testing.T, fn func()) (string, string) { - t.Helper() - t.Setenv("NO_COLOR", "1") - - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - t.Fatalf("os.Pipe() stdout error = %v", err) - } - stderrReader, stderrWriter, err := os.Pipe() - if err != nil { - t.Fatalf("os.Pipe() stderr error = %v", err) - } - - oldStdout := os.Stdout - oldStderr := os.Stderr - os.Stdout = stdoutWriter - os.Stderr = stderrWriter - t.Cleanup(func() { - os.Stdout = oldStdout - os.Stderr = oldStderr - }) - - fn() - - if err := stdoutWriter.Close(); err != nil { - t.Fatalf("stdout close error = %v", err) - } - if err := stderrWriter.Close(); err != nil { - t.Fatalf("stderr close error = %v", err) - } - - var stdoutBuf bytes.Buffer - if _, err := io.Copy(&stdoutBuf, stdoutReader); err != nil { - t.Fatalf("stdout copy error = %v", err) - } - - var stderrBuf bytes.Buffer - if _, err := io.Copy(&stderrBuf, stderrReader); err != nil { - t.Fatalf("stderr copy error = %v", err) - } - - return stdoutBuf.String(), stderrBuf.String() -} - func prefixed(msg string) string { return "miro › " + msg } - -func writeFile(t *testing.T, path, content string) { - t.Helper() - - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatalf("MkdirAll(%q) error = %v", filepath.Dir(path), err) - } - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } -} - -func writeScenarioFixtures(t *testing.T, dir, in, out string) { - t.Helper() - - writeFile(t, filepath.Join(dir, "in"), in) - writeFile(t, filepath.Join(dir, "out"), out) -} - -func mustReadFile(t *testing.T, path string) string { - t.Helper() - - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("ReadFile(%q) error = %v", path, err) - } - return string(data) -} - -func withWorkingDir(t *testing.T, dir string, fn func()) { - t.Helper() - - wd, err := os.Getwd() - if err != nil { - t.Fatalf("Getwd() error = %v", err) - } - if err := os.Chdir(dir); err != nil { - t.Fatalf("Chdir(%q) error = %v", dir, err) - } - t.Cleanup(func() { - if err := os.Chdir(wd); err != nil { - t.Fatalf("restore working directory: %v", err) - } - }) - - fn() -} - -func addFakeRecordDependencies(t *testing.T, names ...string) { - t.Helper() - - binDir := t.TempDir() - for _, name := range names { - path := filepath.Join(binDir, name) - body := "#!/bin/sh\nexit 0\n" - if name == "script" { - body = `#!/bin/sh -if [ -n "${FAKE_SCRIPT_ARGS_FILE:-}" ]; then - : > "$FAKE_SCRIPT_ARGS_FILE" - for arg in "$@"; do - printf '%s\n' "$arg" >> "$FAKE_SCRIPT_ARGS_FILE" - done -fi -in='' -out='' -cmd='' -while [ "$#" -gt 0 ]; do - case "$1" in - -I) - in="$2" - shift 2 - ;; - -O) - out="$2" - shift 2 - ;; - -c) - cmd="$2" - shift 2 - ;; - *) - shift - ;; - esac -done -if [ -n "${FAKE_SCRIPT_COMMAND_BODY_FILE:-}" ] && [ -n "$cmd" ]; then - : > "$FAKE_SCRIPT_COMMAND_BODY_FILE" - while IFS= read -r line || [ -n "$line" ]; do - printf '%s\n' "$line" >> "$FAKE_SCRIPT_COMMAND_BODY_FILE" - done < "$cmd" -fi -command_has_compare_marker=0 -if [ -n "$cmd" ] && /bin/grep -q '__MIRO_E2E_BEGIN__' "$cmd" 2>/dev/null; then - command_has_compare_marker=1 -fi -stdin_file='' -if [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] || [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ]; then - stdin_file="${TMPDIR:-/tmp}/miro-fake-script-stdin-$$" - /bin/cat > "$stdin_file" -fi -if [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ] && [ -n "$stdin_file" ]; then - /bin/cp "$stdin_file" "$FAKE_SCRIPT_CAPTURE_STDIN_FILE" -fi -if [ -n "$in" ] && [ -n "${FAKE_SCRIPT_LOG_IN+x}" ]; then - printf '%s' "$FAKE_SCRIPT_LOG_IN" > "$in" -elif [ -n "$in" ] && [ -n "$stdin_file" ]; then - /bin/cp "$stdin_file" "$in" -elif [ -n "$in" ]; then - printf '%s' 'fake recorded input -' > "$in" -fi -if [ -n "${FAKE_SCRIPT_STREAM_OUT+x}" ]; then - printf '%s' "$FAKE_SCRIPT_STREAM_OUT" -elif [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then - /bin/cat "$stdin_file" - if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then - printf '%s\n' '__MIRO_E2E_BEGIN__' - /bin/cat "$stdin_file" - fi -fi -if [ -n "${FAKE_SCRIPT_STREAM_ERR+x}" ]; then - printf '%s' "$FAKE_SCRIPT_STREAM_ERR" >&2 -fi -if [ -n "$out" ] && [ -n "${FAKE_SCRIPT_LOG_OUT+x}" ]; then - printf '%s' "$FAKE_SCRIPT_LOG_OUT" > "$out" -elif [ -n "$out" ] && [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then - { - printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] -' - /bin/cat "$stdin_file" - if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then - printf '%s\n' '__MIRO_E2E_BEGIN__' - /bin/cat "$stdin_file" - fi - printf '%s' 'Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] -' - } > "$out" -elif [ -n "$out" ]; then - printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] -fake recorded output -Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] -' > "$out" -fi -if [ -n "$stdin_file" ]; then - /bin/rm -f "$stdin_file" -fi -if [ -n "${FAKE_SCRIPT_EXIT_CODE:-}" ]; then - exit "$FAKE_SCRIPT_EXIT_CODE" -fi -exit 0 -` - } - if err := os.WriteFile(path, []byte(body), 0o755); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } - } - - t.Setenv("PATH", binDir) -} - -func withStdin(t *testing.T, input string, fn func()) { - t.Helper() - - path := filepath.Join(t.TempDir(), "stdin.txt") - if err := os.WriteFile(path, []byte(input), 0o644); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } - - file, err := os.Open(path) - if err != nil { - t.Fatalf("Open(%q) error = %v", path, err) - } - - oldStdin := os.Stdin - os.Stdin = file - t.Cleanup(func() { - os.Stdin = oldStdin - if err := file.Close(); err != nil { - t.Fatalf("close stdin file: %v", err) - } - }) - - 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" -} diff --git a/internal/miro/path_resolution_test.go b/internal/miro/path_resolution_test.go index 2382ef1..c352e61 100644 --- a/internal/miro/path_resolution_test.go +++ b/internal/miro/path_resolution_test.go @@ -5,13 +5,15 @@ import ( "path/filepath" "strings" "testing" + + "miro/internal/testutil" ) func TestResolvePathWithinTestDirAcceptsRelativePath(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - got := withWorkingDir(t, root, func() string { + got := testutil.WithWorkingDir(t, root, func() string { path, err := resolvePathWithinTestDir(testDir, filepath.Join("suite", "spec"), "record") if err != nil { t.Fatalf("resolvePathWithinTestDir() error = %v", err) @@ -29,7 +31,7 @@ func TestResolvePathWithinTestDirAcceptsExplicitTestDirPrefix(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - got := withWorkingDir(t, root, func() string { + got := testutil.WithWorkingDir(t, root, func() string { path, err := resolvePathWithinTestDir(testDir, filepath.Join("e2e", "suite", "spec"), "record") if err != nil { t.Fatalf("resolvePathWithinTestDir() error = %v", err) @@ -74,9 +76,9 @@ func TestResolvePathWithinTestDirRejectsAbsolutePathOutsideTestDir(t *testing.T) func TestResolvePathWithinTestDirAllowsPathCurrentDirectory(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - mustMkdirAll(t, testDir) + testutil.MustMkdirAll(t, testDir) - got := withWorkingDir(t, testDir, func() string { + got := testutil.WithWorkingDir(t, testDir, func() string { path, err := resolvePathWithinTestDir(testDir, ".", "record") if err != nil { t.Fatalf("resolvePathWithinTestDir() error = %v", err) diff --git a/internal/miro/post_process_test.go b/internal/miro/post_process_test.go index e339a20..fd08a56 100644 --- a/internal/miro/post_process_test.go +++ b/internal/miro/post_process_test.go @@ -3,11 +3,13 @@ package miro import ( "path/filepath" "testing" + + "miro/internal/testutil" ) func TestLoadRecordedInputTrimsTrailingNewlineAfterEOF(t *testing.T) { path := filepath.Join(t.TempDir(), "in") - writeFile(t, path, "echo hi\r"+string([]byte{eofByte})+"\n") + testutil.WriteFile(t, path, "echo hi\r"+string([]byte{eofByte})+"\n") got, err := loadRecordedInput(path) if err != nil { @@ -22,7 +24,7 @@ func TestLoadRecordedInputTrimsTrailingNewlineAfterEOF(t *testing.T) { func TestLoadRecordedInputTrimsTrailingNewlineAfterCarriageReturn(t *testing.T) { path := filepath.Join(t.TempDir(), "in") - writeFile(t, path, "echo hi\rexit\r\n") + testutil.WriteFile(t, path, "echo hi\rexit\r\n") got, err := loadRecordedInput(path) if err != nil { @@ -37,7 +39,7 @@ func TestLoadRecordedInputTrimsTrailingNewlineAfterCarriageReturn(t *testing.T) func TestLoadRecordedInputLeavesNormalTrailingNewline(t *testing.T) { path := filepath.Join(t.TempDir(), "in") - writeFile(t, path, "echo hi\n") + testutil.WriteFile(t, path, "echo hi\n") got, err := loadRecordedInput(path) if err != nil { diff --git a/internal/miro/record_test.go b/internal/miro/record_test.go index bd54ba1..f078351 100644 --- a/internal/miro/record_test.go +++ b/internal/miro/record_test.go @@ -4,23 +4,24 @@ import ( "bytes" "errors" "os" - "os/exec" "path/filepath" "strings" "testing" "time" + + "miro/internal/testutil" ) func TestRecordCreatesRelativePath(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - mustMkdirAll(t, testDir) - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) + testutil.MustMkdirAll(t, testDir) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("e2e")) mustWriteRecordShell(t, testDir) - addFakeRecordDependencies(t, "script") + testutil.AddFakeRecordDependencies(t, "script") - got := withWorkingDir(t, root, func() string { - withStdin(t, "y\n", func() {}) + got := testutil.WithWorkingDir(t, root, func() string { + testutil.WithStdin(t, "y\n", func() {}) path, err := Record(filepath.Join("a", "b", "c") + string(os.PathSeparator)) if err != nil { t.Fatalf("Record() error = %v", err) @@ -39,10 +40,10 @@ func TestRecordCreatesRelativePath(t *testing.T) { } } - if got := readFile(t, filepath.Join(want, "in")); got != "fake recorded input\n" { + if got := testutil.ReadFile(t, filepath.Join(want, "in")); got != "fake recorded input\n" { t.Fatalf("saved in = %q, want %q", got, "fake recorded input\n") } - if got := readFile(t, filepath.Join(want, "out")); got != "fake recorded output\n" { + if got := testutil.ReadFile(t, filepath.Join(want, "out")); got != "fake recorded output\n" { t.Fatalf("saved out = %q, want %q", got, "fake recorded output\n") } } @@ -50,13 +51,13 @@ func TestRecordCreatesRelativePath(t *testing.T) { func TestRecordReturnsDiscardedErrorWhenSaveDeclined(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - mustMkdirAll(t, testDir) + testutil.MustMkdirAll(t, testDir) mustWriteRecordShell(t, testDir) - addFakeRecordDependencies(t, "script") + testutil.AddFakeRecordDependencies(t, "script") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { target := filepath.Join(testDir, "a", "b", "c") - mustMkdirAll(t, target) + testutil.MustMkdirAll(t, target) return withRecordStreams(t, "n\n", func(rio recordIO) error { return recordScenario(target, recordShellPath(testDir), rio, defaultSandboxConfig()) }) @@ -78,13 +79,13 @@ func TestRecordReturnsDiscardedErrorWhenOverwriteDeclined(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") target := filepath.Join(testDir, "a", "b", "c") - mustMkdirAll(t, target) + testutil.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") + testutil.AddFakeRecordDependencies(t, "script") + testutil.WriteFile(t, filepath.Join(target, "in"), "existing in\n") + testutil.WriteFile(t, filepath.Join(target, "out"), "existing out\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { return withRecordStreams(t, "n\n", func(rio recordIO) error { return recordScenario(target, recordShellPath(testDir), rio, defaultSandboxConfig()) }) @@ -187,7 +188,7 @@ func TestRecordSessionEnvWithExtraIncludesAdditionalEntries(t *testing.T) { func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") mustWriteRecordShell(t, testDir) - addFakeRecordDependencies(t, "script") + testutil.AddFakeRecordDependencies(t, "script") argsPath := filepath.Join(t.TempDir(), "script.args") commandBodyPath := filepath.Join(t.TempDir(), "script.command") @@ -208,7 +209,7 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { t.Fatalf("runRecordSession() error = %v", err) } - args := strings.Split(strings.TrimSpace(readFile(t, argsPath)), "\n") + args := strings.Split(strings.TrimSpace(testutil.ReadFile(t, argsPath)), "\n") if len(args) != 9 { t.Fatalf("script args = %q, want 9 args", args) } @@ -225,7 +226,7 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { t.Fatalf("script args[8] = %q, want %q", args[8], shellPath) } - body := readFile(t, commandBodyPath) + body := testutil.ReadFile(t, commandBodyPath) for _, want := range []string{ "host_home=${MIRO_HOST_HOME:?}", "visible_home=${MIRO_VISIBLE_HOME:?}", @@ -244,12 +245,12 @@ func TestRunRecordSessionUsesSandboxedScriptCommand(t *testing.T) { } func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { - requireCommands(t, "script", "bwrap", "bash") + testutil.RequireCommands(t, "script", "bwrap", "bash") root := t.TempDir() testDir := filepath.Join(root, "e2e") target := filepath.Join(testDir, "suite", "spec") - mustMkdirAll(t, target) + testutil.MustMkdirAll(t, target) mustWriteRecordShell(t, testDir) sandboxConfig := map[string]string{ "visible_home": "/home/test", @@ -287,7 +288,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { writeDone <- nil }() - err = withWorkingDir(t, root, func() error { + err = testutil.WithWorkingDir(t, root, func() error { return recordScenario(target, recordShellPath(testDir), recordIO{ in: reader, out: ioDiscard{}, @@ -301,7 +302,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { t.Fatalf("write session input: %v", err) } - recordedIn := readFile(t, filepath.Join(target, "in")) + recordedIn := testutil.ReadFile(t, filepath.Join(target, "in")) if strings.Contains(recordedIn, "Script started on ") { t.Fatalf("saved in = %q, want stripped script wrapper", recordedIn) } @@ -311,7 +312,7 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) { } } - recordedOut := readFile(t, filepath.Join(target, "out")) + recordedOut := testutil.ReadFile(t, filepath.Join(target, "out")) if strings.Contains(recordedOut, "Script started on ") { t.Fatalf("saved out = %q, want stripped script wrapper", recordedOut) } @@ -331,12 +332,12 @@ 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"), validConfigContent("e2e")) - mustMkdirAll(t, testDir) - addFakeRecordDependencies(t, "script") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("e2e")) + testutil.MustMkdirAll(t, testDir) + testutil.AddFakeRecordDependencies(t, "script") target := filepath.Join(testDir, "suite", "spec") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := Record(filepath.Join("suite", "spec")) return err }) @@ -350,217 +351,3 @@ func TestRecordFailsWhenRecorderShellMissing(t *testing.T) { 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() - - path := filepath.Join(t.TempDir(), "stdin.txt") - if err := os.WriteFile(path, []byte(input), 0o644); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } - - file, err := os.Open(path) - if err != nil { - t.Fatalf("Open(%q) error = %v", path, err) - } - - t.Cleanup(func() { - if err := file.Close(); err != nil { - t.Fatalf("close record input: %v", err) - } - }) - - return fn(recordIO{ - in: file, - out: ioDiscard{}, - err: &bytes.Buffer{}, - }) -} - -func withStdin(t *testing.T, input string, fn func()) { - t.Helper() - - path := filepath.Join(t.TempDir(), "stdin.txt") - if err := os.WriteFile(path, []byte(input), 0o644); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } - - file, err := os.Open(path) - if err != nil { - t.Fatalf("Open(%q) error = %v", path, err) - } - - oldStdin := os.Stdin - os.Stdin = file - t.Cleanup(func() { - os.Stdin = oldStdin - if err := file.Close(); err != nil { - t.Fatalf("close stdin file: %v", err) - } - }) - - fn() -} - -func readFile(t *testing.T, path string) string { - t.Helper() - - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("ReadFile(%q) error = %v", path, err) - } - - return string(data) -} - -type ioDiscard struct{} - -func (ioDiscard) Write(p []byte) (int, error) { - return len(p), nil -} - -func addFakeRecordDependencies(t *testing.T, names ...string) { - t.Helper() - - binDir := t.TempDir() - for _, name := range names { - path := filepath.Join(binDir, name) - body := "#!/bin/sh\nexit 0\n" - if name == "script" { - body = `#!/bin/sh -if [ -n "${FAKE_SCRIPT_ARGS_FILE:-}" ]; then - : > "$FAKE_SCRIPT_ARGS_FILE" - for arg in "$@"; do - printf '%s\n' "$arg" >> "$FAKE_SCRIPT_ARGS_FILE" - done -fi -in='' -out='' -cmd='' -while [ "$#" -gt 0 ]; do - case "$1" in - -I) - in="$2" - shift 2 - ;; - -O) - out="$2" - shift 2 - ;; - -c) - cmd="$2" - shift 2 - ;; - *) - shift - ;; - esac -done -if [ -n "${FAKE_SCRIPT_COMMAND_BODY_FILE:-}" ] && [ -n "$cmd" ]; then - : > "$FAKE_SCRIPT_COMMAND_BODY_FILE" - while IFS= read -r line || [ -n "$line" ]; do - printf '%s\n' "$line" >> "$FAKE_SCRIPT_COMMAND_BODY_FILE" - done < "$cmd" -fi -command_has_compare_marker=0 -if [ -n "$cmd" ] && /bin/grep -q '__MIRO_E2E_BEGIN__' "$cmd" 2>/dev/null; then - command_has_compare_marker=1 -fi -stdin_file='' -if [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] || [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ]; then - stdin_file="${TMPDIR:-/tmp}/miro-fake-script-stdin-$$" - /bin/cat > "$stdin_file" -fi -if [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ] && [ -n "$stdin_file" ]; then - /bin/cp "$stdin_file" "$FAKE_SCRIPT_CAPTURE_STDIN_FILE" -fi -if [ -n "$in" ] && [ -n "${FAKE_SCRIPT_LOG_IN+x}" ]; then - printf '%s' "$FAKE_SCRIPT_LOG_IN" > "$in" -elif [ -n "$in" ] && [ -n "$stdin_file" ]; then - /bin/cp "$stdin_file" "$in" -elif [ -n "$in" ]; then - printf '%s' 'fake recorded input -' > "$in" -fi -if [ -n "${FAKE_SCRIPT_STREAM_OUT+x}" ]; then - printf '%s' "$FAKE_SCRIPT_STREAM_OUT" -elif [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then - /bin/cat "$stdin_file" - if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then - printf '%s\n' '__MIRO_E2E_BEGIN__' - /bin/cat "$stdin_file" - fi -fi -if [ -n "${FAKE_SCRIPT_STREAM_ERR+x}" ]; then - printf '%s' "$FAKE_SCRIPT_STREAM_ERR" >&2 -fi -if [ -n "$out" ] && [ -n "${FAKE_SCRIPT_LOG_OUT+x}" ]; then - printf '%s' "$FAKE_SCRIPT_LOG_OUT" > "$out" -elif [ -n "$out" ] && [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then - { - printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] -' - /bin/cat "$stdin_file" - if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then - printf '%s\n' '__MIRO_E2E_BEGIN__' - /bin/cat "$stdin_file" - fi - printf '%s' 'Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] -' - } > "$out" -elif [ -n "$out" ]; then - printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] -fake recorded output -Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] -' > "$out" -fi -if [ -n "$stdin_file" ]; then - /bin/rm -f "$stdin_file" -fi -if [ -n "${FAKE_SCRIPT_EXIT_CODE:-}" ]; then - exit "$FAKE_SCRIPT_EXIT_CODE" -fi -exit 0 -` - } - if err := os.WriteFile(path, []byte(body), 0o755); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } - } - - t.Setenv("PATH", binDir) -} - -func requireCommands(t *testing.T, names ...string) { - t.Helper() - - for _, name := range names { - if _, err := exec.LookPath(name); err != nil { - t.Skipf("missing command %q: %v", name, err) - } - } -} - -func mustWriteRecordShell(t *testing.T, testDir string) { - t.Helper() - - if err := writeRecordShell(testDir); err != nil { - 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 -} diff --git a/internal/miro/test_test.go b/internal/miro/test_test.go index bc32765..7b54962 100644 --- a/internal/miro/test_test.go +++ b/internal/miro/test_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "time" + + "miro/internal/testutil" ) func TestFormatElapsed(t *testing.T) { @@ -30,10 +32,10 @@ func TestFormatElapsed(t *testing.T) { func TestDiscoverTestScenariosFindsNestedFixturesAndSorts(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") - writeScenarioFixtures(t, filepath.Join(testDir, "nested", "b"), "echo two\n", "echo two\n") - writeScenarioFixtures(t, filepath.Join(testDir, "a"), "echo one\n", "echo one\n") - writeFile(t, filepath.Join(testDir, "shell.sh"), "#!/bin/sh\n") - writeFile(t, filepath.Join(testDir, "notes.txt"), "ignore me\n") + testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "nested", "b"), "echo two\n", "echo two\n") + testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "a"), "echo one\n", "echo one\n") + testutil.WriteFile(t, filepath.Join(testDir, "shell.sh"), "#!/bin/sh\n") + testutil.WriteFile(t, filepath.Join(testDir, "notes.txt"), "ignore me\n") got, err := discoverTestScenarios(testDir, testDir) if err != nil { @@ -63,7 +65,7 @@ func TestDiscoverTestScenariosFindsNestedFixturesAndSorts(t *testing.T) { func TestDiscoverTestScenariosRejectsMissingOutFixture(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") - writeFile(t, filepath.Join(testDir, "broken", "in"), "echo broken\n") + testutil.WriteFile(t, filepath.Join(testDir, "broken", "in"), "echo broken\n") _, err := discoverTestScenarios(testDir, testDir) if err == nil { @@ -76,7 +78,7 @@ func TestDiscoverTestScenariosRejectsMissingOutFixture(t *testing.T) { func TestDiscoverTestScenariosRejectsMissingInFixture(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") - writeFile(t, filepath.Join(testDir, "broken", "out"), "echo broken\n") + testutil.WriteFile(t, filepath.Join(testDir, "broken", "out"), "echo broken\n") _, err := discoverTestScenarios(testDir, testDir) if err == nil { @@ -88,14 +90,14 @@ func TestDiscoverTestScenariosRejectsMissingInFixture(t *testing.T) { } func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T) { - addFakeRecordDependencies(t, "script") + testutil.AddFakeRecordDependencies(t, "script") t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") testDir := filepath.Join(t.TempDir(), "e2e") shellPath := filepath.Join(testDir, "shell.sh") mustWriteRecordShell(t, testDir) scenarioDir := filepath.Join(testDir, "suite", "spec") - writeScenarioFixtures(t, scenarioDir, "echo replay\nexit\n", "echo replay\nexit\n") + testutil.WriteScenarioFixtures(t, scenarioDir, "echo replay\nexit\n", "echo replay\nexit\n") capturedInput := filepath.Join(t.TempDir(), "stdin.capture") t.Setenv("FAKE_SCRIPT_CAPTURE_STDIN_FILE", capturedInput) @@ -115,7 +117,7 @@ func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T) t.Fatalf("replayScenario() error = %v", err) } - if got := readFile(t, capturedInput); got != "echo replay\nexit\n" { + if got := testutil.ReadFile(t, capturedInput); got != "echo replay\nexit\n" { t.Fatalf("captured stdin = %q, want recorded input", got) } if stdout.String() != "" { @@ -127,14 +129,14 @@ func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T) } func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) { - addFakeRecordDependencies(t, "script") + testutil.AddFakeRecordDependencies(t, "script") t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") testDir := filepath.Join(t.TempDir(), "e2e") shellPath := filepath.Join(testDir, "shell.sh") - writeFile(t, shellPath, "#!/bin/sh\n") + testutil.WriteFile(t, shellPath, "#!/bin/sh\n") scenarioDir := filepath.Join(testDir, "suite", "spec") - writeScenarioFixtures(t, scenarioDir, "echo replay\nexit\n", "echo replay\nexit\n") + testutil.WriteScenarioFixtures(t, scenarioDir, "echo replay\nexit\n", "echo replay\nexit\n") err := replayScenario(testScenario{ dir: scenarioDir, @@ -156,7 +158,7 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) { func TestDiscoverTestScenariosUsesDisplayRootForRelativePaths(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") scopedDir := filepath.Join(testDir, "nested") - writeScenarioFixtures(t, filepath.Join(scopedDir, "b"), "echo two\n", "echo two\n") + testutil.WriteScenarioFixtures(t, filepath.Join(scopedDir, "b"), "echo two\n", "echo two\n") got, err := discoverTestScenarios(scopedDir, testDir) if err != nil { @@ -172,7 +174,7 @@ func TestDiscoverTestScenariosUsesDisplayRootForRelativePaths(t *testing.T) { func TestResolveTestDiscoveryRootRejectsMissingPath(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") - mustMkdirAll(t, testDir) + testutil.MustMkdirAll(t, testDir) _, err := resolveTestDiscoveryRoot(testDir, "missing") if err == nil { @@ -186,9 +188,9 @@ func TestResolveTestDiscoveryRootRejectsMissingPath(t *testing.T) { func TestResolveTestDiscoveryRootRejectsFile(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") - writeFile(t, filepath.Join(testDir, "case.txt"), "hello\n") + testutil.WriteFile(t, filepath.Join(testDir, "case.txt"), "hello\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := resolveTestDiscoveryRoot(testDir, filepath.Join("e2e", "case.txt")) return err }) @@ -204,11 +206,11 @@ func TestRunTestsScopedRunEmptyDirectoryFails(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") emptyDir := filepath.Join(testDir, "empty") - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("e2e")) mustWriteRecordShell(t, testDir) - mustMkdirAll(t, emptyDir) + testutil.MustMkdirAll(t, emptyDir) - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { return runTests("empty", testIO{ out: &bytes.Buffer{}, err: &bytes.Buffer{}, @@ -221,10 +223,3 @@ func TestRunTestsScopedRunEmptyDirectoryFails(t *testing.T) { t.Fatalf("runTests() error = %q, want empty-directory error", err.Error()) } } - -func writeScenarioFixtures(t *testing.T, dir, in, out string) { - t.Helper() - - writeFile(t, filepath.Join(dir, "in"), in) - writeFile(t, filepath.Join(dir, "out"), out) -} diff --git a/internal/miro/testdir_test.go b/internal/miro/testdir_test.go index 4cf84ae..66ca63c 100644 --- a/internal/miro/testdir_test.go +++ b/internal/miro/testdir_test.go @@ -2,36 +2,37 @@ package miro import ( "os" - "os/exec" "path/filepath" "strings" "testing" + + "miro/internal/testutil" ) func TestInitCreatesConfigAtProjectRoot(t *testing.T) { root := t.TempDir() - withWorkingDir(t, root, func() struct{} { + testutil.WithWorkingDir(t, root, func() struct{} { if err := Init(); err != nil { t.Fatalf("Init() error = %v", err) } return struct{}{} }) - got := readFile(t, filepath.Join(root, "miro.toml")) - if got != defaultWrittenConfig("e2e") { - t.Fatalf("config = %q, want %q", got, defaultWrittenConfig("e2e")) + got := testutil.ReadFile(t, filepath.Join(root, "miro.toml")) + if got != testutil.DefaultWrittenConfig("e2e") { + t.Fatalf("config = %q, want %q", got, testutil.DefaultWrittenConfig("e2e")) } assertRecordShell(t, filepath.Join(root, "e2e", recordShellName)) } func TestInitUsesGitRoot(t *testing.T) { root := t.TempDir() - mustGitInit(t, root) + testutil.MustGitInit(t, root) subdir := filepath.Join(root, "nested", "dir") - mustMkdirAll(t, subdir) + testutil.MustMkdirAll(t, subdir) - withWorkingDir(t, subdir, func() struct{} { + testutil.WithWorkingDir(t, subdir, func() struct{} { if err := Init(); err != nil { t.Fatalf("Init() error = %v", err) } @@ -46,30 +47,30 @@ func TestInitUsesGitRoot(t *testing.T) { func TestInitLeavesExistingValidConfigUntouchedAndRefreshesShell(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite")) - writeFile(t, filepath.Join(root, "custom", "suite", recordShellName), "outdated\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("custom/suite")) + testutil.WriteFile(t, filepath.Join(root, "custom", "suite", recordShellName), "outdated\n") - withWorkingDir(t, root, func() struct{} { + testutil.WithWorkingDir(t, root, func() struct{} { if err := Init(); err != nil { t.Fatalf("Init() error = %v", err) } return struct{}{} }) - got := readFile(t, filepath.Join(root, "miro.toml")) - if got != validConfigContent("custom/suite") { - t.Fatalf("config = %q, want %q", got, validConfigContent("custom/suite")) + got := testutil.ReadFile(t, filepath.Join(root, "miro.toml")) + if got != testutil.ValidConfigContent("custom/suite") { + t.Fatalf("config = %q, want %q", got, testutil.ValidConfigContent("custom/suite")) } - if got := readFile(t, filepath.Join(root, "custom", "suite", recordShellName)); got != buildRecordShellScript() { + if got := testutil.ReadFile(t, filepath.Join(root, "custom", "suite", recordShellName)); got != buildRecordShellScript() { t.Fatalf("shell = %q, want refreshed recorder shell", got) } } func TestInitCreatesMissingConfiguredTestDir(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite")) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("custom/suite")) - withWorkingDir(t, root, func() struct{} { + testutil.WithWorkingDir(t, root, func() struct{} { if err := Init(); err != nil { t.Fatalf("Init() error = %v", err) } @@ -81,9 +82,9 @@ func TestInitCreatesMissingConfiguredTestDir(t *testing.T) { func TestInitFailsWhenExistingConfigInvalid(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), "test_dir = \"e2e\"\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { return Init() }) if err == nil { @@ -97,10 +98,10 @@ func TestInitFailsWhenExistingConfigInvalid(t *testing.T) { func TestResolveTestDirFromConfig(t *testing.T) { root := t.TempDir() configured := filepath.Join(root, "custom", "suite") - mustMkdirAll(t, configured) - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("custom/suite")) + testutil.MustMkdirAll(t, configured) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("custom/suite")) - got := withWorkingDir(t, root, func() string { + got := testutil.WithWorkingDir(t, root, func() string { path, err := ResolveTestDir() if err != nil { t.Fatalf("ResolveTestDir() error = %v", err) @@ -116,7 +117,7 @@ func TestResolveTestDirFromConfig(t *testing.T) { func TestResolveTestDirMissingConfigFails(t *testing.T) { root := t.TempDir() - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := ResolveTestDir() return err }) @@ -131,9 +132,9 @@ func TestResolveTestDirMissingConfigFails(t *testing.T) { func TestResolveTestDirMissingTestDirFails(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), "[miro]\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := ResolveTestDir() return err }) @@ -148,9 +149,9 @@ func TestResolveTestDirMissingTestDirFails(t *testing.T) { func TestResolveTestDirEmptyTestDirFails(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"\"\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"\"\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := ResolveTestDir() return err }) @@ -165,9 +166,9 @@ func TestResolveTestDirEmptyTestDirFails(t *testing.T) { func TestResolveTestDirMalformedConfigFails(t *testing.T) { root := t.TempDir() - writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = [\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = [\n") - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := ResolveTestDir() return err }) @@ -183,9 +184,9 @@ 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"), validConfigContent("missing")) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("missing")) - got := withWorkingDir(t, root, func() string { + got := testutil.WithWorkingDir(t, root, func() string { path, err := ResolveTestDir() if err != nil { t.Fatalf("ResolveTestDir() error = %v", err) @@ -200,10 +201,10 @@ 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"), validConfigContent("case.txt")) + testutil.WriteFile(t, filepath.Join(root, "case.txt"), "hello\n") + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("case.txt")) - err := withWorkingDir(t, root, func() error { + err := testutil.WithWorkingDir(t, root, func() error { _, err := ResolveTestDir() return err }) @@ -218,13 +219,13 @@ func TestResolveTestDirConfiguredFileFails(t *testing.T) { func TestResolveTestDirUsesGitRoot(t *testing.T) { root := t.TempDir() - mustGitInit(t, root) + testutil.MustGitInit(t, root) want := filepath.Join(root, "e2e") - writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) + testutil.WriteFile(t, filepath.Join(root, "miro.toml"), testutil.ValidConfigContent("e2e")) subdir := filepath.Join(root, "nested", "dir") - mustMkdirAll(t, subdir) + testutil.MustMkdirAll(t, subdir) - got := withWorkingDir(t, subdir, func() string { + got := testutil.WithWorkingDir(t, subdir, func() string { path, err := ResolveTestDir() if err != nil { t.Fatalf("ResolveTestDir() error = %v", err) @@ -236,74 +237,3 @@ func TestResolveTestDirUsesGitRoot(t *testing.T) { t.Fatalf("ResolveTestDir() = %q, want %q", got, want) } } - -func withWorkingDir[T any](t *testing.T, dir string, fn func() T) T { - t.Helper() - - wd, err := os.Getwd() - if err != nil { - t.Fatalf("Getwd() error = %v", err) - } - if err := os.Chdir(dir); err != nil { - t.Fatalf("Chdir(%q) error = %v", dir, err) - } - t.Cleanup(func() { - if err := os.Chdir(wd); err != nil { - t.Fatalf("restore working directory: %v", err) - } - }) - - return fn() -} - -func writeFile(t *testing.T, path, content string) { - t.Helper() - - mustMkdirAll(t, filepath.Dir(path)) - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - t.Fatalf("WriteFile(%q) error = %v", path, err) - } -} - -func mustMkdirAll(t *testing.T, path string) { - t.Helper() - - if err := os.MkdirAll(path, 0o755); err != nil { - t.Fatalf("MkdirAll(%q) error = %v", path, err) - } -} - -func mustGitInit(t *testing.T, dir string) { - t.Helper() - - cmd := exec.Command("git", "init") - cmd.Dir = dir - out, err := cmd.CombinedOutput() - if err != nil { - 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) - } -} - -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" -} diff --git a/internal/miro/testhelpers_test.go b/internal/miro/testhelpers_test.go new file mode 100644 index 0000000..acccb10 --- /dev/null +++ b/internal/miro/testhelpers_test.go @@ -0,0 +1,80 @@ +package miro + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "miro/internal/testutil" +) + +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 := testutil.ReadFile(t, path); got != buildRecordShellScript() { + t.Fatalf("shell = %q, want generated recorder shell", got) + } +} + +func withRecordStreams[T any](t *testing.T, input string, fn func(recordIO) T) T { + t.Helper() + + path := filepath.Join(t.TempDir(), "stdin.txt") + testutil.WriteFile(t, path, input) + + file, err := os.Open(path) + if err != nil { + t.Fatalf("Open(%q) error = %v", path, err) + } + + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Fatalf("close record input: %v", err) + } + }) + + return fn(recordIO{ + in: file, + out: ioDiscard{}, + err: &bytes.Buffer{}, + }) +} + +type ioDiscard struct{} + +func (ioDiscard) Write(p []byte) (int, error) { + return len(p), nil +} + +func mustWriteRecordShell(t *testing.T, testDir string) { + t.Helper() + + if err := writeRecordShell(testDir); err != nil { + 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 +} diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go new file mode 100644 index 0000000..b143486 --- /dev/null +++ b/internal/testutil/testutil.go @@ -0,0 +1,273 @@ +package testutil + +import ( + "bytes" + "io" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func CaptureOutput(t *testing.T, fn func()) (string, string) { + t.Helper() + t.Setenv("NO_COLOR", "1") + + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stdout error = %v", err) + } + stderrReader, stderrWriter, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stderr error = %v", err) + } + + oldStdout := os.Stdout + oldStderr := os.Stderr + os.Stdout = stdoutWriter + os.Stderr = stderrWriter + t.Cleanup(func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + }) + + fn() + + if err := stdoutWriter.Close(); err != nil { + t.Fatalf("stdout close error = %v", err) + } + if err := stderrWriter.Close(); err != nil { + t.Fatalf("stderr close error = %v", err) + } + + var stdoutBuf bytes.Buffer + if _, err := io.Copy(&stdoutBuf, stdoutReader); err != nil { + t.Fatalf("stdout copy error = %v", err) + } + + var stderrBuf bytes.Buffer + if _, err := io.Copy(&stderrBuf, stderrReader); err != nil { + t.Fatalf("stderr copy error = %v", err) + } + + return stdoutBuf.String(), stderrBuf.String() +} + +func WriteFile(t *testing.T, path, content string) { + t.Helper() + + MustMkdirAll(t, filepath.Dir(path)) + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%q) error = %v", path, err) + } +} + +func WriteScenarioFixtures(t *testing.T, dir, in, out string) { + t.Helper() + + WriteFile(t, filepath.Join(dir, "in"), in) + WriteFile(t, filepath.Join(dir, "out"), out) +} + +func ReadFile(t *testing.T, path string) string { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile(%q) error = %v", path, err) + } + return string(data) +} + +func MustMkdirAll(t *testing.T, path string) { + t.Helper() + + if err := os.MkdirAll(path, 0o755); err != nil { + t.Fatalf("MkdirAll(%q) error = %v", path, err) + } +} + +func WithWorkingDir[T any](t *testing.T, dir string, fn func() T) T { + t.Helper() + + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd() error = %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("Chdir(%q) error = %v", dir, err) + } + t.Cleanup(func() { + if err := os.Chdir(wd); err != nil { + t.Fatalf("restore working directory: %v", err) + } + }) + + return fn() +} + +func WithStdin(t *testing.T, input string, fn func()) { + t.Helper() + + path := filepath.Join(t.TempDir(), "stdin.txt") + if err := os.WriteFile(path, []byte(input), 0o644); err != nil { + t.Fatalf("WriteFile(%q) error = %v", path, err) + } + + file, err := os.Open(path) + if err != nil { + t.Fatalf("Open(%q) error = %v", path, err) + } + + oldStdin := os.Stdin + os.Stdin = file + t.Cleanup(func() { + os.Stdin = oldStdin + if err := file.Close(); err != nil { + t.Fatalf("close stdin file: %v", err) + } + }) + + fn() +} + +func AddFakeRecordDependencies(t *testing.T, names ...string) { + t.Helper() + + binDir := t.TempDir() + for _, name := range names { + path := filepath.Join(binDir, name) + body := "#!/bin/sh\nexit 0\n" + if name == "script" { + body = `#!/bin/sh +if [ -n "${FAKE_SCRIPT_ARGS_FILE:-}" ]; then + : > "$FAKE_SCRIPT_ARGS_FILE" + for arg in "$@"; do + printf '%s\n' "$arg" >> "$FAKE_SCRIPT_ARGS_FILE" + done +fi +in='' +out='' +cmd='' +while [ "$#" -gt 0 ]; do + case "$1" in + -I) + in="$2" + shift 2 + ;; + -O) + out="$2" + shift 2 + ;; + -c) + cmd="$2" + shift 2 + ;; + *) + shift + ;; + esac +done +if [ -n "${FAKE_SCRIPT_COMMAND_BODY_FILE:-}" ] && [ -n "$cmd" ]; then + : > "$FAKE_SCRIPT_COMMAND_BODY_FILE" + while IFS= read -r line || [ -n "$line" ]; do + printf '%s\n' "$line" >> "$FAKE_SCRIPT_COMMAND_BODY_FILE" + done < "$cmd" +fi +command_has_compare_marker=0 +if [ -n "$cmd" ] && /bin/grep -q '__MIRO_E2E_BEGIN__' "$cmd" 2>/dev/null; then + command_has_compare_marker=1 +fi +stdin_file='' +if [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] || [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ]; then + stdin_file="${TMPDIR:-/tmp}/miro-fake-script-stdin-$$" + /bin/cat > "$stdin_file" +fi +if [ -n "${FAKE_SCRIPT_CAPTURE_STDIN_FILE:-}" ] && [ -n "$stdin_file" ]; then + /bin/cp "$stdin_file" "$FAKE_SCRIPT_CAPTURE_STDIN_FILE" +fi +if [ -n "$in" ] && [ -n "${FAKE_SCRIPT_LOG_IN+x}" ]; then + printf '%s' "$FAKE_SCRIPT_LOG_IN" > "$in" +elif [ -n "$in" ] && [ -n "$stdin_file" ]; then + /bin/cp "$stdin_file" "$in" +elif [ -n "$in" ]; then + printf '%s' 'fake recorded input +' > "$in" +fi +if [ -n "${FAKE_SCRIPT_STREAM_OUT+x}" ]; then + printf '%s' "$FAKE_SCRIPT_STREAM_OUT" +elif [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then + /bin/cat "$stdin_file" + if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then + printf '%s\n' '__MIRO_E2E_BEGIN__' + /bin/cat "$stdin_file" + fi +fi +if [ -n "${FAKE_SCRIPT_STREAM_ERR+x}" ]; then + printf '%s' "$FAKE_SCRIPT_STREAM_ERR" >&2 +fi +if [ -n "$out" ] && [ -n "${FAKE_SCRIPT_LOG_OUT+x}" ]; then + printf '%s' "$FAKE_SCRIPT_LOG_OUT" > "$out" +elif [ -n "$out" ] && [ "${FAKE_SCRIPT_ECHO_STDIN:-}" = "1" ] && [ -n "$stdin_file" ]; then + { + printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] +' + /bin/cat "$stdin_file" + if [ "${MIRO_COMPARE_MARKER:-0}" = "1" ] && [ "$command_has_compare_marker" = "1" ]; then + printf '%s\n' '__MIRO_E2E_BEGIN__' + /bin/cat "$stdin_file" + fi + printf '%s' 'Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] +' + } > "$out" +elif [ -n "$out" ]; then + printf '%s' 'Script started on 2026-03-18 11:13:38+00:00 [TERM="xterm-256color"] +fake recorded output +Script done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE="0"] +' > "$out" +fi +if [ -n "$stdin_file" ]; then + /bin/rm -f "$stdin_file" +fi +if [ -n "${FAKE_SCRIPT_EXIT_CODE:-}" ]; then + exit "$FAKE_SCRIPT_EXIT_CODE" +fi +exit 0 +` + } + if err := os.WriteFile(path, []byte(body), 0o755); err != nil { + t.Fatalf("WriteFile(%q) error = %v", path, err) + } + } + + t.Setenv("PATH", binDir) +} + +func RequireCommands(t *testing.T, names ...string) { + t.Helper() + + for _, name := range names { + if _, err := exec.LookPath(name); err != nil { + t.Skipf("missing command %q: %v", name, err) + } + } +} + +func MustGitInit(t *testing.T, dir string) { + t.Helper() + + cmd := exec.Command("git", "init") + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git init: %v\n%s", err, out) + } +} + +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" +}