Skip to content

Commit b1dfae8

Browse files
committed
Add mock handler
Implement a mock handler that validates the test framework itself. The handler embeds test cases, reads requests from stdin, and returns expected responses from the JSON test files.
1 parent 62b1729 commit b1dfae8

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Go
16+
uses: actions/setup-go@v5
17+
with:
18+
go-version: '1.23'
19+
20+
- name: Build
21+
run: make build
22+
23+
- name: Run tests against mock handler
24+
run: make test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build/

Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.PHONY: all build test clean runner mock-handler deps lint
2+
3+
BUILD_DIR := build
4+
RUNNER_BIN := $(BUILD_DIR)/runner
5+
MOCK_HANDLER_BIN := $(BUILD_DIR)/mock-handler
6+
7+
all: build test
8+
9+
build: runner mock-handler
10+
11+
runner:
12+
@echo "Building test runner..."
13+
@mkdir -p $(BUILD_DIR)
14+
go build -o $(RUNNER_BIN) ./cmd/runner
15+
16+
mock-handler:
17+
@echo "Building mock handler..."
18+
@mkdir -p $(BUILD_DIR)
19+
go build -o $(MOCK_HANDLER_BIN) ./cmd/mock-handler
20+
21+
test:
22+
@echo "Running conformance tests with mock handler..."
23+
$(RUNNER_BIN) -handler $(MOCK_HANDLER_BIN)
24+
25+
clean:
26+
@echo "Cleaning build artifacts..."
27+
rm -rf $(BUILD_DIR)

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,21 @@ The framework ensures that all language bindings (Go, Python, Rust, etc.) behave
3232
**This repository contains:**
3333
1. [**Test Runner**](./cmd/runner/main.go): Spawns handler binary, sends test requests via stdin, validates responses from stdout
3434
2. [**Test Cases**](./testdata): JSON files defining requests and expected responses
35+
3. [**Mock Handler**](./cmd/mock-handler/main.go): Validates the runner by echoing expected responses from test cases
3536

3637
**Handler binaries** are not hosted in this repository. They must be implemented separately and should:
3738
- Implement the JSON protocol for communication with the test runner
3839
- Call the binding API to execute operations
39-
- Pin to a specific version/tag of this test repository
40+
- Pin to a specific version/tag of this test repository
41+
42+
## Getting Started
43+
44+
Build and test against the mock handler:
45+
46+
```bash
47+
# Build both runner and mock handler
48+
make build
49+
50+
# Run tests against the mock handler
51+
make test
52+
```

cmd/mock-handler/main.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
10+
"github.com/stringintech/kernel-bindings-tests/runner"
11+
"github.com/stringintech/kernel-bindings-tests/testdata"
12+
)
13+
14+
func main() {
15+
// Build a map of test ID -> filename
16+
testIndex, err := buildTestIndex()
17+
if err != nil {
18+
fmt.Fprintf(os.Stderr, "Failed to build test index: %v\n", err)
19+
os.Exit(1)
20+
}
21+
22+
// Read requests from stdin and respond with expected results
23+
scanner := bufio.NewScanner(os.Stdin)
24+
for scanner.Scan() {
25+
line := scanner.Text()
26+
if err := handleRequest(line, testIndex); err != nil {
27+
fmt.Fprintf(os.Stderr, "Error handling request: %v\n", err)
28+
continue
29+
}
30+
}
31+
32+
if err := scanner.Err(); err != nil {
33+
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
34+
os.Exit(1)
35+
}
36+
}
37+
38+
// buildTestIndex creates a map of test ID -> filename without loading full test data
39+
func buildTestIndex() (map[string]string, error) {
40+
testFiles, err := fs.Glob(testdata.FS, "*.json")
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to find test files: %w", err)
43+
}
44+
45+
index := make(map[string]string)
46+
for _, testFile := range testFiles {
47+
// Read file to extract test IDs only
48+
data, err := fs.ReadFile(testdata.FS, testFile)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to read test file %s: %w", testFile, err)
51+
}
52+
53+
// Parse just enough to get test IDs
54+
var suite struct {
55+
Tests []struct {
56+
ID string `json:"id"`
57+
} `json:"tests"`
58+
}
59+
if err := json.Unmarshal(data, &suite); err != nil {
60+
return nil, fmt.Errorf("failed to parse test file %s: %w", testFile, err)
61+
}
62+
63+
for _, test := range suite.Tests {
64+
index[test.ID] = testFile
65+
}
66+
}
67+
68+
return index, nil
69+
}
70+
71+
// handleRequest processes a single request and outputs the expected response
72+
func handleRequest(line string, testIndex map[string]string) error {
73+
// Parse request
74+
var req runner.Request
75+
if err := json.Unmarshal([]byte(line), &req); err != nil {
76+
return fmt.Errorf("failed to parse request: %w", err)
77+
}
78+
79+
filename, ok := testIndex[req.ID]
80+
if !ok {
81+
resp := runner.Response{
82+
ID: req.ID,
83+
Error: &runner.ErrorObj{
84+
Code: "UNKNOWN_TEST",
85+
Message: fmt.Sprintf("No test case found with ID: %s", req.ID),
86+
},
87+
}
88+
return writeResponse(resp)
89+
}
90+
91+
// Load the test suite containing this test case
92+
suite, err := runner.LoadTestSuiteFromFS(testdata.FS, filename)
93+
if err != nil {
94+
resp := runner.Response{
95+
ID: req.ID,
96+
Error: &runner.ErrorObj{
97+
Code: "LOAD_ERROR",
98+
Message: fmt.Sprintf("Failed to load test suite: %v", err),
99+
},
100+
}
101+
return writeResponse(resp)
102+
}
103+
104+
// Find the specific test case
105+
var testCase *runner.TestCase
106+
for _, test := range suite.Tests {
107+
if test.ID == req.ID {
108+
testCase = &test
109+
break
110+
}
111+
}
112+
if testCase == nil {
113+
resp := runner.Response{
114+
ID: req.ID,
115+
Error: &runner.ErrorObj{
116+
Code: "TEST_NOT_FOUND",
117+
Message: fmt.Sprintf("Test case %s not found in file %s", req.ID, filename),
118+
},
119+
}
120+
return writeResponse(resp)
121+
}
122+
123+
// Verify method matches
124+
if req.Method != testCase.Method {
125+
resp := runner.Response{
126+
ID: req.ID,
127+
Error: &runner.ErrorObj{
128+
Code: "METHOD_MISMATCH",
129+
Message: fmt.Sprintf("Expected method %s, got %s", testCase.Method, req.Method),
130+
},
131+
}
132+
return writeResponse(resp)
133+
}
134+
135+
// Build response based on expected result
136+
var resp runner.Response
137+
resp.ID = req.ID
138+
if testCase.Expected.Error != nil {
139+
resp.Error = &runner.ErrorObj{
140+
Code: testCase.Expected.Error.Code,
141+
Message: testCase.Expected.Error.Message,
142+
}
143+
}
144+
if testCase.Expected.Success != nil {
145+
resp.Result = *testCase.Expected.Success
146+
}
147+
return writeResponse(resp)
148+
}
149+
150+
// writeResponse writes a response to stdout as JSON
151+
func writeResponse(resp runner.Response) error {
152+
data, err := json.Marshal(resp)
153+
if err != nil {
154+
return fmt.Errorf("failed to marshal response: %w", err)
155+
}
156+
fmt.Println(string(data))
157+
return nil
158+
}

0 commit comments

Comments
 (0)