Skip to content

Commit ca8e166

Browse files
authoredFeb 1, 2024
simulators/ethereum/rpc-compat: handle test comments and avoid error message comparison (#984)
1 parent a8fe1d5 commit ca8e166

File tree

5 files changed

+216
-111
lines changed

5 files changed

+216
-111
lines changed
 

‎simulators/ethereum/rpc-compat/go.mod

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ require (
2525
github.com/sergi/go-diff v1.2.0 // indirect
2626
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
2727
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
28+
github.com/tidwall/gjson v1.17.0 // indirect
29+
github.com/tidwall/match v1.1.1 // indirect
30+
github.com/tidwall/pretty v1.2.0 // indirect
31+
github.com/tidwall/sjson v1.2.5 // indirect
2832
github.com/tklauser/go-sysconf v0.3.12 // indirect
2933
github.com/tklauser/numcpus v0.6.1 // indirect
3034
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect

‎simulators/ethereum/rpc-compat/go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
9090
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
9191
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
9292
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
93+
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
94+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
95+
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
96+
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
97+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
98+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
99+
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
100+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
101+
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
102+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
93103
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
94104
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
95105
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=

‎simulators/ethereum/rpc-compat/main.go

+52-111
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import (
77
"io"
88
"net"
99
"net/http"
10-
"os"
11-
"path/filepath"
1210
"regexp"
1311
"strings"
1412
"time"
1513

1614
"github.com/ethereum/go-ethereum/common"
1715
"github.com/ethereum/hive/hivesim"
16+
"github.com/tidwall/gjson"
17+
"github.com/tidwall/sjson"
1818
diff "github.com/yudai/gojsondiff"
1919
"github.com/yudai/gojsondiff/formatter"
2020
)
@@ -26,11 +26,6 @@ var (
2626
}
2727
)
2828

29-
type test struct {
30-
Name string
31-
Data []byte
32-
}
33-
3429
func main() {
3530
// Load fork environment.
3631
var clientEnv hivesim.Params
@@ -66,63 +61,71 @@ conformance with the execution API specification.`[1:],
6661
func runAllTests(t *hivesim.T, c *hivesim.Client, clientName string) {
6762
_, testPattern := t.Sim.TestPattern()
6863
re := regexp.MustCompile(testPattern)
69-
7064
tests := loadTests(t, "tests", re)
7165
for _, test := range tests {
66+
test := test
7267
t.Run(hivesim.TestSpec{
73-
Name: fmt.Sprintf("%s (%s)", test.Name, clientName),
68+
Name: fmt.Sprintf("%s (%s)", test.name, clientName),
69+
Description: test.comment,
7470
Run: func(t *hivesim.T) {
75-
if err := runTest(t, c, test.Data); err != nil {
71+
if err := runTest(t, c, &test); err != nil {
7672
t.Fatal(err)
7773
}
7874
},
7975
})
8076
}
8177
}
8278

83-
func runTest(t *hivesim.T, c *hivesim.Client, data []byte) error {
79+
func runTest(t *hivesim.T, c *hivesim.Client, test *rpcTest) error {
8480
var (
85-
client = &http.Client{
86-
Timeout: 5 * time.Second,
87-
Transport: &loggingRoundTrip{
88-
t: t,
89-
inner: http.DefaultTransport,
90-
},
91-
}
92-
url = fmt.Sprintf("http://%s", net.JoinHostPort(c.IP.String(), "8545"))
93-
err error
94-
resp []byte
81+
client = &http.Client{Timeout: 5 * time.Second}
82+
url = fmt.Sprintf("http://%s", net.JoinHostPort(c.IP.String(), "8545"))
83+
err error
84+
respBytes []byte
9585
)
9686

97-
for _, line := range strings.Split(string(data), "\n") {
98-
line = strings.TrimSpace(line)
99-
switch {
100-
case len(line) == 0 || strings.HasPrefix(line, "//"):
101-
// Skip comments, blank lines.
102-
continue
103-
case strings.HasPrefix(line, ">> "):
87+
for _, msg := range test.messages {
88+
if msg.send {
10489
// Send request.
105-
resp, err = postHttp(client, url, []byte(line[3:]))
90+
t.Log(">> ", msg.data)
91+
respBytes, err = postHttp(client, url, strings.NewReader(msg.data))
10692
if err != nil {
10793
return err
10894
}
109-
case strings.HasPrefix(line, "<< "):
110-
// Read response. Unmarshal to interface{} to verify deep equality. Marshal
111-
// again to remove padding differences and to print each field in the same
112-
// order. This makes it easy to spot any discrepancies.
113-
if resp == nil {
95+
} else {
96+
// Receive a response.
97+
if respBytes == nil {
11498
return fmt.Errorf("invalid test, response before request")
11599
}
116-
want := []byte(strings.TrimSpace(line)[3:]) // trim leading "<< "
117-
// Now compare.
118-
d, err := diff.New().Compare(resp, want)
100+
expectedData := msg.data
101+
resp := string(bytes.TrimSpace(respBytes))
102+
t.Log("<< ", resp)
103+
if !gjson.Valid(resp) {
104+
return fmt.Errorf("invalid JSON response")
105+
}
106+
107+
// Patch JSON to remove error messages. We only do this in the specific case
108+
// where an error is expected AND returned by the client.
109+
var errorRedacted bool
110+
if gjson.Get(resp, "error").Exists() && gjson.Get(expectedData, "error").Exists() {
111+
resp, _ = sjson.Delete(resp, "error.message")
112+
expectedData, _ = sjson.Delete(expectedData, "error.message")
113+
errorRedacted = true
114+
}
115+
116+
// Compare responses.
117+
d, err := diff.New().Compare([]byte(resp), []byte(expectedData))
119118
if err != nil {
120119
return fmt.Errorf("failed to unmarshal value: %s\n", err)
121120
}
121+
122122
// If there is a discrepancy, return error.
123123
if d.Modified() {
124-
var got map[string]interface{}
125-
json.Unmarshal(resp, &got)
124+
if errorRedacted {
125+
t.Log("note: error messages removed from comparison")
126+
}
127+
var got map[string]any
128+
json.Unmarshal([]byte(resp), &got)
126129
config := formatter.AsciiFormatterConfig{
127130
ShowArrayIndex: true,
128131
Coloring: false,
@@ -131,22 +134,20 @@ func runTest(t *hivesim.T, c *hivesim.Client, data []byte) error {
131134
diffString, _ := formatter.Format(d)
132135
return fmt.Errorf("response differs from expected (-- client, ++ test):\n%s", diffString)
133136
}
134-
resp = nil
135-
default:
136-
t.Fatalf("invalid line in test script: %s", line)
137+
respBytes = nil
137138
}
138139
}
139-
if resp != nil {
140+
141+
if respBytes != nil {
140142
t.Fatalf("unhandled response in test case")
141143
}
142144
return nil
143145
}
144146

145147
// sendHttp sends an HTTP POST with the provided json data and reads the
146148
// response into a byte slice and returns it.
147-
func postHttp(c *http.Client, url string, d []byte) ([]byte, error) {
148-
data := bytes.NewBuffer(d)
149-
req, err := http.NewRequest("POST", url, data)
149+
func postHttp(c *http.Client, url string, d io.Reader) ([]byte, error) {
150+
req, err := http.NewRequest("POST", url, d)
150151
if err != nil {
151152
return nil, fmt.Errorf("error building request: %v", err)
152153
}
@@ -158,35 +159,7 @@ func postHttp(c *http.Client, url string, d []byte) ([]byte, error) {
158159
return io.ReadAll(resp.Body)
159160
}
160161

161-
// loadTests walks the given directory looking for *.io files to load.
162-
func loadTests(t *hivesim.T, root string, re *regexp.Regexp) []test {
163-
tests := make([]test, 0)
164-
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
165-
if err != nil {
166-
t.Logf("unable to walk path: %s", err)
167-
return err
168-
}
169-
if info.IsDir() {
170-
return nil
171-
}
172-
if fname := info.Name(); !strings.HasSuffix(fname, ".io") {
173-
return nil
174-
}
175-
pathname := strings.TrimSuffix(strings.TrimPrefix(path, root), ".io")
176-
if !re.MatchString(pathname) {
177-
fmt.Println("skip", pathname)
178-
return nil // skip
179-
}
180-
data, err := os.ReadFile(path)
181-
if err != nil {
182-
return err
183-
}
184-
tests = append(tests, test{strings.TrimLeft(pathname, "/"), data})
185-
return nil
186-
})
187-
return tests
188-
}
189-
162+
// sendForkchoiceUpdated delivers the initial FcU request to the client.
190163
func sendForkchoiceUpdated(t *hivesim.T, client *hivesim.Client) {
191164
var request struct {
192165
Method string
@@ -195,43 +168,11 @@ func sendForkchoiceUpdated(t *hivesim.T, client *hivesim.Client) {
195168
if err := common.LoadJSON("tests/headfcu.json", &request); err != nil {
196169
t.Fatal("error loading forkchoiceUpdated:", err)
197170
}
198-
err := client.EngineAPI().Call(nil, request.Method, request.Params...)
171+
t.Logf("sending %s: %v", request.Method, request.Params)
172+
var resp any
173+
err := client.EngineAPI().Call(&resp, request.Method, request.Params...)
199174
if err != nil {
200175
t.Fatal("client rejected forkchoiceUpdated:", err)
201176
}
202-
}
203-
204-
// loggingRoundTrip writes requests and responses to the test log.
205-
type loggingRoundTrip struct {
206-
t *hivesim.T
207-
inner http.RoundTripper
208-
}
209-
210-
func (rt *loggingRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) {
211-
// Read and log the request body.
212-
reqBytes, err := io.ReadAll(req.Body)
213-
req.Body.Close()
214-
if err != nil {
215-
return nil, err
216-
}
217-
rt.t.Logf(">> %s", bytes.TrimSpace(reqBytes))
218-
reqCopy := *req
219-
reqCopy.Body = io.NopCloser(bytes.NewReader(reqBytes))
220-
221-
// Do the round trip.
222-
resp, err := rt.inner.RoundTrip(&reqCopy)
223-
if err != nil {
224-
return nil, err
225-
}
226-
defer resp.Body.Close()
227-
228-
// Read and log the response bytes.
229-
respBytes, err := io.ReadAll(resp.Body)
230-
if err != nil {
231-
return nil, err
232-
}
233-
respCopy := *resp
234-
respCopy.Body = io.NopCloser(bytes.NewReader(respBytes))
235-
rt.t.Logf("<< %s", bytes.TrimSpace(respBytes))
236-
return &respCopy, nil
177+
t.Logf("response: %v", resp)
237178
}
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"regexp"
10+
"strings"
11+
12+
"github.com/ethereum/hive/hivesim"
13+
"github.com/tidwall/gjson"
14+
)
15+
16+
type rpcTest struct {
17+
name string
18+
comment string
19+
speconly bool
20+
messages []rpcTestMessage
21+
}
22+
23+
type rpcTestMessage struct {
24+
data string
25+
// if true, the message is a send (>>), otherwise it's a receive (<<)
26+
send bool
27+
}
28+
29+
func loadTestFile(name string, r io.Reader) (rpcTest, error) {
30+
var (
31+
rdr = bufio.NewReader(r)
32+
scan = bufio.NewScanner(rdr)
33+
inHeader = true
34+
test = rpcTest{name: name}
35+
)
36+
for scan.Scan() {
37+
line := strings.TrimSpace(scan.Text())
38+
switch {
39+
case len(line) == 0:
40+
continue
41+
42+
case strings.HasPrefix(line, "//"):
43+
if !inHeader {
44+
continue // ignore comments after requests
45+
}
46+
text := strings.TrimPrefix(strings.TrimPrefix(line, "//"), " ")
47+
test.comment += text + "\n"
48+
if strings.HasPrefix(text, "speconly:") {
49+
test.speconly = true
50+
}
51+
52+
case strings.HasPrefix(line, ">>") || strings.HasPrefix(line, "<<"):
53+
inHeader = false
54+
data := strings.TrimSpace(line[2:])
55+
if !gjson.Valid(data) {
56+
return test, fmt.Errorf("invalid JSON in line %q", line)
57+
}
58+
test.messages = append(test.messages, rpcTestMessage{
59+
data: data,
60+
send: strings.HasPrefix(line, ">>"),
61+
})
62+
63+
default:
64+
return test, fmt.Errorf("invalid test line: %q", line)
65+
}
66+
}
67+
return test, scan.Err()
68+
}
69+
70+
// loadTests walks the given directory looking for *.io files to load.
71+
func loadTests(t *hivesim.T, root string, re *regexp.Regexp) []rpcTest {
72+
var tests []rpcTest
73+
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
74+
if err != nil {
75+
t.Logf("unable to walk path: %s", err)
76+
return err
77+
}
78+
if info.IsDir() {
79+
return nil
80+
}
81+
if fname := info.Name(); !strings.HasSuffix(fname, ".io") {
82+
return nil
83+
}
84+
pathname := strings.TrimSuffix(strings.TrimPrefix(path, root+"/"), ".io")
85+
if !re.MatchString(pathname) {
86+
fmt.Println("skip", pathname)
87+
return nil // skip
88+
}
89+
fd, err := os.Open(path)
90+
if err != nil {
91+
return err
92+
}
93+
defer fd.Close()
94+
test, err := loadTestFile(pathname, fd)
95+
if err != nil {
96+
return fmt.Errorf("invalid test %s: %v", info.Name(), err)
97+
}
98+
tests = append(tests, test)
99+
return nil
100+
})
101+
return tests
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestLoad(t *testing.T) {
10+
data := `// this is a test comment
11+
// this is the second line
12+
// speconly: lalalala
13+
>> {"type":"send"}
14+
<< {"type":"recv"}
15+
`
16+
17+
expectedComment := `this is a test comment
18+
this is the second line
19+
speconly: lalalala
20+
`
21+
expectedMessages := []rpcTestMessage{
22+
{
23+
data: `{"type":"send"}`,
24+
send: true,
25+
},
26+
{
27+
data: `{"type":"recv"}`,
28+
send: false,
29+
},
30+
}
31+
32+
result, err := loadTestFile("the-test", strings.NewReader(data))
33+
if err != nil {
34+
t.Fatal("error:", err)
35+
}
36+
if result.name != "the-test" {
37+
t.Error("wrong test name:", result.comment)
38+
}
39+
if result.comment != expectedComment {
40+
t.Errorf("wrong test comment %q", result.comment)
41+
}
42+
if !result.speconly {
43+
t.Error("test is not marked speconly")
44+
}
45+
if !reflect.DeepEqual(result.messages, expectedMessages) {
46+
t.Errorf("wrong test messages %+v", result.messages)
47+
}
48+
}

0 commit comments

Comments
 (0)
Please sign in to comment.