1
2
3
4 package websocket_test
5
6 import (
7 "context"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "os"
14 "os/exec"
15 "strconv"
16 "strings"
17 "testing"
18 "time"
19
20 "nhooyr.io/websocket"
21 "nhooyr.io/websocket/internal/errd"
22 "nhooyr.io/websocket/internal/test/assert"
23 "nhooyr.io/websocket/internal/test/wstest"
24 "nhooyr.io/websocket/internal/util"
25 )
26
27 var excludedAutobahnCases = []string{
28
29
30 "6.*", "7.5.1",
31
32
33
34 "13.3.*", "13.4.*", "13.5.*", "13.6.*",
35 }
36
37 var autobahnCases = []string{"*"}
38
39
40
41
42 var onlyAutobahnCases = []string{}
43
44 func TestAutobahn(t *testing.T) {
45 t.Parallel()
46
47 if os.Getenv("AUTOBAHN") == "" {
48 t.SkipNow()
49 }
50
51 if os.Getenv("AUTOBAHN") == "fast" {
52
53 excludedAutobahnCases = append(excludedAutobahnCases,
54 "9.*", "12.*", "13.*",
55 )
56 }
57
58 if len(onlyAutobahnCases) > 0 {
59 excludedAutobahnCases = []string{}
60 autobahnCases = onlyAutobahnCases
61 }
62
63 ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
64 defer cancel()
65
66 wstestURL, closeFn, err := wstestServer(t, ctx)
67 assert.Success(t, err)
68 defer func() {
69 assert.Success(t, closeFn())
70 }()
71
72 err = waitWS(ctx, wstestURL)
73 assert.Success(t, err)
74
75 cases, err := wstestCaseCount(ctx, wstestURL)
76 assert.Success(t, err)
77
78 t.Run("cases", func(t *testing.T) {
79 for i := 1; i <= cases; i++ {
80 i := i
81 t.Run("", func(t *testing.T) {
82 ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
83 defer cancel()
84
85 c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), &websocket.DialOptions{
86 CompressionMode: websocket.CompressionContextTakeover,
87 })
88 assert.Success(t, err)
89 err = wstest.EchoLoop(ctx, c)
90 t.Logf("echoLoop: %v", err)
91 })
92 }
93 })
94
95 c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil)
96 assert.Success(t, err)
97 c.Close(websocket.StatusNormalClosure, "")
98
99 checkWSTestIndex(t, "./ci/out/autobahn-report/index.json")
100 }
101
102 func waitWS(ctx context.Context, url string) error {
103 ctx, cancel := context.WithTimeout(ctx, time.Second*5)
104 defer cancel()
105
106 for ctx.Err() == nil {
107 c, _, err := websocket.Dial(ctx, url, nil)
108 if err != nil {
109 continue
110 }
111 c.Close(websocket.StatusNormalClosure, "")
112 return nil
113 }
114
115 return ctx.Err()
116 }
117
118 func wstestServer(tb testing.TB, ctx context.Context) (url string, closeFn func() error, err error) {
119 defer errd.Wrap(&err, "failed to start autobahn wstest server")
120
121 serverAddr, err := unusedListenAddr()
122 if err != nil {
123 return "", nil, err
124 }
125 _, serverPort, err := net.SplitHostPort(serverAddr)
126 if err != nil {
127 return "", nil, err
128 }
129
130 url = "ws://" + serverAddr
131 const outDir = "ci/out/autobahn-report"
132
133 specFile, err := tempJSONFile(map[string]interface{}{
134 "url": url,
135 "outdir": outDir,
136 "cases": autobahnCases,
137 "exclude-cases": excludedAutobahnCases,
138 })
139 if err != nil {
140 return "", nil, fmt.Errorf("failed to write spec: %w", err)
141 }
142
143 ctx, cancel := context.WithTimeout(ctx, time.Hour)
144 defer func() {
145 if err != nil {
146 cancel()
147 }
148 }()
149
150 dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite")
151 dockerPull.Stdout = util.WriterFunc(func(p []byte) (int, error) {
152 tb.Log(string(p))
153 return len(p), nil
154 })
155 dockerPull.Stderr = util.WriterFunc(func(p []byte) (int, error) {
156 tb.Log(string(p))
157 return len(p), nil
158 })
159 tb.Log(dockerPull)
160 err = dockerPull.Run()
161 if err != nil {
162 return "", nil, fmt.Errorf("failed to pull docker image: %w", err)
163 }
164
165 wd, err := os.Getwd()
166 if err != nil {
167 return "", nil, err
168 }
169
170 var args []string
171 args = append(args, "run", "-i", "--rm",
172 "-v", fmt.Sprintf("%s:%[1]s", specFile),
173 "-v", fmt.Sprintf("%s/ci:/ci", wd),
174 fmt.Sprintf("-p=%s:%s", serverAddr, serverPort),
175 "crossbario/autobahn-testsuite",
176 )
177 args = append(args, "wstest", "--mode", "fuzzingserver", "--spec", specFile,
178
179
180 "--webport=0",
181 )
182 wstest := exec.CommandContext(ctx, "docker", args...)
183 wstest.Stdout = util.WriterFunc(func(p []byte) (int, error) {
184 tb.Log(string(p))
185 return len(p), nil
186 })
187 wstest.Stderr = util.WriterFunc(func(p []byte) (int, error) {
188 tb.Log(string(p))
189 return len(p), nil
190 })
191 tb.Log(wstest)
192 err = wstest.Start()
193 if err != nil {
194 return "", nil, fmt.Errorf("failed to start wstest: %w", err)
195 }
196
197 return url, func() error {
198 err = wstest.Process.Kill()
199 if err != nil {
200 return fmt.Errorf("failed to kill wstest: %w", err)
201 }
202 err = wstest.Wait()
203 var ee *exec.ExitError
204 if errors.As(err, &ee) && ee.ExitCode() == -1 {
205 return nil
206 }
207 return err
208 }, nil
209 }
210
211 func wstestCaseCount(ctx context.Context, url string) (cases int, err error) {
212 defer errd.Wrap(&err, "failed to get case count")
213
214 c, _, err := websocket.Dial(ctx, url+"/getCaseCount", nil)
215 if err != nil {
216 return 0, err
217 }
218 defer c.Close(websocket.StatusInternalError, "")
219
220 _, r, err := c.Reader(ctx)
221 if err != nil {
222 return 0, err
223 }
224 b, err := io.ReadAll(r)
225 if err != nil {
226 return 0, err
227 }
228 cases, err = strconv.Atoi(string(b))
229 if err != nil {
230 return 0, err
231 }
232
233 c.Close(websocket.StatusNormalClosure, "")
234
235 return cases, nil
236 }
237
238 func checkWSTestIndex(t *testing.T, path string) {
239 wstestOut, err := os.ReadFile(path)
240 assert.Success(t, err)
241
242 var indexJSON map[string]map[string]struct {
243 Behavior string `json:"behavior"`
244 BehaviorClose string `json:"behaviorClose"`
245 }
246 err = json.Unmarshal(wstestOut, &indexJSON)
247 assert.Success(t, err)
248
249 for _, tests := range indexJSON {
250 for test, result := range tests {
251 t.Run(test, func(t *testing.T) {
252 switch result.BehaviorClose {
253 case "OK", "INFORMATIONAL":
254 default:
255 t.Errorf("bad close behaviour")
256 }
257
258 switch result.Behavior {
259 case "OK", "NON-STRICT", "INFORMATIONAL":
260 default:
261 t.Errorf("failed")
262 }
263 })
264 }
265 }
266
267 if t.Failed() {
268 htmlPath := strings.Replace(path, ".json", ".html", 1)
269 t.Errorf("detected autobahn violation, see %q", htmlPath)
270 }
271 }
272
273 func unusedListenAddr() (_ string, err error) {
274 defer errd.Wrap(&err, "failed to get unused listen address")
275 l, err := net.Listen("tcp", "localhost:0")
276 if err != nil {
277 return "", err
278 }
279 l.Close()
280 return l.Addr().String(), nil
281 }
282
283 func tempJSONFile(v interface{}) (string, error) {
284 f, err := os.CreateTemp("", "temp.json")
285 if err != nil {
286 return "", fmt.Errorf("temp file: %w", err)
287 }
288 defer f.Close()
289
290 e := json.NewEncoder(f)
291 e.SetIndent("", "\t")
292 err = e.Encode(v)
293 if err != nil {
294 return "", fmt.Errorf("json encode: %w", err)
295 }
296
297 err = f.Close()
298 if err != nil {
299 return "", fmt.Errorf("close temp file: %w", err)
300 }
301
302 return f.Name(), nil
303 }
304
View as plain text