Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions cmd/mock-handler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ func buildTestIndex() (map[string]string, error) {
// Parse just enough to get test IDs
var suite struct {
Tests []struct {
ID string `json:"id"`
Request struct {
ID string `json:"id"`
} `json:"request"`
} `json:"tests"`
}
if err := json.Unmarshal(data, &suite); err != nil {
return nil, fmt.Errorf("failed to parse test file %s: %w", testFile, err)
}

for _, test := range suite.Tests {
index[test.ID] = testFile
index[test.Request.ID] = testFile
}
}

Expand All @@ -79,7 +81,6 @@ func handleRequest(line string, testIndex map[string]string) error {
filename, ok := testIndex[req.ID]
if !ok {
resp := runner.Response{
ID: req.ID,
Error: &runner.Error{
Code: &runner.ErrorCode{
Type: "Handler",
Expand All @@ -94,7 +95,6 @@ func handleRequest(line string, testIndex map[string]string) error {
suite, err := runner.LoadTestSuiteFromFS(testdata.FS, filename)
if err != nil {
resp := runner.Response{
ID: req.ID,
Error: &runner.Error{
Code: &runner.ErrorCode{
Type: "Handler",
Expand All @@ -108,14 +108,13 @@ func handleRequest(line string, testIndex map[string]string) error {
// Find the specific test case
var testCase *runner.TestCase
for _, test := range suite.Tests {
if test.ID == req.ID {
if test.Request.ID == req.ID {
testCase = &test
break
}
}
if testCase == nil {
resp := runner.Response{
ID: req.ID,
Error: &runner.Error{
Code: &runner.ErrorCode{
Type: "Handler",
Expand All @@ -127,9 +126,8 @@ func handleRequest(line string, testIndex map[string]string) error {
}

// Verify method matches
if req.Method != testCase.Method {
if req.Method != testCase.Request.Method {
resp := runner.Response{
ID: req.ID,
Error: &runner.Error{
Code: &runner.ErrorCode{
Type: "Handler",
Expand All @@ -142,9 +140,8 @@ func handleRequest(line string, testIndex map[string]string) error {

// Build response based on expected result
return writeResponse(runner.Response{
ID: req.ID,
Result: testCase.Expected.Result,
Error: testCase.Expected.Error,
Result: testCase.ExpectedResponse.Result,
Error: testCase.ExpectedResponse.Error,
})
}

Expand Down
10 changes: 2 additions & 8 deletions docs/handler-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ Handlers communicate with the test runner via **stdin/stdout**:

```json
{
"id": "unique-request-id",
"result": null,
"error": {
"code": {
Expand All @@ -42,7 +41,6 @@ Handlers communicate with the test runner via **stdin/stdout**:
```

**Fields:**
- `id` (string, required): Must match the request ID
- `result` (any, optional): The return value, or `null` for void/nullptr operations. Must be `null` on error
- `error` (object, optional): Error details. Must be `null` on success. An empty object `{}` is used to indicate an error is raised without further details, it is NOT equivalent to `null`
- `code` (object, optional): Error code details
Expand All @@ -55,9 +53,8 @@ Handlers communicate with the test runner via **stdin/stdout**:

1. **Input Processing**: Read JSON requests line-by-line from stdin
2. **Response Order**: Responses must match request order (process sequentially)
3. **ID Matching**: Response `id` must exactly match the request `id`
4. **Error Handling**: Return error responses for invalid requests or failed operations
5. **Exit Behavior**: Exit cleanly when stdin closes
3. **Error Handling**: Return error responses for invalid requests or failed operations
4. **Exit Behavior**: Exit cleanly when stdin closes

## Test Suites and Expected Responses

Expand All @@ -73,14 +70,12 @@ Test cases where the script verification operation executes successfully and ret
**Expected Response Format:**
```json
{
"id": "test-id",
"result": true
}
```
or
```json
{
"id": "test-id",
"result": false
}
```
Expand All @@ -95,7 +90,6 @@ Test cases where the verification operation fails to determine validity of the s
**Expected Response Format:**
```json
{
"id": "test-id",
"result": null,
"error": {
"code": {
Expand Down
53 changes: 20 additions & 33 deletions runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,17 @@ func (tr *TestRunner) runTest(ctx context.Context, test TestCase) SingleTestResu
select {
case <-ctx.Done():
return SingleTestResult{
TestID: test.ID,
TestID: test.Request.ID,
Passed: false,
Message: fmt.Sprintf("Total execution timeout exceeded (%v)", tr.timeout),
}
default:
}

req := Request{
ID: test.ID,
Method: test.Method,
Params: test.Params,
}

err := tr.SendRequest(req)
err := tr.SendRequest(test.Request)
if err != nil {
return SingleTestResult{
TestID: test.ID,
TestID: test.Request.ID,
Passed: false,
Message: fmt.Sprintf("Failed to send request: %v", err),
}
Expand All @@ -151,36 +145,29 @@ func (tr *TestRunner) runTest(ctx context.Context, test TestCase) SingleTestResu
resp, err := tr.ReadResponse()
if err != nil {
return SingleTestResult{
TestID: test.ID,
TestID: test.Request.ID,
Passed: false,
Message: fmt.Sprintf("Failed to read response: %v", err),
}
}

if err := validateResponse(test, resp); err != nil {
return SingleTestResult{
TestID: test.ID,
TestID: test.Request.ID,
Passed: false,
Message: fmt.Sprintf("Invalid response: %s", err.Error()),
}
}
return SingleTestResult{
TestID: test.ID,
TestID: test.Request.ID,
Passed: true,
}
}

// validateResponse validates that a response matches the expected test outcome.
// Returns an error if:
//
// Response ID does not match the request ID
// Response does not match the expected outcome (error or success)
// Returns an error if the response does not match the expected outcome (error or success).
func validateResponse(test TestCase, resp *Response) error {
if resp.ID != test.ID {
return fmt.Errorf("response ID mismatch: expected %s, got %s", test.ID, resp.ID)
}

if test.Expected.Error != nil {
if test.ExpectedResponse.Error != nil {
return validateResponseForError(test, resp)
}

Expand All @@ -191,14 +178,14 @@ func validateResponse(test TestCase, resp *Response) error {
// It ensures the response contains an error, the result is null or omitted, and if an
// error code is expected, it matches the expected type and member.
func validateResponseForError(test TestCase, resp *Response) error {
if test.Expected.Error == nil {
if test.ExpectedResponse.Error == nil {
panic("validateResponseForError expects non-nil error")
}

if resp.Error == nil {
if test.Expected.Error.Code != nil {
if test.ExpectedResponse.Error.Code != nil {
return fmt.Errorf("expected error %s.%s, but got no error",
test.Expected.Error.Code.Type, test.Expected.Error.Code.Member)
test.ExpectedResponse.Error.Code.Type, test.ExpectedResponse.Error.Code.Member)
}
return fmt.Errorf("expected error, but got no error")
}
Expand All @@ -207,18 +194,18 @@ func validateResponseForError(test TestCase, resp *Response) error {
return fmt.Errorf("expected result to be null or omitted when error is present, got: %s", string(resp.Result))
}

if test.Expected.Error.Code != nil {
if test.ExpectedResponse.Error.Code != nil {
if resp.Error.Code == nil {
return fmt.Errorf("expected error code %s.%s, but got error with no code",
test.Expected.Error.Code.Type, test.Expected.Error.Code.Member)
test.ExpectedResponse.Error.Code.Type, test.ExpectedResponse.Error.Code.Member)
}

if resp.Error.Code.Type != test.Expected.Error.Code.Type {
return fmt.Errorf("expected error type %s, got %s", test.Expected.Error.Code.Type, resp.Error.Code.Type)
if resp.Error.Code.Type != test.ExpectedResponse.Error.Code.Type {
return fmt.Errorf("expected error type %s, got %s", test.ExpectedResponse.Error.Code.Type, resp.Error.Code.Type)
}

if resp.Error.Code.Member != test.Expected.Error.Code.Member {
return fmt.Errorf("expected error member %s, got %s", test.Expected.Error.Code.Member, resp.Error.Code.Member)
if resp.Error.Code.Member != test.ExpectedResponse.Error.Code.Member {
return fmt.Errorf("expected error member %s, got %s", test.ExpectedResponse.Error.Code.Member, resp.Error.Code.Member)
}
}
return nil
Expand All @@ -228,7 +215,7 @@ func validateResponseForError(test TestCase, resp *Response) error {
// It ensures the response contains no error, and if a result is expected, it matches the
// expected value.
func validateResponseForSuccess(test TestCase, resp *Response) error {
if test.Expected.Error != nil {
if test.ExpectedResponse.Error != nil {
panic("validateResponseForSuccess expects nil error")
}

Expand All @@ -239,14 +226,14 @@ func validateResponseForSuccess(test TestCase, resp *Response) error {
return fmt.Errorf("expected success with no error, but got error")
}

if test.Expected.Result.IsNullOrOmitted() {
if test.ExpectedResponse.Result.IsNullOrOmitted() {
if !resp.Result.IsNullOrOmitted() {
return fmt.Errorf("expected null or omitted result, got: %s", string(resp.Result))
}
return nil
}

expectedNorm, err := test.Expected.Result.Normalize()
expectedNorm, err := test.ExpectedResponse.Result.Normalize()
if err != nil {
return fmt.Errorf("failed to normalize expected result: %w", err)
}
Expand Down
Loading
Loading