Skip to content

Commit 7bb0694

Browse files
committed
Add configurable timeout for handler response to each test case
Add --handler-timeout flag to runner binary allowing users to configure max response wait time per test case (default: 10s). Add TestHandler_Unresponsive to verify timeout behavior when handler becomes unresponsive.
1 parent 7d8a41a commit 7bb0694

File tree

4 files changed

+100
-27
lines changed

4 files changed

+100
-27
lines changed

cmd/runner/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import (
66
"io/fs"
77
"os"
88
"strings"
9+
"time"
910

1011
"github.com/stringintech/kernel-bindings-tests/runner"
1112
"github.com/stringintech/kernel-bindings-tests/testdata"
1213
)
1314

1415
func main() {
1516
handlerPath := flag.String("handler", "", "Path to handler binary")
17+
handlerTimeout := flag.Duration("handler-timeout", 10*time.Second, "Max time to wait for handler to respond to each test case (e.g., 10s, 500ms)")
1618
flag.Parse()
1719

1820
if *handlerPath == "" {
@@ -34,7 +36,7 @@ func main() {
3436
}
3537

3638
// Create test runner
37-
testRunner, err := runner.NewTestRunner(*handlerPath)
39+
testRunner, err := runner.NewTestRunner(*handlerPath, *handlerTimeout)
3840
if err != nil {
3941
fmt.Fprintf(os.Stderr, "Error creating test runner: %v\n", err)
4042
os.Exit(1)

runner/handler.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,23 @@ type HandlerConfig struct {
2323
Path string
2424
Args []string
2525
Env []string
26+
// Timeout specifies the maximum duration to wait when reading from the handler's
27+
// stdout. If zero, defaults to 10 seconds. The handler is killed if it fails to
28+
// write output within this timeout.
29+
Timeout time.Duration
2630
}
2731

2832
// Handler manages a conformance handler process communicating via stdin/stdout
2933
type Handler struct {
30-
cmd *exec.Cmd
31-
stdin io.WriteCloser
32-
stdout *bufio.Scanner
33-
stderr io.ReadCloser
34+
cmd *exec.Cmd
35+
stdin io.WriteCloser
36+
stdout *bufio.Scanner
37+
stderr io.ReadCloser
38+
timeout time.Duration
3439
}
3540

3641
// NewHandler spawns a new handler process with the given configuration
37-
func NewHandler(cfg HandlerConfig) (*Handler, error) {
42+
func NewHandler(cfg *HandlerConfig) (*Handler, error) {
3843
cmd := exec.Command(cfg.Path, cfg.Args...)
3944
if cfg.Env != nil {
4045
cmd.Env = append(cmd.Environ(), cfg.Env...)
@@ -60,11 +65,17 @@ func NewHandler(cfg HandlerConfig) (*Handler, error) {
6065
return nil, fmt.Errorf("failed to start handler: %w", err)
6166
}
6267

68+
timeout := cfg.Timeout
69+
if timeout == 0 {
70+
timeout = 10 * time.Second
71+
}
72+
6373
return &Handler{
64-
cmd: cmd,
65-
stdin: stdin,
66-
stdout: bufio.NewScanner(stdout),
67-
stderr: stderr,
74+
cmd: cmd,
75+
stdin: stdin,
76+
stdout: bufio.NewScanner(stdout),
77+
stderr: stderr,
78+
timeout: timeout,
6879
}, nil
6980
}
7081

@@ -74,7 +85,7 @@ func (h *Handler) SendLine(line []byte) error {
7485
return err
7586
}
7687

77-
// ReadLine reads a line from the handler's stdout with a 10-second timeout
88+
// ReadLine reads a line from the handler's stdout with a configurable timeout
7889
func (h *Handler) ReadLine() ([]byte, error) {
7990
// Use a timeout for Scan() in case the handler hangs
8091
scanDone := make(chan bool, 1)
@@ -93,7 +104,7 @@ func (h *Handler) ReadLine() ([]byte, error) {
93104
}
94105
// EOF - handler closed stdout prematurely, fall through to kill and capture stderr
95106
baseErr = ErrHandlerClosed
96-
case <-time.After(10 * time.Second):
107+
case <-time.After(h.timeout):
97108
// Timeout - handler didn't respond, fall through to kill and capture stderr
98109
baseErr = ErrHandlerTimeout
99110
}

runner/handler_test.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package runner
22

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"os"
78
"testing"
9+
"time"
810
)
911

1012
const (
@@ -14,12 +16,14 @@ const (
1416
// envTestHelperName specifies which helper function to execute in subprocess mode.
1517
envTestHelperName = "TEST_HELPER_NAME"
1618

17-
helperNameNormal = "normal"
19+
helperNameNormal = "normal"
20+
helperNameUnresponsive = "unresponsive"
1821
)
1922

2023
// testHelpers maps helper names to functions that simulate different handler behaviors.
2124
var testHelpers = map[string]func(){
22-
helperNameNormal: helperNormal,
25+
helperNameNormal: helperNormal,
26+
helperNameUnresponsive: helperUnresponsive,
2327
}
2428

2529
// TestMain allows the test binary to serve two purposes:
@@ -46,7 +50,7 @@ func TestMain(m *testing.M) {
4650

4751
// TestHandler_NormalOperation tests that a well-behaved handler works correctly
4852
func TestHandler_NormalOperation(t *testing.T) {
49-
h, err := newHandlerForTest(t, helperNameNormal)
53+
h, err := newHandlerForTest(t, helperNameNormal, 0)
5054
if err != nil {
5155
t.Fatalf("Failed to create handler: %v", err)
5256
}
@@ -86,13 +90,60 @@ func helperNormal() {
8690
}
8791
}
8892

93+
// TestHandler_Unresponsive tests that the runner correctly handles an unresponsive handler
94+
func TestHandler_Unresponsive(t *testing.T) {
95+
h, err := newHandlerForTest(t, helperNameUnresponsive, 100*time.Millisecond)
96+
if err != nil {
97+
t.Fatalf("Failed to create handler: %v", err)
98+
}
99+
defer h.Close()
100+
101+
// Send a request to the handler
102+
request := `{"id":1,"method":"test"}`
103+
if err := h.SendLine([]byte(request)); err != nil {
104+
t.Fatalf("Failed to send request: %v", err)
105+
}
106+
107+
// Try to read the response - should Timeout
108+
start := time.Now()
109+
_, err = h.ReadLine()
110+
elapsed := time.Since(start)
111+
112+
if err == nil {
113+
t.Fatal("Expected error from unresponsive handler, got nil")
114+
}
115+
116+
// Verify it's the Timeout error we expect
117+
if !errors.Is(err, ErrHandlerTimeout) {
118+
t.Errorf("Expected ErrHandlerTimeout, got: %v", err)
119+
}
120+
121+
// Verify Timeout happened quickly (within reasonable margin)
122+
if elapsed > 200*time.Millisecond {
123+
t.Errorf("Timeout took too long: %v (expected ~100ms)", elapsed)
124+
}
125+
}
126+
127+
// helperUnresponsive simulates a handler that receives requests but never responds,
128+
// triggering the Timeout mechanism in the runner.
129+
func helperUnresponsive() {
130+
// Read from stdin to prevent broken pipe, but never write responses
131+
scanner := bufio.NewScanner(os.Stdin)
132+
for scanner.Scan() {
133+
// Sleep indefinitely to simulate unresponsiveness
134+
time.Sleep(1 * time.Hour)
135+
}
136+
}
137+
89138
// newHandlerForTest creates a Handler that runs a test helper as a subprocess.
90139
// The helperName identifies which helper to run (e.g., "normal", "crash", "hang").
91-
func newHandlerForTest(t *testing.T, helperName string) (*Handler, error) {
140+
// The timeout parameter sets the per-request timeout (0 uses default).
141+
func newHandlerForTest(t *testing.T, helperName string, timeout time.Duration) (*Handler, error) {
92142
t.Helper()
93143

94-
return NewHandler(HandlerConfig{
95-
Path: os.Args[0],
96-
Env: []string{"TEST_AS_SUBPROCESS=1", "TEST_HELPER_NAME=" + helperName},
144+
return NewHandler(&HandlerConfig{
145+
Path: os.Args[0],
146+
Env: []string{"TEST_AS_SUBPROCESS=1", "TEST_HELPER_NAME=" + helperName},
147+
Timeout: timeout,
97148
})
98149
}

runner/runner.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,44 @@ import (
88
"log/slog"
99
"os"
1010
"path/filepath"
11+
"time"
1112
)
1213

1314
// TestRunner executes test suites against a handler binary
1415
type TestRunner struct {
15-
handlerPath string
16-
handler *Handler
16+
handler *Handler
17+
handlerConfig *HandlerConfig
1718
}
1819

19-
// NewTestRunner creates a new test runner
20-
func NewTestRunner(handlerPath string) (*TestRunner, error) {
20+
// NewTestRunner creates a new test runner for executing test suites against a handler binary.
21+
// The handlerTimeout parameter specifies the maximum duration to wait for the handler to
22+
// respond to each test case. If zero, defaults to 10 seconds.
23+
func NewTestRunner(handlerPath string, handlerTimeout time.Duration) (*TestRunner, error) {
2124
if _, err := os.Stat(handlerPath); os.IsNotExist(err) {
2225
return nil, fmt.Errorf("handler binary not found: %s", handlerPath)
2326
}
2427

25-
handler, err := NewHandler(HandlerConfig{Path: handlerPath})
28+
handler, err := NewHandler(&HandlerConfig{
29+
Path: handlerPath,
30+
Timeout: handlerTimeout,
31+
})
2632
if err != nil {
2733
return nil, err
2834
}
2935

3036
return &TestRunner{
31-
handlerPath: handlerPath,
32-
handler: handler,
37+
handler: handler,
38+
handlerConfig: &HandlerConfig{
39+
Path: handlerPath,
40+
Timeout: handlerTimeout,
41+
},
3342
}, nil
3443
}
3544

3645
// SendRequest sends a request to the handler, spawning a new handler if needed
3746
func (tr *TestRunner) SendRequest(req Request) error {
3847
if tr.handler == nil {
39-
handler, err := NewHandler(HandlerConfig{Path: tr.handlerPath})
48+
handler, err := NewHandler(tr.handlerConfig)
4049
if err != nil {
4150
return fmt.Errorf("failed to spawn new handler: %w", err)
4251
}

0 commit comments

Comments
 (0)