bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
165 lines
3.9 KiB
Go
165 lines
3.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package bus
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/event/protocol"
|
|
)
|
|
|
|
func TestConn_SenderWritesEvents(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
bc := NewConn(server, nil, "im.msg", []string{"im.message.receive_v1"}, 12345, "")
|
|
go bc.SenderLoop()
|
|
|
|
bc.SendCh() <- &protocol.Event{
|
|
Type: protocol.MsgTypeEvent,
|
|
EventType: "im.message.receive_v1",
|
|
}
|
|
|
|
scanner := bufio.NewScanner(client)
|
|
client.SetReadDeadline(time.Now().Add(time.Second))
|
|
if !scanner.Scan() {
|
|
t.Fatalf("expected to read a line: %v", scanner.Err())
|
|
}
|
|
line := scanner.Bytes()
|
|
if !bytes.Contains(line, []byte(`"event"`)) {
|
|
t.Errorf("unexpected line: %s", line)
|
|
}
|
|
}
|
|
|
|
type serializingDetector struct {
|
|
net.Conn
|
|
inFlight atomic.Int32
|
|
violated atomic.Bool
|
|
}
|
|
|
|
func (s *serializingDetector) Write(b []byte) (int, error) {
|
|
if s.inFlight.Add(1) > 1 {
|
|
s.violated.Store(true)
|
|
}
|
|
time.Sleep(500 * time.Microsecond)
|
|
defer s.inFlight.Add(-1)
|
|
return s.Conn.Write(b)
|
|
}
|
|
|
|
// Two goroutines writing frames (event + ack) must not overlap on the underlying net.Conn.
|
|
func TestConn_ConcurrentWritesSerialised(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
det := &serializingDetector{Conn: server}
|
|
bc := NewConn(det, nil, "im.msg", []string{"im.msg"}, 12345, "")
|
|
|
|
go func() { _, _ = io.Copy(io.Discard, client) }()
|
|
|
|
go bc.SenderLoop()
|
|
|
|
var wg sync.WaitGroup
|
|
const workers = 8
|
|
const perWorker = 20
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < perWorker && time.Now().Before(deadline); j++ {
|
|
bc.SendCh() <- &protocol.Event{Type: protocol.MsgTypeEvent, EventType: "im.msg"}
|
|
}
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < perWorker && time.Now().Before(deadline); j++ {
|
|
bc.handleControlMessage(&protocol.PreShutdownCheck{EventKey: "im.msg"})
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
bc.Close()
|
|
|
|
if det.violated.Load() {
|
|
t.Error("concurrent Write on net.Conn detected: SenderLoop and handleControlMessage " +
|
|
"overlapped without serialisation (framing / deadline race)")
|
|
}
|
|
}
|
|
|
|
func TestConn_TrySend_NonEvicting(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
bc := NewConn(server, nil, "im.msg", []string{"im.msg"}, 12345, "")
|
|
|
|
for i := 0; i < sendChCap; i++ {
|
|
if !bc.TrySend(i) {
|
|
t.Fatalf("TrySend returned false at iteration %d; expected all sendChCap (%d) to fit", i, sendChCap)
|
|
}
|
|
}
|
|
if bc.TrySend("overflow") {
|
|
t.Fatal("TrySend on full channel returned true: TrySend must be non-evicting")
|
|
}
|
|
first := <-bc.SendCh()
|
|
if first != 0 {
|
|
t.Errorf("first drained item = %v, want 0", first)
|
|
}
|
|
}
|
|
|
|
func TestConn_ReaderDetectsEOF(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
|
|
bc := NewConn(server, nil, "im.msg", []string{"im.msg"}, 12345, "")
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
bc.ReaderLoop()
|
|
close(done)
|
|
}()
|
|
|
|
client.Close()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("ReaderLoop did not exit on EOF")
|
|
}
|
|
}
|
|
|
|
func TestConn_SubscriptionID(t *testing.T) {
|
|
c1, c2 := net.Pipe()
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
conn := NewConn(c1, nil, "mail.x", []string{"mail.x"}, 999, "mail.x:abc")
|
|
if got := conn.SubscriptionID(); got != "mail.x:abc" {
|
|
t.Errorf("SubscriptionID() = %q, want %q", got, "mail.x:abc")
|
|
}
|
|
}
|
|
|
|
func TestConn_SubscriptionID_EmptyFallsBackToEventKey(t *testing.T) {
|
|
c1, c2 := net.Pipe()
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
conn := NewConn(c1, nil, "mail.x", []string{"mail.x"}, 999, "")
|
|
if got := conn.SubscriptionID(); got != "mail.x" {
|
|
t.Errorf("SubscriptionID() with empty input = %q, want fallback %q", got, "mail.x")
|
|
}
|
|
}
|