Skip to content

Commit c6708fe

Browse files
committed
Restructure expected response format and improve semantics
Also refactor response validation to be aligned with the improved semantics: - Success case: result contains the return value (or null for void/nullptr), error must be null/omitted - Error case: result must be null/omitted, error contains error details Additional changes: - Replace Error.type/variant with Error.code.type/member - Split validation into separate validateResponseForSuccess/Error functions - Update test cases to reflect new format
1 parent 9828c21 commit c6708fe

File tree

5 files changed

+242
-205
lines changed

5 files changed

+242
-205
lines changed

cmd/mock-handler/main.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ func handleRequest(line string, testIndex map[string]string) error {
8181
resp := runner.Response{
8282
ID: req.ID,
8383
Error: &runner.Error{
84-
Type: "Handler",
85-
Variant: "UnknownTest",
84+
Code: &runner.ErrorCode{
85+
Type: "Handler",
86+
Member: "UNKNOWN_TEST",
87+
},
8688
},
8789
}
8890
return writeResponse(resp)
@@ -94,8 +96,10 @@ func handleRequest(line string, testIndex map[string]string) error {
9496
resp := runner.Response{
9597
ID: req.ID,
9698
Error: &runner.Error{
97-
Type: "Handler",
98-
Variant: "LoadError",
99+
Code: &runner.ErrorCode{
100+
Type: "Handler",
101+
Member: "LOAD_ERROR",
102+
},
99103
},
100104
}
101105
return writeResponse(resp)
@@ -113,8 +117,10 @@ func handleRequest(line string, testIndex map[string]string) error {
113117
resp := runner.Response{
114118
ID: req.ID,
115119
Error: &runner.Error{
116-
Type: "Handler",
117-
Variant: "TestNotFound",
120+
Code: &runner.ErrorCode{
121+
Type: "Handler",
122+
Member: "TEST_NOT_FOUND",
123+
},
118124
},
119125
}
120126
return writeResponse(resp)
@@ -125,18 +131,20 @@ func handleRequest(line string, testIndex map[string]string) error {
125131
resp := runner.Response{
126132
ID: req.ID,
127133
Error: &runner.Error{
128-
Type: "Handler",
129-
Variant: "MethodMismatch",
134+
Code: &runner.ErrorCode{
135+
Type: "Handler",
136+
Member: "METHOD_MISMATCH",
137+
},
130138
},
131139
}
132140
return writeResponse(resp)
133141
}
134142

135143
// Build response based on expected result
136144
return writeResponse(runner.Response{
137-
ID: req.ID,
138-
Success: testCase.Expected.Success,
139-
Error: testCase.Expected.Error,
145+
ID: req.ID,
146+
Result: testCase.Expected.Result,
147+
Error: testCase.Expected.Error,
140148
})
141149
}
142150

runner/runner.go

Lines changed: 88 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,15 @@ func (tr *TestRunner) RunTestSuite(suite TestSuite) TestResult {
112112
}
113113

114114
for _, test := range suite.Tests {
115-
testResult := tr.runTest(test)
115+
err := tr.runTest(test)
116+
testResult := SingleTestResult{
117+
TestID: test.ID,
118+
Passed: err == nil,
119+
}
120+
if err != nil {
121+
testResult.Message = err.Error()
122+
}
123+
116124
result.TestResults = append(result.TestResults, testResult)
117125
if testResult.Passed {
118126
result.PassedTests++
@@ -124,8 +132,9 @@ func (tr *TestRunner) RunTestSuite(suite TestSuite) TestResult {
124132
return result
125133
}
126134

127-
// runTest executes a single test case
128-
func (tr *TestRunner) runTest(test TestCase) SingleTestResult {
135+
// runTest executes a single test case and validates the response.
136+
// Returns an error if communication with the handler fails or validation fails.
137+
func (tr *TestRunner) runTest(test TestCase) error {
129138
req := Request{
130139
ID: test.ID,
131140
Method: test.Method,
@@ -134,118 +143,102 @@ func (tr *TestRunner) runTest(test TestCase) SingleTestResult {
134143

135144
resp, err := tr.SendRequest(req)
136145
if err != nil {
137-
return SingleTestResult{
138-
TestID: test.ID,
139-
Passed: false,
140-
Message: fmt.Sprintf("Failed to send request: %v", err),
141-
}
142-
}
143-
if resp.ID != test.ID {
144-
return SingleTestResult{
145-
TestID: test.ID,
146-
Passed: false,
147-
Message: fmt.Sprintf("Response ID mismatch: expected %s, got %s", test.ID, resp.ID),
148-
}
146+
return err
149147
}
148+
150149
return validateResponse(test, resp)
151150
}
152151

153-
// validateResponse checks if response matches expected result
154-
func validateResponse(test TestCase, resp *Response) SingleTestResult {
155-
// Check if we expected an error
152+
// validateResponse validates that a response matches the expected test outcome.
153+
// Returns an error if:
154+
//
155+
// Response ID does not match the request ID
156+
// Response does not match the expected outcome (error or success)
157+
func validateResponse(test TestCase, resp *Response) error {
158+
if resp.ID != test.ID {
159+
return fmt.Errorf("response ID mismatch: expected %s, got %s", test.ID, resp.ID)
160+
}
161+
156162
if test.Expected.Error != nil {
157-
if resp.Error == nil {
158-
return SingleTestResult{
159-
TestID: test.ID,
160-
Passed: false,
161-
Message: fmt.Sprintf("Expected error type %s, but got no error", test.Expected.Error.Type),
162-
}
163-
}
163+
return validateResponseForError(test, resp)
164+
}
164165

165-
// Check error type
166-
if resp.Error.Type != test.Expected.Error.Type {
167-
return SingleTestResult{
168-
TestID: test.ID,
169-
Passed: false,
170-
Message: fmt.Sprintf("Expected error type %s, got %s", test.Expected.Error.Type, resp.Error.Type),
171-
}
172-
}
166+
return validateResponseForSuccess(test, resp)
167+
}
173168

174-
// Check error variant if specified
175-
if test.Expected.Error.Variant != "" && resp.Error.Variant != test.Expected.Error.Variant {
176-
return SingleTestResult{
177-
TestID: test.ID,
178-
Passed: false,
179-
Message: fmt.Sprintf("Expected error variant %s, got %s", test.Expected.Error.Variant, resp.Error.Variant),
180-
}
181-
}
169+
// validateResponseForError validates that a response correctly represents an error case.
170+
// It ensures the response contains an error, the result is null or omitted, and if an
171+
// error code is expected, it matches the expected type and member.
172+
func validateResponseForError(test TestCase, resp *Response) error {
173+
if test.Expected.Error == nil {
174+
panic("validateResponseForError expects non-nil error")
175+
}
182176

183-
return SingleTestResult{
184-
TestID: test.ID,
185-
Passed: true,
186-
Message: "Test passed (expected error matched)",
177+
if resp.Error == nil {
178+
if test.Expected.Error.Code != nil {
179+
return fmt.Errorf("expected error %s.%s, but got no error",
180+
test.Expected.Error.Code.Type, test.Expected.Error.Code.Member)
187181
}
182+
return fmt.Errorf("expected error, but got no error")
188183
}
189184

190-
// Check if we expected success
191-
if test.Expected.Success != nil {
192-
if resp.Error != nil {
193-
errMsg := fmt.Sprintf("Expected success, but got error: %s", resp.Error.Type)
194-
if resp.Error.Variant != "" {
195-
errMsg += fmt.Sprintf(" (variant: %s)", resp.Error.Variant)
196-
}
197-
return SingleTestResult{
198-
TestID: test.ID,
199-
Passed: false,
200-
Message: errMsg,
201-
}
202-
}
185+
if !resp.Result.IsNullOrOmitted() {
186+
return fmt.Errorf("expected result to be null or omitted when error is present, got: %s", string(resp.Result))
187+
}
203188

204-
if resp.Success == nil {
205-
return SingleTestResult{
206-
TestID: test.ID,
207-
Passed: false,
208-
Message: "Expected success, but response contained no success field",
209-
}
189+
if test.Expected.Error.Code != nil {
190+
if resp.Error.Code == nil {
191+
return fmt.Errorf("expected error code %s.%s, but got error with no code",
192+
test.Expected.Error.Code.Type, test.Expected.Error.Code.Member)
210193
}
211194

212-
// Normalize JSON for comparison
213-
var expectedData, actualData interface{}
214-
if err := json.Unmarshal(bytes.TrimSpace(*test.Expected.Success), &expectedData); err != nil {
215-
return SingleTestResult{
216-
TestID: test.ID,
217-
Passed: false,
218-
Message: fmt.Sprintf("Invalid expected JSON: %v", err),
219-
}
195+
if resp.Error.Code.Type != test.Expected.Error.Code.Type {
196+
return fmt.Errorf("expected error type %s, got %s", test.Expected.Error.Code.Type, resp.Error.Code.Type)
220197
}
221-
if err := json.Unmarshal(bytes.TrimSpace(*resp.Success), &actualData); err != nil {
222-
return SingleTestResult{
223-
TestID: test.ID,
224-
Passed: false,
225-
Message: fmt.Sprintf("Invalid response JSON: %v", err),
226-
}
198+
199+
if resp.Error.Code.Member != test.Expected.Error.Code.Member {
200+
return fmt.Errorf("expected error member %s, got %s", test.Expected.Error.Code.Member, resp.Error.Code.Member)
227201
}
228-
expectedNormalized, _ := json.Marshal(expectedData)
229-
actualNormalized, _ := json.Marshal(actualData)
230-
231-
if !bytes.Equal(expectedNormalized, actualNormalized) {
232-
return SingleTestResult{
233-
TestID: test.ID,
234-
Passed: false,
235-
Message: fmt.Sprintf("Success mismatch:\nExpected: %s\nActual: %s", string(expectedNormalized), string(actualNormalized)),
236-
}
202+
}
203+
return nil
204+
}
205+
206+
// validateResponseForSuccess validates that a response correctly represents a success case.
207+
// It ensures the response contains no error, and if a result is expected, it matches the
208+
// expected value.
209+
func validateResponseForSuccess(test TestCase, resp *Response) error {
210+
if test.Expected.Error != nil {
211+
panic("validateResponseForSuccess expects nil error")
212+
}
213+
214+
if resp.Error != nil {
215+
if resp.Error.Code != nil {
216+
return fmt.Errorf("expected success with no error, but got error: %s.%s", resp.Error.Code.Type, resp.Error.Code.Member)
237217
}
238-
return SingleTestResult{
239-
TestID: test.ID,
240-
Passed: true,
241-
Message: "Test passed",
218+
return fmt.Errorf("expected success with no error, but got error")
219+
}
220+
221+
if test.Expected.Result.IsNullOrOmitted() {
222+
if !resp.Result.IsNullOrOmitted() {
223+
return fmt.Errorf("expected null or omitted result, got: %s", string(resp.Result))
242224
}
225+
return nil
243226
}
244-
return SingleTestResult{
245-
TestID: test.ID,
246-
Passed: false,
247-
Message: "Test has no expected result defined",
227+
228+
expectedNorm, err := test.Expected.Result.Normalize()
229+
if err != nil {
230+
return fmt.Errorf("failed to normalize expected result: %w", err)
248231
}
232+
233+
actualNorm, err := resp.Result.Normalize()
234+
if err != nil {
235+
return fmt.Errorf("failed to normalize actual result: %w", err)
236+
}
237+
238+
if expectedNorm != actualNorm {
239+
return fmt.Errorf("result mismatch: expected %s, got %s", expectedNorm, actualNorm)
240+
}
241+
return nil
249242
}
250243

251244
// TestResult contains results from running a test suite

runner/types.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ type TestCase struct {
1313
Expected TestExpectation `json:"expected"`
1414
}
1515

16-
// TestExpectation defines what response is expected
16+
// TestExpectation defines what response is expected.
17+
// If expecting success, result contains the expected value (or null for void/nullptr) and error must be null.
18+
// If expecting failure, result must be null and error contains the expected error details.
1719
type TestExpectation struct {
18-
Success *json.RawMessage `json:"success,omitempty"` // Expected successful result
19-
Error *Error `json:"error,omitempty"` // Expected error
20+
Result Result `json:"result"` // Expected return value (null for void/nullptr/error cases)
21+
Error *Error `json:"error,omitempty"` // Expected error (null for success cases)
2022
}
2123

2224
// TestSuite represents a collection of test cases
@@ -33,15 +35,54 @@ type Request struct {
3335
Params json.RawMessage `json:"params"`
3436
}
3537

36-
// Response represents a response from the handler
38+
// Response represents a response from the handler.
39+
// If the operation succeeds, result contains the return value (or null for void/nullptr) and error must be null.
40+
// If the operation fails, result must be null and error contains error details.
3741
type Response struct {
38-
ID string `json:"id"`
39-
Success *json.RawMessage `json:"success,omitempty"`
40-
Error *Error `json:"error,omitempty"`
42+
ID string `json:"id"`
43+
Result Result `json:"result"` // Return value (null for void/nullptr/error cases)
44+
Error *Error `json:"error,omitempty"` // Error details (null for success cases)
4145
}
4246

43-
// Error represents an error response
47+
// Error represents an error response.
48+
// Code can be null for generic errors without specific error codes.
4449
type Error struct {
45-
Type string `json:"type"`
46-
Variant string `json:"variant,omitempty"`
50+
Code *ErrorCode `json:"code,omitempty"`
51+
}
52+
53+
type ErrorCode struct {
54+
Type string `json:"type"` // e.g., "btck_ScriptVerifyStatus"
55+
Member string `json:"member"` // e.g., "ERROR_INVALID_FLAGS_COMBINATION"
56+
}
57+
58+
// Result is a type alias for json.RawMessage with helper methods.
59+
type Result json.RawMessage
60+
61+
// MarshalJSON implements json.Marshaler by delegating to json.RawMessage.
62+
func (r Result) MarshalJSON() ([]byte, error) {
63+
return (json.RawMessage)(r).MarshalJSON()
64+
}
65+
66+
// UnmarshalJSON implements json.Unmarshaler by delegating to json.RawMessage.
67+
func (r *Result) UnmarshalJSON(data []byte) error {
68+
return (*json.RawMessage)(r).UnmarshalJSON(data)
69+
}
70+
71+
// IsNullOrOmitted checks if the result is nil or represents a JSON null value.
72+
func (r Result) IsNullOrOmitted() bool {
73+
return r == nil || string(r) == "null"
74+
}
75+
76+
// Normalize normalizes JSON data by parsing and re-marshaling it.
77+
// This ensures consistent formatting and key ordering for comparison.
78+
func (r Result) Normalize() (string, error) {
79+
var v interface{}
80+
if err := json.Unmarshal(r, &v); err != nil {
81+
return "", err
82+
}
83+
normalized, err := json.Marshal(v)
84+
if err != nil {
85+
return "", err
86+
}
87+
return string(normalized), nil
4788
}

0 commit comments

Comments
 (0)