fix: handle ctrl+c based interrupts while in prompt
This commit is contained in:
@@ -9,14 +9,18 @@ import (
|
|||||||
var (
|
var (
|
||||||
scriptStartPrefix = []byte("Script started on ")
|
scriptStartPrefix = []byte("Script started on ")
|
||||||
scriptDonePrefix = []byte("Script done on ")
|
scriptDonePrefix = []byte("Script done on ")
|
||||||
|
shellPromptPrefix = []byte("\x1b[?2004h$ ")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
eofByte = byte(0x04)
|
interruptByte = byte(0x03)
|
||||||
escByte = byte(0x1b)
|
eofByte = byte(0x04)
|
||||||
bellByte = byte(0x07)
|
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) {
|
func loadRecordedInput(path string) ([]byte, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +55,62 @@ func loadRecordedOutput(path string) ([]byte, error) {
|
|||||||
return stripTerminalTitlePrefixes(data), nil
|
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 {
|
func hasScriptWrapper(data []byte) bool {
|
||||||
if bytes.HasPrefix(data, scriptStartPrefix) {
|
if bytes.HasPrefix(data, scriptStartPrefix) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func recordScenario(target, shellPath string, rio recordIO, sandboxConfig map[st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
recordedIn, recordedOut = sanitizeInterrupts(recordedIn, recordedOut)
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(target, "in"), recordedIn, 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(target, "in"), recordedIn, 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"mire/internal/testutil"
|
"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) {
|
func TestRecordFailsWhenRecorderShellMissing(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
testDir := filepath.Join(root, "e2e")
|
testDir := filepath.Join(root, "e2e")
|
||||||
|
|||||||
Reference in New Issue
Block a user