fix: use __MIRE_PROMPT_READY__ as the common marker
This commit is contained in:
@@ -15,7 +15,7 @@ const recordShellName = "shell.sh"
|
|||||||
const (
|
const (
|
||||||
compareMarkerEnvName = "MIRE_COMPARE_MARKER"
|
compareMarkerEnvName = "MIRE_COMPARE_MARKER"
|
||||||
compareMarkerEnabledValue = "1"
|
compareMarkerEnabledValue = "1"
|
||||||
compareOutputMarker = "__MIRE_E2E_BEGIN__"
|
compareOutputMarker = "__MIRE_PROMPT_READY__"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed record_shell.sh
|
//go:embed record_shell.sh
|
||||||
|
|||||||
@@ -19,11 +19,21 @@ for path in /tmp/mire-setup-scripts/*.sh; do
|
|||||||
source "$path"
|
source "$path"
|
||||||
cd "${HOME:?}"
|
cd "${HOME:?}"
|
||||||
done
|
done
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
|
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
|
fi
|
||||||
|
}
|
||||||
|
PROMPT_COMMAND=__mire_prompt_ready
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
--ro-bind / / \
|
--ro-bind / / \
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
|
|||||||
`set -- "$@" --ro-bind "$host_path" "$visible_path"`,
|
`set -- "$@" --ro-bind "$host_path" "$visible_path"`,
|
||||||
"${MIRE_SETUP_SCRIPTS-}",
|
"${MIRE_SETUP_SCRIPTS-}",
|
||||||
`if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then`,
|
`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_home\" \"$visible_home\"",
|
||||||
"--bind \"$host_tmp\" '/tmp'",
|
"--bind \"$host_tmp\" '/tmp'",
|
||||||
"--setenv HOME \"$visible_home\"",
|
"--setenv HOME \"$visible_home\"",
|
||||||
@@ -147,6 +148,9 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
|
|||||||
t.Fatalf("wrapper = %q, want substring %q", body, want)
|
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) {
|
func TestRecordSessionEnvIncludesConfiguredSandboxEnv(t *testing.T) {
|
||||||
|
|||||||
+8
-30
@@ -47,8 +47,8 @@ type testMismatchError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
replayPromptReadyMarker = "\x1b[?2004h$ "
|
replayPromptReadyMarker = compareOutputMarker
|
||||||
replayPromptReadyTimeout = 30 * time.Second
|
replayPromptReadyTimeout = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *testMismatchError) Error() string {
|
func (e *testMismatchError) Error() string {
|
||||||
@@ -260,24 +260,16 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
|
|||||||
compareMarkerEnvName: compareMarkerEnabledValue,
|
compareMarkerEnvName: compareMarkerEnabledValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
outputLog := io.Writer(rawOutFile)
|
|
||||||
var promptWriter *replayPromptWriter
|
|
||||||
var promptReady <-chan struct{}
|
|
||||||
if replayNeedsInteractivePrompt(input) {
|
|
||||||
ready := make(chan struct{})
|
ready := make(chan struct{})
|
||||||
promptWriter = newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
promptWriter := newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
|
||||||
outputLog = promptWriter
|
|
||||||
promptReady = ready
|
|
||||||
|
|
||||||
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
|
||||||
defer promptTimeout.Stop()
|
defer promptTimeout.Stop()
|
||||||
}
|
|
||||||
|
|
||||||
if err := screen.Replay(screen.ReplayRequest{
|
if err := screen.Replay(screen.ReplayRequest{
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Input: input,
|
Input: input,
|
||||||
InputReady: promptReady,
|
InputReady: ready,
|
||||||
OutputLog: outputLog,
|
OutputLog: promptWriter,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
rawOutFile.Close()
|
rawOutFile.Close()
|
||||||
return fmt.Errorf("replay failed: %v", err)
|
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 {
|
if err := rawOutFile.Close(); err != nil {
|
||||||
return fmt.Errorf("failed to close replay output: %v", err)
|
return fmt.Errorf("failed to close replay output: %v", err)
|
||||||
}
|
}
|
||||||
if promptWriter != nil && !promptWriter.seen {
|
if !promptWriter.seen {
|
||||||
return fmt.Errorf("replay shell never reached the initial interactive prompt; inspect %q or rerun `mire init`", shellPath)
|
return fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := loadRecordedOutput(rawOut)
|
got, err := loadRecordedOutput(rawOut)
|
||||||
@@ -328,7 +320,7 @@ func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
|||||||
idx := bytes.Index(data, []byte(compareOutputMarker))
|
idx := bytes.Index(data, []byte(compareOutputMarker))
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return nil, fmt.Errorf(
|
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,
|
compareOutputMarker,
|
||||||
shellPath,
|
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 {
|
type replayPromptWriter struct {
|
||||||
dst io.Writer
|
dst io.Writer
|
||||||
marker []byte
|
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) {
|
func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
|
||||||
testDir := filepath.Join(t.TempDir(), "e2e")
|
testDir := filepath.Join(t.TempDir(), "e2e")
|
||||||
shellPath := filepath.Join(testDir, "shell.sh")
|
shellPath := filepath.Join(testDir, "shell.sh")
|
||||||
@@ -146,29 +175,8 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("replayScenario() error = nil, want error")
|
t.Fatal("replayScenario() error = nil, want error")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "missing compare marker") || !strings.Contains(err.Error(), "rerun `mire init`") {
|
if !strings.Contains(err.Error(), "__MIRE_PROMPT_READY__") || !strings.Contains(err.Error(), "rerun `mire init`") {
|
||||||
t.Fatalf("replayScenario() error = %q, want compare marker refresh hint", err.Error())
|
t.Fatalf("replayScenario() error = %q, want prompt-ready 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -18,6 +19,7 @@ const (
|
|||||||
defaultRows = 24
|
defaultRows = 24
|
||||||
defaultCols = 80
|
defaultCols = 80
|
||||||
terminalEOF = byte(0x04)
|
terminalEOF = byte(0x04)
|
||||||
|
replayReadySettleDelay = 10 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordRequest struct {
|
type RecordRequest struct {
|
||||||
@@ -91,9 +93,11 @@ func Replay(req ReplayRequest) error {
|
|||||||
defer ptmx.Close()
|
defer ptmx.Close()
|
||||||
|
|
||||||
outputDone := copyAsync(combineWriters(req.OutputLog), ptmx)
|
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()
|
waitErr := req.Cmd.Wait()
|
||||||
|
close(processDone)
|
||||||
ptmx.Close()
|
ptmx.Close()
|
||||||
|
|
||||||
outputErr := <-outputDone
|
outputErr := <-outputDone
|
||||||
@@ -129,11 +133,17 @@ func copyAsync(dst io.Writer, src io.Reader) <-chan error {
|
|||||||
return done
|
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)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
if ready != nil {
|
if ready != nil {
|
||||||
<-ready
|
select {
|
||||||
|
case <-ready:
|
||||||
|
time.Sleep(replayReadySettleDelay)
|
||||||
|
case <-stop:
|
||||||
|
done <- nil
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err := io.Copy(dst, src)
|
_, err := io.Copy(dst, src)
|
||||||
done <- normalizeCopyError(err)
|
done <- normalizeCopyError(err)
|
||||||
|
|||||||
@@ -388,8 +388,12 @@ while [ "$#" -gt 0 ]; do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
|
||||||
/bin/stty -echo 2>/dev/null || true
|
/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
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
printf '%s\n' "$line"
|
printf '%s\n' "$line"
|
||||||
if [ "$line" = "exit" ]; then
|
if [ "$line" = "exit" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user