good morning!!!!

Skip to content
Snippets Groups Projects
autobahn_test.go 6.76 KiB
Newer Older
Anmol Sethi's avatar
Anmol Sethi committed
// +build !js

Anmol Sethi's avatar
Anmol Sethi committed
package websocket_test

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
Anmol Sethi's avatar
Anmol Sethi committed
	"os"
Anmol Sethi's avatar
Anmol Sethi committed
	"os/exec"
	"strconv"
	"strings"
	"testing"
	"time"
Anmol Sethi's avatar
Anmol Sethi committed

	"nhooyr.io/websocket"
Anmol Sethi's avatar
Anmol Sethi committed
	"nhooyr.io/websocket/internal/errd"
Anmol Sethi's avatar
Anmol Sethi committed
	"nhooyr.io/websocket/internal/test/assert"
Anmol Sethi's avatar
Anmol Sethi committed
	"nhooyr.io/websocket/internal/test/wstest"
Anmol Sethi's avatar
Anmol Sethi committed
)

Anmol Sethi's avatar
Anmol Sethi committed
var excludedAutobahnCases = []string{
	// We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just
	// more performance overhead.
	"6.*", "7.5.1",

	// We skip the tests related to requestMaxWindowBits as that is unimplemented due
	// to limitations in compress/flate. See https://github.com/golang/go/issues/3155
	"13.3.*", "13.4.*", "13.5.*", "13.6.*",
}

var autobahnCases = []string{"*"}

// Used to run individual test cases. autobahnCases runs only those cases matched
// and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases
// is niled.
// TODO:
var forceAutobahnCases = []string{}

Anmol Sethi's avatar
Anmol Sethi committed
func TestAutobahn(t *testing.T) {
Anmol Sethi's avatar
Anmol Sethi committed
	t.Parallel()

Anmol Sethi's avatar
Anmol Sethi committed
	if os.Getenv("AUTOBAHN") == "" {
Anmol Sethi's avatar
Anmol Sethi committed
		t.SkipNow()
	}

Anmol Sethi's avatar
Anmol Sethi committed
	if os.Getenv("AUTOBAHN") == "fast" {
		// These are the slow tests.
		excludedAutobahnCases = append(excludedAutobahnCases,
	ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
Anmol Sethi's avatar
Anmol Sethi committed
	defer cancel()
Anmol Sethi's avatar
Anmol Sethi committed

	wstestURL, closeFn, err := wstestServer(ctx)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
	defer func() {
		assert.Success(t, closeFn())
	}()
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
	err = waitWS(ctx, wstestURL)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed

	cases, err := wstestCaseCount(ctx, wstestURL)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed

	t.Run("cases", func(t *testing.T) {
		for i := 1; i <= cases; i++ {
			i := i
			t.Run("", func(t *testing.T) {
				ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
Anmol Sethi's avatar
Anmol Sethi committed
				defer cancel()

				c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), &websocket.DialOptions{
					CompressionMode: websocket.CompressionContextTakeover,
				})
Anmol Sethi's avatar
Anmol Sethi committed
				assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed
				err = wstest.EchoLoop(ctx, c)
				t.Logf("echoLoop: %v", err)
Anmol Sethi's avatar
Anmol Sethi committed
			})
		}
	})

	c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed
	c.Close(websocket.StatusNormalClosure, "")

	checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json")
}

func waitWS(ctx context.Context, url string) error {
	ctx, cancel := context.WithTimeout(ctx, time.Second*5)
	defer cancel()

	for ctx.Err() == nil {
		c, _, err := websocket.Dial(ctx, url, nil)
		if err != nil {
			continue
		}
		c.Close(websocket.StatusNormalClosure, "")
		return nil
Anmol Sethi's avatar
Anmol Sethi committed
	}

Anmol Sethi's avatar
Anmol Sethi committed
	return ctx.Err()
}

// TODO: Let docker pick the port and use docker port to find it.
// Does mean we can't use -i but that's fine.
func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) {
	defer errd.Wrap(&err, "failed to start autobahn wstest server")

Anmol Sethi's avatar
Anmol Sethi committed
	serverAddr, err := unusedListenAddr()
Anmol Sethi's avatar
Anmol Sethi committed
	if err != nil {
Anmol Sethi's avatar
Anmol Sethi committed
		return "", nil, err
Anmol Sethi's avatar
Anmol Sethi committed
	}
	_, serverPort, err := net.SplitHostPort(serverAddr)
	if err != nil {
		return "", nil, err
	}
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
	url = "ws://" + serverAddr
	const outDir = "ci/out/wstestClientReports"
Anmol Sethi's avatar
Anmol Sethi committed

	specFile, err := tempJSONFile(map[string]interface{}{
		"url":           url,
Anmol Sethi's avatar
Anmol Sethi committed
		"cases":         autobahnCases,
		"exclude-cases": excludedAutobahnCases,
	})
Anmol Sethi's avatar
Anmol Sethi committed
	if err != nil {
		return "", nil, fmt.Errorf("failed to write spec: %w", err)
Anmol Sethi's avatar
Anmol Sethi committed
	}

	ctx, cancel := context.WithTimeout(ctx, time.Hour)
Anmol Sethi's avatar
Anmol Sethi committed
	defer func() {
		if err != nil {
			cancel()
		}
	}()
Anmol Sethi's avatar
Anmol Sethi committed

	dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite")
	// TODO: log to *testing.T
	dockerPull.Stdout = os.Stdout
	dockerPull.Stderr = os.Stderr
	err = dockerPull.Run()
	if err != nil {
		return "", nil, fmt.Errorf("failed to pull docker image: %w", err)
	}

	wd, err := os.Getwd()
	if err != nil {
		return "", nil, err
	}

	var args []string
	args = append(args, "run", "-i", "--rm",
		"-v", fmt.Sprintf("%s:%[1]s", specFile),
		"-v", fmt.Sprintf("%s/ci:/ci", wd),
		fmt.Sprintf("-p=%s:%s", serverAddr, serverPort),
		"crossbario/autobahn-testsuite",
	)
	args = append(args, "wstest", "--mode", "fuzzingserver", "--spec", specFile,
Anmol Sethi's avatar
Anmol Sethi committed
		// Disables some server that runs as part of fuzzingserver mode.
		// See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124
		"--webport=0",
	)
	fmt.Println(strings.Join(args, " "))
	wstest := exec.CommandContext(ctx, "docker", args...)
	// TODO: log to *testing.T
	wstest.Stdout = os.Stdout
	wstest.Stderr = os.Stderr
Anmol Sethi's avatar
Anmol Sethi committed
	err = wstest.Start()
	if err != nil {
		return "", nil, fmt.Errorf("failed to start wstest: %w", err)
Anmol Sethi's avatar
Anmol Sethi committed
	}

	// TODO: kill
	return url, func() error {
		err = wstest.Process.Kill()
		if err != nil {
			return fmt.Errorf("failed to kill wstest: %w", err)
		}
		return nil
Anmol Sethi's avatar
Anmol Sethi committed
	}, nil
}
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
func wstestCaseCount(ctx context.Context, url string) (cases int, err error) {
	defer errd.Wrap(&err, "failed to get case count")
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
	c, _, err := websocket.Dial(ctx, url+"/getCaseCount", nil)
	if err != nil {
		return 0, err
Anmol Sethi's avatar
Anmol Sethi committed
	}
Anmol Sethi's avatar
Anmol Sethi committed
	defer c.Close(websocket.StatusInternalError, "")
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
	_, r, err := c.Reader(ctx)
	if err != nil {
		return 0, err
	}
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return 0, err
	}
	cases, err = strconv.Atoi(string(b))
Anmol Sethi's avatar
Anmol Sethi committed
	if err != nil {
Anmol Sethi's avatar
Anmol Sethi committed
		return 0, err
Anmol Sethi's avatar
Anmol Sethi committed
	}
Anmol Sethi's avatar
Anmol Sethi committed

Anmol Sethi's avatar
Anmol Sethi committed
	c.Close(websocket.StatusNormalClosure, "")

Anmol Sethi's avatar
Anmol Sethi committed
	return cases, nil
Anmol Sethi's avatar
Anmol Sethi committed
}

func checkWSTestIndex(t *testing.T, path string) {
	wstestOut, err := ioutil.ReadFile(path)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed

	var indexJSON map[string]map[string]struct {
		Behavior      string `json:"behavior"`
		BehaviorClose string `json:"behaviorClose"`
	}
	err = json.Unmarshal(wstestOut, &indexJSON)
Anmol Sethi's avatar
Anmol Sethi committed
	assert.Success(t, err)
Anmol Sethi's avatar
Anmol Sethi committed

	for _, tests := range indexJSON {
		for test, result := range tests {
Anmol Sethi's avatar
Anmol Sethi committed
			t.Run(test, func(t *testing.T) {
				switch result.BehaviorClose {
				case "OK", "INFORMATIONAL":
				default:
					t.Errorf("bad close behaviour")
				}

				switch result.Behavior {
				case "OK", "NON-STRICT", "INFORMATIONAL":
				default:
					t.Errorf("failed")
				}
			})
Anmol Sethi's avatar
Anmol Sethi committed
		}
	}

Anmol Sethi's avatar
Anmol Sethi committed
	if t.Failed() {
		htmlPath := strings.Replace(path, ".json", ".html", 1)
		t.Errorf("detected autobahn violation, see %q", htmlPath)
Anmol Sethi's avatar
Anmol Sethi committed
	}
}
Anmol Sethi's avatar
Anmol Sethi committed

func unusedListenAddr() (_ string, err error) {
	defer errd.Wrap(&err, "failed to get unused listen address")
	l, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		return "", err
	}
	l.Close()
	return l.Addr().String(), nil
}

func tempJSONFile(v interface{}) (string, error) {
	f, err := ioutil.TempFile("", "temp.json")
	if err != nil {
		return "", fmt.Errorf("temp file: %w", err)
Anmol Sethi's avatar
Anmol Sethi committed
	}
	defer f.Close()

	e := json.NewEncoder(f)
	e.SetIndent("", "\t")
	err = e.Encode(v)
	if err != nil {
		return "", fmt.Errorf("json encode: %w", err)
Anmol Sethi's avatar
Anmol Sethi committed
	}

	err = f.Close()
	if err != nil {
		return "", fmt.Errorf("close temp file: %w", err)
Anmol Sethi's avatar
Anmol Sethi committed
	}

	return f.Name(), nil
}