Skip to content

Commit 7d8a41a

Browse files
committed
Add test re-exec pattern for simulating handler behaviors
Introduce HandlerConfig struct and refactor NewHandler to support custom args/env. Add TestMain-based re-exec pattern allowing the test binary to serve as both test runner and mock handler subprocess. This enables unit testing of different handler behaviors (normal, crash, hang, unresponsive) without external fixtures, verifying the runner correctly handles each scenario.
1 parent b944097 commit 7d8a41a

File tree

4 files changed

+114
-6
lines changed

4 files changed

+114
-6
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ mock-handler:
2020

2121
test:
2222
@echo "Running runner unit tests..."
23-
go test ./runner/...
23+
go test -v ./runner/...
2424
@echo "Running conformance tests with mock handler..."
2525
$(RUNNER_BIN) -handler $(MOCK_HANDLER_BIN)
2626

runner/handler.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ var (
1818
ErrHandlerClosed = errors.New("handler closed unexpectedly")
1919
)
2020

21+
// HandlerConfig configures a handler process
22+
type HandlerConfig struct {
23+
Path string
24+
Args []string
25+
Env []string
26+
}
27+
2128
// Handler manages a conformance handler process communicating via stdin/stdout
2229
type Handler struct {
2330
cmd *exec.Cmd
@@ -26,9 +33,12 @@ type Handler struct {
2633
stderr io.ReadCloser
2734
}
2835

29-
// NewHandler spawns a new handler process at the given path
30-
func NewHandler(path string) (*Handler, error) {
31-
cmd := exec.Command(path)
36+
// NewHandler spawns a new handler process with the given configuration
37+
func NewHandler(cfg HandlerConfig) (*Handler, error) {
38+
cmd := exec.Command(cfg.Path, cfg.Args...)
39+
if cfg.Env != nil {
40+
cmd.Env = append(cmd.Environ(), cfg.Env...)
41+
}
3242

3343
stdin, err := cmd.StdinPipe()
3444
if err != nil {

runner/handler_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package runner
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"testing"
8+
)
9+
10+
const (
11+
// envTestAsSubprocess signals the binary to run as a subprocess helper.
12+
envTestAsSubprocess = "TEST_AS_SUBPROCESS"
13+
14+
// envTestHelperName specifies which helper function to execute in subprocess mode.
15+
envTestHelperName = "TEST_HELPER_NAME"
16+
17+
helperNameNormal = "normal"
18+
)
19+
20+
// testHelpers maps helper names to functions that simulate different handler behaviors.
21+
var testHelpers = map[string]func(){
22+
helperNameNormal: helperNormal,
23+
}
24+
25+
// TestMain allows the test binary to serve two purposes:
26+
// 1. Normal mode: runs tests when TEST_AS_SUBPROCESS != "1"
27+
// 2. Subprocess mode: executes a helper function when TEST_AS_SUBPROCESS == "1"
28+
//
29+
// This enables tests to spawn the binary itself as a mock handler subprocess,
30+
// avoiding the need for separate test fixture binaries.
31+
func TestMain(m *testing.M) {
32+
if os.Getenv(envTestAsSubprocess) != "1" {
33+
// Run tests normally
34+
os.Exit(m.Run())
35+
}
36+
37+
// Run as subprocess helper based on which helper is requested
38+
helperName := os.Getenv(envTestHelperName)
39+
if helper, ok := testHelpers[helperName]; ok {
40+
helper()
41+
} else {
42+
fmt.Fprintf(os.Stderr, "Unknown test helper: %s\n", helperName)
43+
os.Exit(1)
44+
}
45+
}
46+
47+
// TestHandler_NormalOperation tests that a well-behaved handler works correctly
48+
func TestHandler_NormalOperation(t *testing.T) {
49+
h, err := newHandlerForTest(t, helperNameNormal)
50+
if err != nil {
51+
t.Fatalf("Failed to create handler: %v", err)
52+
}
53+
defer h.Close()
54+
55+
// Send a request to the handler
56+
request := `{"id":1,"method":"test"}`
57+
if err := h.SendLine([]byte(request)); err != nil {
58+
t.Fatalf("Failed to send request: %v", err)
59+
}
60+
61+
// Read the response
62+
line, err := h.ReadLine()
63+
if err != nil {
64+
t.Fatalf("Failed to read line: %v", err)
65+
}
66+
67+
expected := `{"id":1,"result":true}`
68+
if string(line) != expected {
69+
t.Errorf("Expected %q, got %q", expected, string(line))
70+
}
71+
}
72+
73+
// helperNormal simulates a normal well-behaved handler that reads a request,
74+
// validates it, and sends a response.
75+
func helperNormal() {
76+
// Read requests from stdin and respond with expected results
77+
scanner := bufio.NewScanner(os.Stdin)
78+
for scanner.Scan() {
79+
request := scanner.Text()
80+
expected := `{"id":1,"method":"test"}`
81+
if request != expected {
82+
fmt.Fprintf(os.Stderr, "Expected request %q, got %q\n", expected, request)
83+
os.Exit(1)
84+
}
85+
fmt.Println(`{"id":1,"result":true}`)
86+
}
87+
}
88+
89+
// newHandlerForTest creates a Handler that runs a test helper as a subprocess.
90+
// The helperName identifies which helper to run (e.g., "normal", "crash", "hang").
91+
func newHandlerForTest(t *testing.T, helperName string) (*Handler, error) {
92+
t.Helper()
93+
94+
return NewHandler(HandlerConfig{
95+
Path: os.Args[0],
96+
Env: []string{"TEST_AS_SUBPROCESS=1", "TEST_HELPER_NAME=" + helperName},
97+
})
98+
}

runner/runner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func NewTestRunner(handlerPath string) (*TestRunner, error) {
2222
return nil, fmt.Errorf("handler binary not found: %s", handlerPath)
2323
}
2424

25-
handler, err := NewHandler(handlerPath)
25+
handler, err := NewHandler(HandlerConfig{Path: handlerPath})
2626
if err != nil {
2727
return nil, err
2828
}
@@ -36,7 +36,7 @@ func NewTestRunner(handlerPath string) (*TestRunner, error) {
3636
// SendRequest sends a request to the handler, spawning a new handler if needed
3737
func (tr *TestRunner) SendRequest(req Request) error {
3838
if tr.handler == nil {
39-
handler, err := NewHandler(tr.handlerPath)
39+
handler, err := NewHandler(HandlerConfig{Path: tr.handlerPath})
4040
if err != nil {
4141
return fmt.Errorf("failed to spawn new handler: %w", err)
4242
}

0 commit comments

Comments
 (0)