fix: handle ctrl+c based interrupts while in prompt
This commit is contained in:
@@ -9,14 +9,18 @@ import (
|
||||
var (
|
||||
scriptStartPrefix = []byte("Script started on ")
|
||||
scriptDonePrefix = []byte("Script done on ")
|
||||
shellPromptPrefix = []byte("\x1b[?2004h$ ")
|
||||
)
|
||||
|
||||
const (
|
||||
interruptByte = byte(0x03)
|
||||
eofByte = byte(0x04)
|
||||
escByte = byte(0x1b)
|
||||
bellByte = byte(0x07)
|
||||
)
|
||||
|
||||
var interruptSuffix = []byte("^C\x1b[?2004l\r\x1b[?2004h\x1b[?2004l\r\r\n")
|
||||
|
||||
func loadRecordedInput(path string) ([]byte, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -51,6 +55,62 @@ func loadRecordedOutput(path string) ([]byte, error) {
|
||||
return stripTerminalTitlePrefixes(data), nil
|
||||
}
|
||||
|
||||
func sanitizeInterrupts(input, output []byte) ([]byte, []byte) {
|
||||
return sanitizeInterruptInput(input), sanitizeInterruptOutput(output)
|
||||
}
|
||||
|
||||
func sanitizeInterruptInput(data []byte) []byte {
|
||||
cleaned := make([]byte, 0, len(data))
|
||||
lineStart := 0
|
||||
|
||||
for _, b := range data {
|
||||
if b == interruptByte {
|
||||
cleaned = cleaned[:lineStart]
|
||||
continue
|
||||
}
|
||||
|
||||
cleaned = append(cleaned, b)
|
||||
if b == '\n' {
|
||||
lineStart = len(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func sanitizeInterruptOutput(data []byte) []byte {
|
||||
cleaned := make([]byte, 0, len(data))
|
||||
cursor := 0
|
||||
|
||||
for {
|
||||
suffixStart := bytes.Index(data[cursor:], interruptSuffix)
|
||||
if suffixStart == -1 {
|
||||
cleaned = append(cleaned, data[cursor:]...)
|
||||
return cleaned
|
||||
}
|
||||
suffixStart += cursor
|
||||
|
||||
promptStart := bytes.LastIndex(data[cursor:suffixStart], shellPromptPrefix)
|
||||
if promptStart == -1 {
|
||||
end := suffixStart + len(interruptSuffix)
|
||||
cleaned = append(cleaned, data[cursor:end]...)
|
||||
cursor = end
|
||||
continue
|
||||
}
|
||||
promptStart += cursor
|
||||
|
||||
if bytes.IndexByte(data[promptStart+len(shellPromptPrefix):suffixStart], '\n') != -1 {
|
||||
end := suffixStart + len(interruptSuffix)
|
||||
cleaned = append(cleaned, data[cursor:end]...)
|
||||
cursor = end
|
||||
continue
|
||||
}
|
||||
|
||||
cleaned = append(cleaned, data[cursor:promptStart]...)
|
||||
cursor = suffixStart + len(interruptSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
func hasScriptWrapper(data []byte) bool {
|
||||
if bytes.HasPrefix(data, scriptStartPrefix) {
|
||||
return true
|
||||
|
||||
@@ -57,6 +57,7 @@ func recordScenario(target, shellPath string, rio recordIO, sandboxConfig map[st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordedIn, recordedOut = sanitizeInterrupts(recordedIn, recordedOut)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(target, "in"), recordedIn, 0o644); err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mire/internal/testutil"
|
||||
)
|
||||
@@ -315,6 +316,94 @@ func TestRecordScenarioUsesDeterministicSandbox(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordScenarioStripsInterruptsFromSavedFixtures(t *testing.T) {
|
||||
testutil.RequireCommands(t, "bwrap", "bash")
|
||||
|
||||
root := t.TempDir()
|
||||
testDir := filepath.Join(root, "e2e")
|
||||
target := filepath.Join(testDir, "suite", "spec")
|
||||
testutil.MustMkdirAll(t, target)
|
||||
mustWriteRecordShell(t, testDir)
|
||||
|
||||
err := testutil.WithWorkingDir(t, root, func() error {
|
||||
reader, writer, pipeErr := os.Pipe()
|
||||
if pipeErr != nil {
|
||||
t.Fatalf("os.Pipe() error = %v", pipeErr)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = reader.Close()
|
||||
_ = writer.Close()
|
||||
})
|
||||
|
||||
errWriter := &promptSignalBuffer{
|
||||
marker: "Save recording?",
|
||||
ready: make(chan struct{}),
|
||||
}
|
||||
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(writeDone)
|
||||
defer writer.Close()
|
||||
|
||||
for _, chunk := range []struct {
|
||||
data string
|
||||
delay time.Duration
|
||||
}{
|
||||
{data: "echo a\n", delay: 100 * time.Millisecond},
|
||||
{data: "echo ", delay: 100 * time.Millisecond},
|
||||
{data: "\x03", delay: 100 * time.Millisecond},
|
||||
{data: "exit\n", delay: 0},
|
||||
} {
|
||||
if _, err := writer.Write([]byte(chunk.data)); err != nil {
|
||||
writeDone <- err
|
||||
return
|
||||
}
|
||||
if chunk.delay > 0 {
|
||||
time.Sleep(chunk.delay)
|
||||
}
|
||||
}
|
||||
|
||||
<-errWriter.ready
|
||||
|
||||
if _, err := writer.Write([]byte("y\n")); err != nil {
|
||||
writeDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
writeDone <- nil
|
||||
}()
|
||||
|
||||
err := recordScenario(target, recordShellPath(testDir), recordIO{
|
||||
in: reader,
|
||||
out: ioDiscard{},
|
||||
err: errWriter,
|
||||
}, defaultSandboxConfig(), nil, nil, nil)
|
||||
if writeErr := <-writeDone; writeErr != nil {
|
||||
t.Fatalf("write record input: %v", writeErr)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("recordScenario() error = %v", err)
|
||||
}
|
||||
|
||||
recordedIn := testutil.ReadFile(t, filepath.Join(target, "in"))
|
||||
if recordedIn != "echo a\nexit\n" {
|
||||
t.Fatalf("saved in = %q, want %q", recordedIn, "echo a\nexit\n")
|
||||
}
|
||||
|
||||
recordedOut := testutil.ReadFile(t, filepath.Join(target, "out"))
|
||||
if strings.Contains(recordedOut, "^C") {
|
||||
t.Fatalf("saved out = %q, want interrupt output removed", recordedOut)
|
||||
}
|
||||
if strings.Contains(recordedOut, "echo exit\r\n") {
|
||||
t.Fatalf("saved out = %q, want interrupted line removed before exit", recordedOut)
|
||||
}
|
||||
if !strings.Contains(recordedOut, "exit\r\n") {
|
||||
t.Fatalf("saved out = %q, want exit preserved", recordedOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordFailsWhenRecorderShellMissing(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
testDir := filepath.Join(root, "e2e")
|
||||
|
||||
Reference in New Issue
Block a user