From 5c3f42aa4e086fbdb5976ece09adc0a1e9e6ba79 Mon Sep 17 00:00:00 2001 From: Vitaly Potyarkin Date: Thu, 27 Apr 2023 14:00:42 +0000 Subject: [PATCH] Add tests to detect forever hanging Read() --- io_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 io_test.go diff --git a/io_test.go b/io_test.go new file mode 100644 index 0000000..f68956c --- /dev/null +++ b/io_test.go @@ -0,0 +1,102 @@ +package pty + +import ( + "testing" + + "context" + "os" + "sync" + "time" +) + +const ( + errMarker byte = 0xEE + timeout = time.Second +) + +var mu sync.Mutex + +// Check that SetDeadline() works for ptmx. +// Outstanding Read() calls must be interrupted by deadline. +// +// https://github.com/creack/pty/issues/162 +func TestReadDeadline(t *testing.T) { + ptmx, success := prepare(t) + + err := ptmx.SetDeadline(time.Now().Add(timeout / 10)) + if err != nil { + t.Fatalf("error: set deadline: %v\n", err) + } + + var buf = make([]byte, 1) + n, err := ptmx.Read(buf) + success() + + if n != 0 && buf[0] != errMarker { + t.Errorf("received unexpected data from pmtx (%d bytes): 0x%X; err=%v", n, buf, err) + } +} + +// Check that ptmx.Close() interrupts outstanding ptmx.Read() calls +// +// https://github.com/creack/pty/issues/114 +// https://github.com/creack/pty/issues/88 +func TestReadClose(t *testing.T) { + ptmx, success := prepare(t) + + go func() { + time.Sleep(timeout / 10) + err := ptmx.Close() + if err != nil { + t.Errorf("failed to close ptmx: %v", err) + } + }() + + var buf = make([]byte, 1) + n, err := ptmx.Read(buf) + success() + + if n != 0 && buf[0] != errMarker { + t.Errorf("received unexpected data from pmtx (%d bytes): 0x%X; err=%v", n, buf, err) + } +} + +// Open pty and setup watchdogs for graceful and not so graceful failure modes +func prepare(t *testing.T) (ptmx *os.File, done func()) { + + // Due to data race potential in (*os.File).Fd() + // we should never run these two tests in parallel + mu.Lock() + t.Cleanup(mu.Unlock) + + ptmx, pts, err := Open() + if err != nil { + t.Fatalf("error: open: %v\n", err) + } + t.Cleanup(func() { _ = ptmx.Close() }) + t.Cleanup(func() { _ = pts.Close() }) + + ctx, done := context.WithCancel(context.Background()) + go func() { + select { + case <-ctx.Done(): + // ptmx.Read() did not block forever, yay! + case <-time.After(timeout): + _, err := pts.Write([]byte{errMarker}) // unblock ptmx.Read() + if err != nil { + t.Errorf("failed to write to pts: %v", err) + } + t.Error("ptmx.Read() was not unblocked") + done() // cancel panic() + } + }() + go func() { + select { + case <-ctx.Done(): + // Test has either failed or succeeded; it definitely did not hang + case <-time.After(timeout * 10 / 9): // timeout +11% + panic("ptmx.Read() was not unblocked; avoid hanging forever") // just in case + } + }() + return ptmx, done +}