feat: add cmd mire rewrite to rewrite all goldens from input again
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"mire/internal/mire"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newRewriteCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rewrite [path]",
|
||||
Short: "Refresh recorded CLI output fixtures",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path := ""
|
||||
if len(args) == 1 {
|
||||
path = args[0]
|
||||
}
|
||||
|
||||
if err := mire.Rewrite(path); err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -37,7 +37,7 @@ func newRootCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(newInitCommand(), newRecordCommand(), newTestCommand())
|
||||
rootCmd.AddCommand(newInitCommand(), newRecordCommand(), newRewriteCommand(), newTestCommand())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestRunShowsHelpWhenNoArgs(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"init Initialise mire in the current project",
|
||||
"record Record a new CLI scenario",
|
||||
"rewrite Refresh recorded CLI output fixtures",
|
||||
"test Replay recorded CLI scenarios",
|
||||
} {
|
||||
if !strings.Contains(stdout, want) {
|
||||
@@ -129,6 +130,51 @@ func TestRunRecord(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunRewrite(t *testing.T) {
|
||||
testutil.AddFakeRecordDependencies(t, "bwrap", "bash")
|
||||
|
||||
root := t.TempDir()
|
||||
testutil.WriteScenarioFixtures(t, filepath.Join(root, "e2e", "suite", "spec"), "echo rewrite\nexit\n", "stale output\n")
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
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 := testutil.CaptureOutput(t, func() {
|
||||
if got := Run([]string{"rewrite"}); got != 0 {
|
||||
t.Fatalf("Run() code = %d, want %d", got, 0)
|
||||
}
|
||||
})
|
||||
|
||||
for _, want := range []string{
|
||||
"RUN suite/spec",
|
||||
"PASS suite/spec (",
|
||||
"Summary: total=1 passed=1 failed=0",
|
||||
} {
|
||||
if !strings.Contains(stdout, want) {
|
||||
t.Fatalf("stdout = %q, want substring %q", stdout, want)
|
||||
}
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Fatalf("stderr = %q, want empty", stderr)
|
||||
}
|
||||
return struct{}{}
|
||||
})
|
||||
|
||||
if got := testutil.ReadFile(t, filepath.Join(root, "e2e", "suite", "spec", "out")); got == "stale output\n" {
|
||||
t.Fatalf("rewrite left stale output unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunInitFailsWhenDependenciesMissing(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("PATH", "")
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package mire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mire/internal/script"
|
||||
)
|
||||
|
||||
type replaySuite struct {
|
||||
shellPath string
|
||||
scenarios []testScenario
|
||||
sandboxConfig map[string]string
|
||||
mounts []string
|
||||
paths []string
|
||||
}
|
||||
|
||||
func loadReplaySuite(path string) (replaySuite, error) {
|
||||
root, err := currentProjectRoot()
|
||||
if err != nil {
|
||||
return replaySuite{}, err
|
||||
}
|
||||
|
||||
cfg, err := readConfigFromRoot(root)
|
||||
if err != nil {
|
||||
return replaySuite{}, fmt.Errorf("failed to resolve test directory: %v", err)
|
||||
}
|
||||
|
||||
testDir, err := resolveTestDirFromConfig(root, cfg)
|
||||
if err != nil {
|
||||
return replaySuite{}, fmt.Errorf("failed to resolve test directory: %v", err)
|
||||
}
|
||||
|
||||
discoveryRoot, err := resolveTestDiscoveryRoot(testDir, path)
|
||||
if err != nil {
|
||||
return replaySuite{}, err
|
||||
}
|
||||
|
||||
shellPath, err := resolveRecordShell(testDir)
|
||||
if err != nil {
|
||||
return replaySuite{}, err
|
||||
}
|
||||
|
||||
scenarios, err := discoverTestScenarios(discoveryRoot, testDir)
|
||||
if err != nil {
|
||||
return replaySuite{}, err
|
||||
}
|
||||
if len(scenarios) == 0 {
|
||||
return replaySuite{}, fmt.Errorf("no test scenarios found in %q", discoveryRoot)
|
||||
}
|
||||
|
||||
return replaySuite{
|
||||
shellPath: shellPath,
|
||||
scenarios: scenarios,
|
||||
sandboxConfig: cfg.Sandbox,
|
||||
mounts: cfg.Mounts,
|
||||
paths: cfg.Paths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func replayScenarioOutput(scenario testScenario, shellPath string, sandboxConfig map[string]string, mounts, paths []string) ([]byte, error) {
|
||||
input, err := loadRecordedInput(scenario.inPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read recorded input: %v", err)
|
||||
}
|
||||
|
||||
_, rawOut, cleanupFiles, err := newRecordFiles()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare replay files: %v", err)
|
||||
}
|
||||
defer cleanupFiles()
|
||||
|
||||
sandbox, cleanupSandbox, err := newRecordSandbox()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare replay sandbox: %v", err)
|
||||
}
|
||||
defer cleanupSandbox()
|
||||
|
||||
rawOutFile, err := os.Create(rawOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare replay output: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(shellPath)
|
||||
cmd.Dir = scenario.dir
|
||||
cmd.Env = recordSessionEnvWithExtra(sandbox, sandboxConfig, mounts, paths, scenario.setupScripts, map[string]string{
|
||||
compareMarkerEnvName: compareMarkerEnabledValue,
|
||||
})
|
||||
|
||||
ready := make(chan struct{})
|
||||
promptWriter := newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
||||
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
||||
defer promptTimeout.Stop()
|
||||
|
||||
replayResult := script.Replay(script.ReplayRequest{
|
||||
Cmd: cmd,
|
||||
Input: input,
|
||||
InputReady: ready,
|
||||
OutputLog: promptWriter,
|
||||
})
|
||||
if err := rawOutFile.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close replay output: %v", err)
|
||||
}
|
||||
if !promptWriter.seen {
|
||||
if err := replayResult.Err(); err != nil {
|
||||
return nil, fmt.Errorf("replay failed: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath)
|
||||
}
|
||||
if replayResult.OutputErr != nil {
|
||||
return nil, fmt.Errorf("replay failed: %v", replayResult.OutputErr)
|
||||
}
|
||||
if replayResult.InputErr != nil {
|
||||
return nil, fmt.Errorf("replay failed: %v", replayResult.InputErr)
|
||||
}
|
||||
|
||||
got, err := loadRecordedOutput(rawOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read replay output: %v", err)
|
||||
}
|
||||
|
||||
got, err = trimReplayOutputToMarker(got, shellPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return got, nil
|
||||
}
|
||||
|
||||
func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
||||
idx := bytes.Index(data, []byte(compareOutputMarker))
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf(
|
||||
"missing replay start marker %q in replay output; rerun `mire init` or refresh %q",
|
||||
compareOutputMarker,
|
||||
shellPath,
|
||||
)
|
||||
}
|
||||
|
||||
data = data[idx+len(compareOutputMarker):]
|
||||
switch {
|
||||
case bytes.HasPrefix(data, []byte("\r\n")):
|
||||
return data[2:], nil
|
||||
case bytes.HasPrefix(data, []byte("\n")):
|
||||
return data[1:], nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
type replayPromptWriter struct {
|
||||
dst io.Writer
|
||||
marker []byte
|
||||
tail []byte
|
||||
ready chan struct{}
|
||||
once sync.Once
|
||||
seen bool
|
||||
}
|
||||
|
||||
func newReplayPromptWriter(dst io.Writer, marker string, ready chan struct{}) *replayPromptWriter {
|
||||
return &replayPromptWriter{
|
||||
dst: dst,
|
||||
marker: []byte(marker),
|
||||
ready: ready,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.dst.Write(p)
|
||||
if n > 0 {
|
||||
w.observe(p[:n])
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) observe(p []byte) {
|
||||
if w.seen || len(w.marker) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]byte, 0, len(w.tail)+len(p))
|
||||
data = append(data, w.tail...)
|
||||
data = append(data, p...)
|
||||
|
||||
if bytes.Contains(data, w.marker) {
|
||||
w.seen = true
|
||||
w.release()
|
||||
}
|
||||
|
||||
keep := len(w.marker) - 1
|
||||
if keep <= 0 {
|
||||
return
|
||||
}
|
||||
if len(data) <= keep {
|
||||
w.tail = append(w.tail[:0], data...)
|
||||
return
|
||||
}
|
||||
|
||||
w.tail = append(w.tail[:0], data[len(data)-keep:]...)
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) release() {
|
||||
w.once.Do(func() {
|
||||
close(w.ready)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package mire
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"mire/internal/output"
|
||||
)
|
||||
|
||||
// Rewrite replays scenarios and refreshes their recorded output fixtures.
|
||||
func Rewrite(path string) error {
|
||||
return rewrite(path, testIO{
|
||||
out: os.Stdout,
|
||||
err: os.Stderr,
|
||||
})
|
||||
}
|
||||
|
||||
func rewrite(path string, tio testIO) error {
|
||||
suite, err := loadReplaySuite(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
summary := testSummary{total: len(suite.scenarios)}
|
||||
for _, scenario := range suite.scenarios {
|
||||
output.Fprintf(tio.out, "%s %s\n", output.Label("RUN", output.Info), scenario.relPath)
|
||||
|
||||
start := time.Now()
|
||||
got, err := replayScenarioOutput(scenario, suite.shellPath, suite.sandboxConfig, suite.mounts, suite.paths)
|
||||
if err != nil {
|
||||
elapsed := time.Since(start)
|
||||
summary.failed++
|
||||
output.Fprintf(tio.out, "%s %s (%s): %v\n", output.Label("FAIL", output.Fail), scenario.relPath, formatElapsed(elapsed), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.WriteFile(scenario.outPath, got, 0o644); err != nil {
|
||||
elapsed := time.Since(start)
|
||||
summary.failed++
|
||||
output.Fprintf(tio.out, "%s %s (%s): failed to write expected output: %v\n", output.Label("FAIL", output.Fail), scenario.relPath, formatElapsed(elapsed), err)
|
||||
continue
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
summary.passed++
|
||||
output.Fprintf(tio.out, "%s %s (%s)\n", output.Label("PASS", output.Pass), scenario.relPath, formatElapsed(elapsed))
|
||||
}
|
||||
|
||||
writeScenarioSummary(tio.out, summary)
|
||||
|
||||
if summary.failed > 0 {
|
||||
return fmt.Errorf("%d scenario(s) failed to rewrite", summary.failed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package mire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"mire/internal/testutil"
|
||||
)
|
||||
|
||||
func TestRewriteScopedRunOnlyUpdatesSelectedSubtree(t *testing.T) {
|
||||
testutil.AddFakeRecordDependencies(t, "bwrap", "bash")
|
||||
|
||||
root := t.TempDir()
|
||||
testDir := filepath.Join(root, "e2e")
|
||||
testutil.WriteValidConfig(t, filepath.Join(root, "mire.toml"), "e2e")
|
||||
mustWriteRecordShell(t, testDir)
|
||||
testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "a"), "echo root\nexit\n", "stale root\n")
|
||||
testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "nested", "b"), "echo nested\nexit\n", "stale nested\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := testutil.WithWorkingDir(t, root, func() error {
|
||||
return rewrite("nested", testIO{
|
||||
out: &stdout,
|
||||
err: &stderr,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("rewrite() error = %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(stdout.String(), "RUN a") {
|
||||
t.Fatalf("stdout = %q, want scoped rewrite to skip root scenario", stdout.String())
|
||||
}
|
||||
if got := testutil.ReadFile(t, filepath.Join(testDir, "a", "out")); got != "stale root\n" {
|
||||
t.Fatalf("root scenario out = %q, want unchanged stale output", got)
|
||||
}
|
||||
if got := testutil.ReadFile(t, filepath.Join(testDir, "nested", "b", "out")); got == "stale nested\n" {
|
||||
t.Fatalf("nested scenario out = %q, want rewritten output", got)
|
||||
}
|
||||
if stderr.String() != "" {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteContinuesAfterFailureAndReturnsError(t *testing.T) {
|
||||
testutil.RequireCommands(t, "bwrap", "bash")
|
||||
|
||||
root := t.TempDir()
|
||||
testDir := filepath.Join(root, "e2e")
|
||||
testutil.WriteValidConfig(t, filepath.Join(root, "mire.toml"), "e2e")
|
||||
mustWriteRecordShell(t, testDir)
|
||||
testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "bad"), "echo bad\nexit\n", "stale bad\n")
|
||||
testutil.WriteFile(t, filepath.Join(testDir, "bad", setupScriptName), "exit 1\n")
|
||||
testutil.WriteScenarioFixtures(t, filepath.Join(testDir, "ok"), "echo ok\nexit\n", "stale ok\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := testutil.WithWorkingDir(t, root, func() error {
|
||||
return rewrite("", testIO{
|
||||
out: &stdout,
|
||||
err: &stderr,
|
||||
})
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("rewrite() error = nil, want failure summary error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "1 scenario(s) failed to rewrite") {
|
||||
t.Fatalf("rewrite() error = %q, want failed rewrite summary", err.Error())
|
||||
}
|
||||
for _, want := range []string{
|
||||
"FAIL bad (",
|
||||
"PASS ok (",
|
||||
"Summary: total=2 passed=1 failed=1",
|
||||
} {
|
||||
if !strings.Contains(stdout.String(), want) {
|
||||
t.Fatalf("stdout = %q, want substring %q", stdout.String(), want)
|
||||
}
|
||||
}
|
||||
if got := testutil.ReadFile(t, filepath.Join(testDir, "bad", "out")); got != "stale bad\n" {
|
||||
t.Fatalf("bad scenario out = %q, want unchanged stale output", got)
|
||||
}
|
||||
if got := testutil.ReadFile(t, filepath.Join(testDir, "ok", "out")); got == "stale ok\n" {
|
||||
t.Fatalf("ok scenario out = %q, want rewritten output", got)
|
||||
}
|
||||
if stderr.String() != "" {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
+13
-177
@@ -7,14 +7,11 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mire/internal/output"
|
||||
"mire/internal/script"
|
||||
)
|
||||
|
||||
type testIO struct {
|
||||
@@ -69,45 +66,17 @@ func RunTests(path string) error {
|
||||
}
|
||||
|
||||
func runTests(path string, tio testIO) error {
|
||||
root, err := currentProjectRoot()
|
||||
suite, err := loadReplaySuite(path)
|
||||
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)
|
||||
}
|
||||
|
||||
discoveryRoot, err := resolveTestDiscoveryRoot(testDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shellPath, err := resolveRecordShell(testDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenarios, err := discoverTestScenarios(discoveryRoot, testDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(scenarios) == 0 {
|
||||
return fmt.Errorf("no test scenarios found in %q", discoveryRoot)
|
||||
}
|
||||
|
||||
summary := testSummary{total: len(scenarios)}
|
||||
for _, scenario := range scenarios {
|
||||
summary := testSummary{total: len(suite.scenarios)}
|
||||
for _, scenario := range suite.scenarios {
|
||||
output.Fprintf(tio.out, "%s %s\n", output.Label("RUN", output.Info), scenario.relPath)
|
||||
|
||||
start := time.Now()
|
||||
if err := replayScenario(scenario, shellPath, tio, cfg.Sandbox, cfg.Mounts, cfg.Paths); err != nil {
|
||||
if err := replayScenario(scenario, suite.shellPath, suite.sandboxConfig, suite.mounts, suite.paths); err != nil {
|
||||
elapsed := time.Since(start)
|
||||
summary.failed++
|
||||
output.Fprintf(tio.out, "%s %s (%s): %v\n", output.Label("FAIL", output.Fail), scenario.relPath, formatElapsed(elapsed), err)
|
||||
@@ -123,21 +92,25 @@ func runTests(path string, tio testIO) error {
|
||||
output.Fprintf(tio.out, "%s %s (%s)\n", output.Label("PASS", output.Pass), scenario.relPath, formatElapsed(elapsed))
|
||||
}
|
||||
|
||||
writeScenarioSummary(tio.out, summary)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeScenarioSummary(w io.Writer, summary testSummary) {
|
||||
summaryColor := output.Pass
|
||||
if summary.failed > 0 {
|
||||
summaryColor = output.Fail
|
||||
}
|
||||
|
||||
output.Fprintf(
|
||||
tio.out,
|
||||
w,
|
||||
"%s\n",
|
||||
output.Label(
|
||||
fmt.Sprintf("Summary: total=%d passed=%d failed=%d", summary.total, summary.passed, summary.failed),
|
||||
summaryColor,
|
||||
),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatElapsed(elapsed time.Duration) string {
|
||||
@@ -236,67 +209,8 @@ func discoverTestScenarios(discoveryRoot, displayRoot string) ([]testScenario, e
|
||||
return scenarios, nil
|
||||
}
|
||||
|
||||
func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxConfig map[string]string, mounts, paths []string) error {
|
||||
input, err := loadRecordedInput(scenario.inPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read recorded input: %v", err)
|
||||
}
|
||||
|
||||
_, rawOut, cleanupFiles, err := newRecordFiles()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare replay files: %v", err)
|
||||
}
|
||||
defer cleanupFiles()
|
||||
|
||||
sandbox, cleanupSandbox, err := newRecordSandbox()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare replay sandbox: %v", err)
|
||||
}
|
||||
defer cleanupSandbox()
|
||||
|
||||
rawOutFile, err := os.Create(rawOut)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare replay output: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(shellPath)
|
||||
cmd.Dir = scenario.dir
|
||||
cmd.Env = recordSessionEnvWithExtra(sandbox, sandboxConfig, mounts, paths, scenario.setupScripts, map[string]string{
|
||||
compareMarkerEnvName: compareMarkerEnabledValue,
|
||||
})
|
||||
|
||||
ready := make(chan struct{})
|
||||
promptWriter := newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
||||
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
||||
defer promptTimeout.Stop()
|
||||
|
||||
replayResult := script.Replay(script.ReplayRequest{
|
||||
Cmd: cmd,
|
||||
Input: input,
|
||||
InputReady: ready,
|
||||
OutputLog: promptWriter,
|
||||
})
|
||||
if err := rawOutFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close replay output: %v", err)
|
||||
}
|
||||
if !promptWriter.seen {
|
||||
if err := replayResult.Err(); err != nil {
|
||||
return fmt.Errorf("replay failed: %v", err)
|
||||
}
|
||||
return fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath)
|
||||
}
|
||||
if replayResult.OutputErr != nil {
|
||||
return fmt.Errorf("replay failed: %v", replayResult.OutputErr)
|
||||
}
|
||||
if replayResult.InputErr != nil {
|
||||
return fmt.Errorf("replay failed: %v", replayResult.InputErr)
|
||||
}
|
||||
|
||||
got, err := loadRecordedOutput(rawOut)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read replay output: %v", err)
|
||||
}
|
||||
got, err = trimReplayOutputToMarker(got, shellPath)
|
||||
func replayScenario(scenario testScenario, shellPath string, sandboxConfig map[string]string, mounts, paths []string) error {
|
||||
got, err := replayScenarioOutput(scenario, shellPath, sandboxConfig, mounts, paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -317,81 +231,3 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
||||
idx := bytes.Index(data, []byte(compareOutputMarker))
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf(
|
||||
"missing replay start marker %q in replay output; rerun `mire init` or refresh %q",
|
||||
compareOutputMarker,
|
||||
shellPath,
|
||||
)
|
||||
}
|
||||
|
||||
data = data[idx+len(compareOutputMarker):]
|
||||
switch {
|
||||
case bytes.HasPrefix(data, []byte("\r\n")):
|
||||
return data[2:], nil
|
||||
case bytes.HasPrefix(data, []byte("\n")):
|
||||
return data[1:], nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
type replayPromptWriter struct {
|
||||
dst io.Writer
|
||||
marker []byte
|
||||
tail []byte
|
||||
ready chan struct{}
|
||||
once sync.Once
|
||||
seen bool
|
||||
}
|
||||
|
||||
func newReplayPromptWriter(dst io.Writer, marker string, ready chan struct{}) *replayPromptWriter {
|
||||
return &replayPromptWriter{
|
||||
dst: dst,
|
||||
marker: []byte(marker),
|
||||
ready: ready,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.dst.Write(p)
|
||||
if n > 0 {
|
||||
w.observe(p[:n])
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) observe(p []byte) {
|
||||
if w.seen || len(w.marker) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]byte, 0, len(w.tail)+len(p))
|
||||
data = append(data, w.tail...)
|
||||
data = append(data, p...)
|
||||
|
||||
if bytes.Contains(data, w.marker) {
|
||||
w.seen = true
|
||||
w.release()
|
||||
}
|
||||
|
||||
keep := len(w.marker) - 1
|
||||
if keep <= 0 {
|
||||
return
|
||||
}
|
||||
if len(data) <= keep {
|
||||
w.tail = append(w.tail[:0], data...)
|
||||
return
|
||||
}
|
||||
|
||||
w.tail = append(w.tail[:0], data[len(data)-keep:]...)
|
||||
}
|
||||
|
||||
func (w *replayPromptWriter) release() {
|
||||
w.once.Do(func() {
|
||||
close(w.ready)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,28 +99,16 @@ func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T)
|
||||
scenarioDir := filepath.Join(testDir, "suite", "spec")
|
||||
testutil.WriteScenarioFixtures(t, scenarioDir, "echo replay\nexit\n", "echo replay\r\nexit\r\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := replayScenario(testScenario{
|
||||
dir: scenarioDir,
|
||||
relPath: filepath.Join("suite", "spec"),
|
||||
inPath: filepath.Join(scenarioDir, "in"),
|
||||
outPath: filepath.Join(scenarioDir, "out"),
|
||||
setupScripts: nil,
|
||||
}, shellPath, testIO{
|
||||
out: &stdout,
|
||||
err: &stderr,
|
||||
}, defaultSandboxConfig(), nil, nil)
|
||||
}, shellPath, defaultSandboxConfig(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("replayScenario() error = %v", err)
|
||||
}
|
||||
|
||||
if stdout.String() != "" {
|
||||
t.Fatalf("stdout = %q, want empty", stdout.String())
|
||||
}
|
||||
if stderr.String() != "" {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayScenarioWaitsForPromptReadyMarkerBeforeSendingInput(t *testing.T) {
|
||||
@@ -143,10 +131,7 @@ func TestReplayScenarioWaitsForPromptReadyMarkerBeforeSendingInput(t *testing.T)
|
||||
inPath: filepath.Join(scenarioDir, "in"),
|
||||
outPath: filepath.Join(scenarioDir, "out"),
|
||||
setupScripts: nil,
|
||||
}, shellPath, testIO{
|
||||
out: &bytes.Buffer{},
|
||||
err: &bytes.Buffer{},
|
||||
}, defaultSandboxConfig(), nil, nil)
|
||||
}, shellPath, defaultSandboxConfig(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("replayScenario() error = %v", err)
|
||||
}
|
||||
@@ -168,10 +153,7 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
|
||||
inPath: filepath.Join(scenarioDir, "in"),
|
||||
outPath: filepath.Join(scenarioDir, "out"),
|
||||
setupScripts: nil,
|
||||
}, shellPath, testIO{
|
||||
out: &bytes.Buffer{},
|
||||
err: &bytes.Buffer{},
|
||||
}, defaultSandboxConfig(), nil, nil)
|
||||
}, shellPath, defaultSandboxConfig(), nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("replayScenario() error = nil, want error")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user