From 10cfede6726bb478c3630697af7fb9a258bd0755 Mon Sep 17 00:00:00 2001 From: Sly_tom_cat Date: Wed, 1 May 2024 21:39:47 +0300 Subject: [PATCH] Correct tests and add them to CI --- .github/workflows/go.yml | 30 ++++++++- build.sh | 6 -- connection.go | 4 +- connection_test.go | 61 +++++++++-------- echo-server/echo-server_test.go | 113 +++++++++++++++++--------------- main_test.go | 21 +----- mockServer.go | 18 +++-- mockServer_test.go | 59 +++++++++++++++++ server/server.go | 3 +- server/server_test.go | 6 +- 10 files changed, 205 insertions(+), 116 deletions(-) create mode 100644 mockServer_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 740289e..83b6d66 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,8 +23,36 @@ jobs: name: build_artifacts path: | ws + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: './go.mod' + - name: Test ws + run: go test -v -coverprofile cover.out . + - name: Format ws coverage + run: go tool cover -html=cover.out -o coverage.html + - name: Test server + run: go test -v -coverprofile cover_server.out ./server + - name: Format ws coverage + run: go tool cover -html=cover_server.out -o coverage_server.html + - name: Test ws + run: go test -v -coverprofile cover_echo-server.out ./echo-server + - name: Format ws coverage + run: go tool cover -html=cover_echo-server.out -o coverage_echo-server.html + - name: Upload coverage to Artifacts + uses: actions/upload-artifact@v4 + with: + name: coverage_artifacts + path: | + coverage.html + coverage_server.html + coverage_echo-server.html push: - needs: build + needs: [build, test] if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest steps: diff --git a/build.sh b/build.sh index 01852d5..a168f17 100755 --- a/build.sh +++ b/build.sh @@ -1,9 +1,3 @@ #!/bin/bash CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags="-s -w -X main.version=$(git branch --show-current)-$(git rev-parse --short HEAD)" . upx -qqq --best ws - -cd echo-server/ -CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags="-s -w -X main.version=$(git branch --show-current)-$(git rev-parse --short HEAD)" . -upx -qqq --best echo-server - - diff --git a/connection.go b/connection.go index b4162c1..ca0532e 100644 --- a/connection.go +++ b/connection.go @@ -108,7 +108,7 @@ func connect(url string, rl *readline.Instance) []error { return nil }) ws.SetPongHandler(func(appData string) error { - fmt.Fprint(rl.Stdout(), ctSprintf("%s < pong\n", getPrefix())) + fmt.Fprint(rl.Stdout(), ctSprintf("%s < pong: %s\n", getPrefix(), appData)) return nil }) } @@ -149,7 +149,7 @@ func (s *Session) pingHandler() { return } if options.pingPong { - fmt.Fprint(s.rl.Stdout(), ctSprintf("%s > ping\n", getPrefix())) + fmt.Fprint(s.rl.Stdout(), ctSprintf("%s > ping: \n", getPrefix())) } } } diff --git a/connection_test.go b/connection_test.go index e4462f8..d8b151e 100644 --- a/connection_test.go +++ b/connection_test.go @@ -44,15 +44,18 @@ func TestSession(t *testing.T) { errLock: sync.Mutex{}, } sent := "test message" + typed := "typed" + binary := "binary" + unknown := "unknown" + toBeFiltered := "must be filtered" // test sendMsg options.timestamp = true defer func() { options.timestamp = false }() err = s.sendMsg(sent) require.NoError(t, err) - require.Eventually(t, func() bool { return len(srv.Received) > 0 }, 30*time.Millisecond, 3*time.Millisecond) + require.Eventually(t, func() bool { return len(srv.Received) > 0 }, 100*time.Millisecond, 3*time.Millisecond) require.Equal(t, sent, <-srv.Received) // test typing - typed := "typed" _, err = rl.WriteStdin([]byte(typed + "\n")) require.NoError(t, err) go func() { @@ -74,10 +77,9 @@ func TestSession(t *testing.T) { // binary as text options.binAsText = true defer func() { options.binAsText = false }() - srv.ToSend <- "binary" + srv.ToSend <- binary require.Eventually(t, func() bool { return len(srv.ToSend) == 0 }, 20*time.Millisecond, 2*time.Millisecond) // filtered - toBeFiltered := "must be filtered" options.filter = regexp.MustCompile("^.*not filtered.*$") defer func() { options.filter = nil }() require.False(t, options.filter.MatchString(toBeFiltered)) @@ -85,7 +87,7 @@ func TestSession(t *testing.T) { require.Eventually(t, func() bool { return len(srv.ToSend) == 0 }, 20*time.Millisecond, 2*time.Millisecond) // unknown mode atomic.StoreInt64(&srv.Mode, 0) - srv.ToSend <- "unknown" + srv.ToSend <- unknown require.Eventually(t, func() bool { return len(srv.ToSend) == 0 }, 20*time.Millisecond, 2*time.Millisecond) time.Sleep(20 * time.Millisecond) cancel() @@ -93,47 +95,52 @@ func TestSession(t *testing.T) { output, err := io.ReadAll(outR) out := string(output) require.NoError(t, err) - require.Contains(t, out, " > test message") - require.Contains(t, out, " > typed") - require.Contains(t, out, " < test message") - require.Contains(t, out, " < \n00000000 74 65 73 74 20 6d 65 73 73 61 67 65 |test message|") - require.Contains(t, out, " < binary") + require.Contains(t, out, " > "+sent) + require.Contains(t, out, " > "+typed) + require.Contains(t, out, " < "+sent) + require.Contains(t, out, " < \n00000000 74 65 73 74 20 6d 65 73 73 61 67 65 |"+sent+"|") + require.Contains(t, out, " < "+binary) require.NotContains(t, out, toBeFiltered) - require.NotContains(t, out, "unknown") + require.NotContains(t, out, unknown) // t.Log(out) } func TestPingPong(t *testing.T) { - srv := newMockServer(2 * time.Millisecond) - defer srv.Close() + srv := newMockServer(5 * time.Millisecond) options.pingPong = true - options.pingInterval = 2 * time.Millisecond + options.pingInterval = 5 * time.Millisecond outR, outW, _ := os.Pipe() - errs := make(chan []error, 1) - rl, err := readline.NewEx(&readline.Config{Prompt: "> ", Stdout: outW, UniqueEditLine: true}) - require.NoError(t, err) - // the only way I found to keep redline working for a while - go func() { - for i := 0; i < 400; i++ { - _, err = rl.WriteStdin([]byte("typed")) - } + inR, inW, _ := os.Pipe() + defer func() { + inW.Close() + outW.Close() + options.pingPong = false + options.pingInterval = 0 + srv.Close() }() - rl.Write([]byte("typed")) + errs := make(chan []error, 1) + // substitute FuncMakeRaw and FuncExitRaw to empty func to use open pipe as Stdin + // switching to raw file descriptor 0 will cause immediately closure of rl due to EOF + success := func() error { return nil } + rl, err := readline.NewEx(&readline.Config{Prompt: "> ", Stdin: inR, Stdout: outW, FuncMakeRaw: success, FuncExitRaw: success}) require.NoError(t, err) go func() { errs <- connect(mockURL, rl) }() - time.Sleep(200 * time.Millisecond) + time.Sleep(20 * time.Millisecond) + require.Eventually(t, func() bool { return session != nil }, 100*time.Millisecond, 2*time.Millisecond) session.cancel() - require.Eventually(t, func() bool { return len(errs) > 0 }, 20*time.Millisecond, 2*time.Millisecond) + inW.Close() outW.Close() + require.Eventually(t, func() bool { return len(errs) > 0 }, 20*time.Millisecond, 2*time.Millisecond) output, err := io.ReadAll(outR) out := string(output) + // t.Log(out) require.NoError(t, err) require.Contains(t, out, "> ping") require.Contains(t, out, "< pong") - require.Contains(t, out, "< ping:") - require.Contains(t, out, "> pong:") + require.Contains(t, out, "< ping") + require.Contains(t, out, "> pong") } func TestInitMsg(t *testing.T) { diff --git a/echo-server/echo-server_test.go b/echo-server/echo-server_test.go index f3c93a0..02fbece 100644 --- a/echo-server/echo-server_test.go +++ b/echo-server/echo-server_test.go @@ -18,58 +18,13 @@ import ( func TestEchoServer(t *testing.T) { envName := fmt.Sprintf("BE_%s", t.Name()) if os.Getenv(envName) == "1" { - go DoMain([]string{""}) - time.Sleep(30 * time.Millisecond) - dialer := websocket.Dialer{} - conn, _, err := dialer.Dial(defaultUrl, nil) - require.NoError(t, err) - testCases := []struct { - name string - toSend string - toReceive []string - }{ - { - name: "echo success", - toSend: `{"type":"echo", "payload":"Hello world!"}`, - toReceive: []string{ - `{"type":"echo","payload":"Hello world!"}`, - }, - }, - { - name: "broadcast success", - toSend: `{"type":"broadcast", "payload":"Hello world!"}`, - toReceive: []string{ - `{"type":"broadcast","payload":"Hello world!"}`, - `{"type":"broadcastResult","payload":"Hello world!","listenerCount":1}`, - }, - }, - { - name: "wrong message type", - toSend: `{"type":"wrong", "payload":"Hello world!"}`, - toReceive: []string{ - `{"type":"error","payload":"unknown type"}`, - }, - }, - { - name: "incorrect json", - toSend: `}`, - toReceive: []string{ - `{"type":"error","payload":"message parsing error: invalid character '}' looking for beginning of value"}`, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := conn.WriteMessage(websocket.TextMessage, []byte(tc.toSend)) - assert.NoError(t, err) - for _, r := range tc.toReceive { - mType, received, err := conn.ReadMessage() - assert.NoError(t, err) - assert.Equal(t, websocket.TextMessage, mType) - assert.Equal(t, string(r), string(received)) - } - }) - } + // conn, _, err := dialer.Dial(defaultUrl, nil) + // if err == nil { + // fmt.Println("unexpected connection") + // os.Exit(2) + // } + go DoMain([]string{"", defaultUrl}) + time.Sleep(500 * time.Millisecond) // server.TryCloseNormally(conn, "tests finish") //require.NoError(t, srv.Close()) syscall.Kill(syscall.Getpid(), syscall.SIGINT) @@ -87,6 +42,60 @@ func TestEchoServer(t *testing.T) { require.NoError(t, err) cmd.Env = append(os.Environ(), envName+"=1") require.NoError(t, cmd.Start()) + time.Sleep(50 * time.Millisecond) + dialer := websocket.Dialer{} + conn, _, err := dialer.Dial(defaultUrl, nil) + if err != nil { + panic(err) + } + testCases := []struct { + name string + toSend string + toReceive []string + }{ + { + name: "echo success", + toSend: `{"type":"echo", "payload":"Hello world!"}`, + toReceive: []string{ + `{"type":"echo","payload":"Hello world!"}`, + }, + }, + { + name: "broadcast success", + toSend: `{"type":"broadcast", "payload":"Hello world!"}`, + toReceive: []string{ + `{"type":"broadcast","payload":"Hello world!"}`, + `{"type":"broadcastResult","payload":"Hello world!","listenerCount":1}`, + }, + }, + { + name: "wrong message type", + toSend: `{"type":"wrong", "payload":"Hello world!"}`, + toReceive: []string{ + `{"type":"error","payload":"unknown type"}`, + }, + }, + { + name: "incorrect json", + toSend: `}`, + toReceive: []string{ + `{"type":"error","payload":"message parsing error: invalid character '}' looking for beginning of value"}`, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := conn.WriteMessage(websocket.TextMessage, []byte(tc.toSend)) + assert.NoError(t, err) + for _, r := range tc.toReceive { + mType, received, err := conn.ReadMessage() + assert.NoError(t, err) + assert.Equal(t, websocket.TextMessage, mType) + assert.Equal(t, string(r), string(received)) + } + }) + } + out, _ := io.ReadAll(r) output := string(out) err = cmd.Wait() diff --git a/main_test.go b/main_test.go index 697942d..99152e5 100644 --- a/main_test.go +++ b/main_test.go @@ -9,30 +9,11 @@ import ( "testing" "time" - "github.com/gorilla/websocket" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestMockServer(t *testing.T) { - s := newMockServer(0) - defer s.Close() - conn := newMockConn() - defer TryCloseNormally(conn, "test finished") - sent := "test" - // test client -> server message - require.NoError(t, conn.WriteMessage(websocket.TextMessage, []byte(sent))) - require.Eventually(t, func() bool { return len(s.Received) > 0 }, 20*time.Millisecond, 2*time.Millisecond) - require.Equal(t, sent, <-s.Received) - // test server -> client message - s.ToSend <- sent - require.NoError(t, conn.SetReadDeadline(time.Now().Add(20*time.Millisecond))) - _, data, err := conn.ReadMessage() - require.NoError(t, err) - require.Equal(t, sent, string(data)) -} - func TestWSinitMsg(t *testing.T) { s := newMockServer(0) defer s.Close() @@ -54,6 +35,8 @@ func TestWSconnectFail(t *testing.T) { envName := fmt.Sprintf("BE_%s", t.Name()) if os.Getenv(envName) == "1" { root(&cobra.Command{}, []string{"wss://127.0.0.1:8080"}) + time.Sleep(300 * time.Millisecond) + session.cancel() return } args := []string{"-test.run=" + t.Name()} diff --git a/mockServer.go b/mockServer.go index 948603e..30f9708 100644 --- a/mockServer.go +++ b/mockServer.go @@ -29,6 +29,7 @@ func newMockServer(interval time.Duration) *mockServer { Close: func() error { cancel() s.Shutdown(ctx) + s.Close() return nil }, Received: received, @@ -36,15 +37,15 @@ func newMockServer(interval time.Duration) *mockServer { Mode: websocket.TextMessage, } s.WSHandleFunc(u.Path, func(conn *websocket.Conn) { + defer conn.Close() if interval != 0 { go func() { ticker := time.NewTicker(interval) + defer ticker.Stop() for { select { case <-ticker.C: - if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(20*time.Millisecond)); err != nil { - return - } + conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(20*time.Millisecond)) case <-ctx.Done(): return } @@ -70,8 +71,15 @@ func newMockServer(interval time.Duration) *mockServer { } } }) - go s.ListenAndServe() - time.Sleep(50 * time.Millisecond) + errCh := make(chan error, 1) + go func() { + errCh <- s.ListenAndServe() + }() + select { + case err := <-errCh: + panic(err) + case <-time.After(50 * time.Millisecond): + } return m } diff --git a/mockServer_test.go b/mockServer_test.go new file mode 100644 index 0000000..a69ba28 --- /dev/null +++ b/mockServer_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/require" +) + +func TestMockServer(t *testing.T) { + s := newMockServer(0) + defer s.Close() + conn := newMockConn() + defer TryCloseNormally(conn, "test finished") + sent := "test" + // test client -> server message + require.NoError(t, conn.WriteMessage(websocket.TextMessage, []byte(sent))) + require.Eventually(t, func() bool { return len(s.Received) > 0 }, 20*time.Millisecond, 2*time.Millisecond) + require.Equal(t, sent, <-s.Received) + // Repeat + require.NoError(t, conn.WriteMessage(websocket.TextMessage, []byte(sent))) + require.Eventually(t, func() bool { return len(s.Received) > 0 }, 20*time.Millisecond, 2*time.Millisecond) + require.Equal(t, sent, <-s.Received) + // test server -> client message + s.ToSend <- sent + require.NoError(t, conn.SetReadDeadline(time.Now().Add(20*time.Millisecond))) + _, data, err := conn.ReadMessage() + require.NoError(t, err) + require.Equal(t, sent, string(data)) +} + +func TestMockServerPing(t *testing.T) { + s := newMockServer(5 * time.Millisecond) + defer s.Close() + conn := newMockConn() + defer TryCloseNormally(conn, "test finished") + var pingCount int64 + conn.SetPingHandler(func(appData string) error { + atomic.AddInt64(&pingCount, 1) + return conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Millisecond)) + }) + conn.SetReadDeadline(time.Now().Add(30 * time.Millisecond)) + conn.ReadMessage() + require.Greater(t, atomic.LoadInt64(&pingCount), int64(3)) + s.Close() + time.Sleep(20 * time.Millisecond) +} + +func TestMockServerDoubleStart(t *testing.T) { + s := newMockServer(0) + defer s.Close() + require.Panics(t, func() { newMockServer(0) }) +} + +func TestMockConnPanic(t *testing.T) { + require.Panics(t, func() { newMockConn() }) +} diff --git a/server/server.go b/server/server.go index 3b15a5b..dd6564b 100644 --- a/server/server.go +++ b/server/server.go @@ -62,7 +62,8 @@ func (s *Server) Close() error { }) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - return s.Server.Shutdown(ctx) + s.Server.Shutdown(ctx) + return s.Server.Close() } // TryCloseNormally tries to close websocket connection normally i.e. according to RFC diff --git a/server/server_test.go b/server/server_test.go index a826ae7..5113807 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -53,7 +53,7 @@ func TestHandshakeServerError(t *testing.T) { s := NewServer(":8080") require.NotNil(t, s) s.Upgrader.CheckOrigin = func(r *http.Request) bool { return false } - s.WSHandleFunc("/", EchoHandler) + defer s.WSHandleFunc("/", EchoHandler) go func() { s.ListenAndServe() }() time.Sleep(50 * time.Millisecond) defer s.Close() @@ -84,12 +84,12 @@ func TestHandshakeClientError(t *testing.T) { func TestRegularHandler(t *testing.T) { s := NewServer("localhost:8080") require.NotNil(t, s) + defer s.Close() s.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) go func() { s.ListenAndServe() }() time.Sleep(50 * time.Millisecond) - defer s.Close() resp, err := http.DefaultClient.Get("http://localhost:8080/ok") require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) @@ -100,10 +100,10 @@ func TestForEachConnection(t *testing.T) { fullURL := "ws://localhost:8080" s := NewServer("localhost:8080") require.NotNil(t, s) + defer s.Close() s.WSHandleFunc("/", EchoHandler) go func() { s.ListenAndServe() }() time.Sleep(50 * time.Millisecond) - defer s.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() sentMsg := []byte("ping")