fix: use __MIRE_PROMPT_READY__ as the common marker
This commit is contained in:
@@ -15,7 +15,7 @@ const recordShellName = "shell.sh"
|
||||
const (
|
||||
compareMarkerEnvName = "MIRE_COMPARE_MARKER"
|
||||
compareMarkerEnabledValue = "1"
|
||||
compareOutputMarker = "__MIRE_E2E_BEGIN__"
|
||||
compareOutputMarker = "__MIRE_PROMPT_READY__"
|
||||
)
|
||||
|
||||
//go:embed record_shell.sh
|
||||
|
||||
@@ -19,11 +19,21 @@ for path in /tmp/mire-setup-scripts/*.sh; do
|
||||
source "$path"
|
||||
cd "${HOME:?}"
|
||||
done
|
||||
EOF
|
||||
|
||||
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
|
||||
printf '__MIRE_E2E_BEGIN__\n'
|
||||
__mire_prompt_ready_original=${PROMPT_COMMAND-}
|
||||
__mire_prompt_ready() {
|
||||
printf '__MIRE_PROMPT_READY__\n'
|
||||
if [ -n "${__mire_prompt_ready_original:-}" ]; then
|
||||
PROMPT_COMMAND=$__mire_prompt_ready_original
|
||||
eval "$PROMPT_COMMAND"
|
||||
else
|
||||
unset PROMPT_COMMAND
|
||||
fi
|
||||
}
|
||||
PROMPT_COMMAND=__mire_prompt_ready
|
||||
fi
|
||||
EOF
|
||||
|
||||
set -- \
|
||||
--ro-bind / / \
|
||||
|
||||
@@ -132,7 +132,8 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
|
||||
`set -- "$@" --ro-bind "$host_path" "$visible_path"`,
|
||||
"${MIRE_SETUP_SCRIPTS-}",
|
||||
`if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then`,
|
||||
"printf '__MIRE_E2E_BEGIN__\\n'",
|
||||
"printf '__MIRE_PROMPT_READY__\\n'",
|
||||
"PROMPT_COMMAND=__mire_prompt_ready",
|
||||
"--bind \"$host_home\" \"$visible_home\"",
|
||||
"--bind \"$host_tmp\" '/tmp'",
|
||||
"--setenv HOME \"$visible_home\"",
|
||||
@@ -147,6 +148,9 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
|
||||
t.Fatalf("wrapper = %q, want substring %q", body, want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "printf '__MIRE_E2E_BEGIN__\\n'") {
|
||||
t.Fatalf("wrapper = %q, want old compare marker removed", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordSessionEnvIncludesConfiguredSandboxEnv(t *testing.T) {
|
||||
|
||||
+13
-35
@@ -47,8 +47,8 @@ type testMismatchError struct {
|
||||
}
|
||||
|
||||
const (
|
||||
replayPromptReadyMarker = "\x1b[?2004h$ "
|
||||
replayPromptReadyTimeout = 30 * time.Second
|
||||
replayPromptReadyMarker = compareOutputMarker
|
||||
replayPromptReadyTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
func (e *testMismatchError) Error() string {
|
||||
@@ -260,24 +260,16 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
|
||||
compareMarkerEnvName: compareMarkerEnabledValue,
|
||||
})
|
||||
|
||||
outputLog := io.Writer(rawOutFile)
|
||||
var promptWriter *replayPromptWriter
|
||||
var promptReady <-chan struct{}
|
||||
if replayNeedsInteractivePrompt(input) {
|
||||
ready := make(chan struct{})
|
||||
promptWriter = newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
||||
outputLog = promptWriter
|
||||
promptReady = ready
|
||||
|
||||
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
||||
defer promptTimeout.Stop()
|
||||
}
|
||||
ready := make(chan struct{})
|
||||
promptWriter := newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
||||
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
||||
defer promptTimeout.Stop()
|
||||
|
||||
if err := screen.Replay(screen.ReplayRequest{
|
||||
Cmd: cmd,
|
||||
Input: input,
|
||||
InputReady: promptReady,
|
||||
OutputLog: outputLog,
|
||||
Cmd: cmd,
|
||||
Input: input,
|
||||
InputReady: ready,
|
||||
OutputLog: promptWriter,
|
||||
}); err != nil {
|
||||
rawOutFile.Close()
|
||||
return fmt.Errorf("replay failed: %v", err)
|
||||
@@ -285,8 +277,8 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
|
||||
if err := rawOutFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close replay output: %v", err)
|
||||
}
|
||||
if promptWriter != nil && !promptWriter.seen {
|
||||
return fmt.Errorf("replay shell never reached the initial interactive prompt; inspect %q or rerun `mire init`", shellPath)
|
||||
if !promptWriter.seen {
|
||||
return fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath)
|
||||
}
|
||||
|
||||
got, err := loadRecordedOutput(rawOut)
|
||||
@@ -328,7 +320,7 @@ func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
||||
idx := bytes.Index(data, []byte(compareOutputMarker))
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf(
|
||||
"missing compare marker %q in replay output; rerun `mire init` or refresh %q",
|
||||
"missing replay start marker %q in replay output; rerun `mire init` or refresh %q",
|
||||
compareOutputMarker,
|
||||
shellPath,
|
||||
)
|
||||
@@ -345,20 +337,6 @@ func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func replayNeedsInteractivePrompt(input []byte) bool {
|
||||
for _, b := range input {
|
||||
switch b {
|
||||
case '\r', '\n', eofByte:
|
||||
continue
|
||||
}
|
||||
if b < 0x20 || b == 0x7f || b == 0x1b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type replayPromptWriter struct {
|
||||
dst io.Writer
|
||||
marker []byte
|
||||
|
||||
+31
-23
@@ -123,6 +123,35 @@ func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayScenarioWaitsForPromptReadyMarkerBeforeSendingInput(t *testing.T) {
|
||||
testutil.RequireCommands(t, "bwrap", "bash")
|
||||
|
||||
testDir := filepath.Join(t.TempDir(), "e2e")
|
||||
shellPath := filepath.Join(testDir, "shell.sh")
|
||||
mustWriteRecordShell(t, testDir)
|
||||
scenarioDir := filepath.Join(testDir, "suite", "spec")
|
||||
testutil.WriteScenarioFixtures(
|
||||
t,
|
||||
scenarioDir,
|
||||
"echo ab\x7fc\nexit\n",
|
||||
"\x1b[?2004h$ echo ab\b \bc\r\n\x1b[?2004l\rac\r\n\x1b[?2004h$ exit\r\n\x1b[?2004l\rexit\r\n",
|
||||
)
|
||||
|
||||
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: &bytes.Buffer{},
|
||||
err: &bytes.Buffer{},
|
||||
}, defaultSandboxConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("replayScenario() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
|
||||
testDir := filepath.Join(t.TempDir(), "e2e")
|
||||
shellPath := filepath.Join(testDir, "shell.sh")
|
||||
@@ -146,29 +175,8 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("replayScenario() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing compare marker") || !strings.Contains(err.Error(), "rerun `mire init`") {
|
||||
t.Fatalf("replayScenario() error = %q, want compare marker refresh hint", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayNeedsInteractivePrompt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{name: "plain text", input: "echo hello\nexit\n", want: false},
|
||||
{name: "backspace", input: "echo ab\x7fc\n", want: true},
|
||||
{name: "escape sequence", input: "echo a\x1b[D\n", want: true},
|
||||
{name: "eof suffix only", input: "echo hello\r" + string([]byte{eofByte}), want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := replayNeedsInteractivePrompt([]byte(tt.input)); got != tt.want {
|
||||
t.Fatalf("replayNeedsInteractivePrompt(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(err.Error(), "__MIRE_PROMPT_READY__") || !strings.Contains(err.Error(), "rerun `mire init`") {
|
||||
t.Fatalf("replayScenario() error = %q, want prompt-ready marker refresh hint", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -15,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRows = 24
|
||||
defaultCols = 80
|
||||
terminalEOF = byte(0x04)
|
||||
defaultRows = 24
|
||||
defaultCols = 80
|
||||
terminalEOF = byte(0x04)
|
||||
replayReadySettleDelay = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
type RecordRequest struct {
|
||||
@@ -30,10 +32,10 @@ type RecordRequest struct {
|
||||
}
|
||||
|
||||
type ReplayRequest struct {
|
||||
Cmd *exec.Cmd
|
||||
Input []byte
|
||||
Cmd *exec.Cmd
|
||||
Input []byte
|
||||
InputReady <-chan struct{}
|
||||
OutputLog io.Writer
|
||||
OutputLog io.Writer
|
||||
}
|
||||
|
||||
func Record(req RecordRequest) error {
|
||||
@@ -91,9 +93,11 @@ func Replay(req ReplayRequest) error {
|
||||
defer ptmx.Close()
|
||||
|
||||
outputDone := copyAsync(combineWriters(req.OutputLog), ptmx)
|
||||
inputDone := copyAsyncWhenReady(ptmx, bytes.NewReader(replayInput(req.Input)), req.InputReady)
|
||||
processDone := make(chan struct{})
|
||||
inputDone := copyAsyncWhenReady(ptmx, bytes.NewReader(replayInput(req.Input)), req.InputReady, processDone)
|
||||
|
||||
waitErr := req.Cmd.Wait()
|
||||
close(processDone)
|
||||
ptmx.Close()
|
||||
|
||||
outputErr := <-outputDone
|
||||
@@ -129,11 +133,17 @@ func copyAsync(dst io.Writer, src io.Reader) <-chan error {
|
||||
return done
|
||||
}
|
||||
|
||||
func copyAsyncWhenReady(dst io.Writer, src io.Reader, ready <-chan struct{}) <-chan error {
|
||||
func copyAsyncWhenReady(dst io.Writer, src io.Reader, ready <-chan struct{}, stop <-chan struct{}) <-chan error {
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
if ready != nil {
|
||||
<-ready
|
||||
select {
|
||||
case <-ready:
|
||||
time.Sleep(replayReadySettleDelay)
|
||||
case <-stop:
|
||||
done <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err := io.Copy(dst, src)
|
||||
done <- normalizeCopyError(err)
|
||||
|
||||
@@ -78,10 +78,10 @@ func TestReplayWaitsForInputReadySignal(t *testing.T) {
|
||||
}()
|
||||
|
||||
if err := Replay(ReplayRequest{
|
||||
Cmd: cmd,
|
||||
Input: []byte("hello\n"),
|
||||
Cmd: cmd,
|
||||
Input: []byte("hello\n"),
|
||||
InputReady: ready,
|
||||
OutputLog: &outputLog,
|
||||
OutputLog: &outputLog,
|
||||
}); err != nil {
|
||||
t.Fatalf("Replay() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -388,8 +388,12 @@ while [ "$#" -gt 0 ]; do
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
/bin/stty -echo 2>/dev/null || true
|
||||
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
|
||||
/bin/stty -echo 2>/dev/null || true
|
||||
printf '%s\n' '__MIRE_PROMPT_READY__'
|
||||
else
|
||||
/bin/stty -echo 2>/dev/null || true
|
||||
fi
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
printf '%s\n' "$line"
|
||||
if [ "$line" = "exit" ]; then
|
||||
|
||||
Reference in New Issue
Block a user