...

Source file src/nhooyr.io/websocket/autobahn_test.go

Documentation: nhooyr.io/websocket

     1  //go:build !js
     2  // +build !js
     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  	// We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just
    29  	// more performance overhead.
    30  	"6.*", "7.5.1",
    31  
    32  	// We skip the tests related to requestMaxWindowBits as that is unimplemented due
    33  	// to limitations in compress/flate. See https://github.com/golang/go/issues/3155
    34  	"13.3.*", "13.4.*", "13.5.*", "13.6.*",
    35  }
    36  
    37  var autobahnCases = []string{"*"}
    38  
    39  // Used to run individual test cases. autobahnCases runs only those cases matched
    40  // and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases
    41  // is niled.
    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  		// These are the slow tests.
    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  		// Disables some server that runs as part of fuzzingserver mode.
   179  		// See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124
   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