mirror of
https://github.com/livekit/livekit.git
synced 2026-09-29 13:19:15 +00:00
* test: stop registering the global DefaultConfig with the logger InitLoggerFromConfig hands the pointer to zaputil.ComponentLeveler as its level resolver, and protocol a879e94 gave logger.Config a ResolveComponentLevel that takes c.lock. The two test packages passed &config.DefaultConfig.Logging, so every first-time component level resolution locked a mutex inside the global DefaultConfig while config.NewConfig marshalled that same global from another goroutine: Read at 0x39baa08 yaml.Marshal -> pkg/config/config.go:633 (NewConfig) Write at 0x39baa08 ComponentLeveler.resolve -> zaputil/leveler.go:117 That tripped the race detector in pkg/rtc TestPreferMediaCodecForPublisher and broke CI on master. Register a test-local LoggingConfig instead, so the logger never touches the global that NewConfig reads. DefaultConfig.Logging only ever sets PionLevel, so behavior is unchanged. A plain struct copy is not an option here: logger.Config holds a mutex and vet's copylocks would reject it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: make RTCClient callbacks safe to set while the client runs The scenario tests assign c2.OnDataReceived after waitUntilConnected, so the assignment raced the data channel goroutine already reading the field in handleDataMessage: Read test/client.(*RTCClient).handleDataMessage client.go:1146 Write test.scenarioDataPublish.func1 scenarios.go:160 This is what failed TestMultinodeDataPublishing on master. Replace the three exported callback fields with atomic pointers behind SetOnConnected/SetOnDataReceived/SetOnDataUnlabeledReceived. OnConnected and OnDataUnlabeledReceived have no writers today, but they are read from the same background goroutines and would race the moment one appeared, so all three move together rather than leaving a split API on one struct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
463 lines
13 KiB
Go
463 lines
13 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pion/transport/v4/vnet"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/twitchtv/twirp"
|
|
|
|
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
|
|
"github.com/livekit/protocol/auth"
|
|
"github.com/livekit/protocol/livekit"
|
|
"github.com/livekit/protocol/logger"
|
|
"github.com/livekit/protocol/utils/guid"
|
|
|
|
"github.com/livekit/livekit-server/pkg/config"
|
|
"github.com/livekit/livekit-server/pkg/routing"
|
|
"github.com/livekit/livekit-server/pkg/rtc"
|
|
"github.com/livekit/livekit-server/pkg/service"
|
|
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
|
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
|
"github.com/livekit/livekit-server/pkg/testutils"
|
|
"github.com/livekit/livekit-server/pkg/testutils/vnettest"
|
|
testclient "github.com/livekit/livekit-server/test/client"
|
|
)
|
|
|
|
const (
|
|
testApiKey = "apikey"
|
|
testApiSecret = "apiSecretExtendTo32BytesAsThatIsMinimum"
|
|
testRoom = "mytestroom"
|
|
defaultServerPort = 7880
|
|
secondServerPort = 8880
|
|
nodeID1 = "node-1"
|
|
nodeID2 = "node-2"
|
|
|
|
syncDelay = 100 * time.Millisecond
|
|
// if there are deadlocks, it's helpful to set a short test timeout (i.e. go test -timeout=30s)
|
|
// let connection timeout happen
|
|
// connectTimeout = 5000 * time.Second
|
|
)
|
|
|
|
var roomClient livekit.RoomService
|
|
|
|
func init() {
|
|
// The logger retains this config and locks it on every component level
|
|
// resolution, so it must not be DefaultConfig: NewConfig marshals that
|
|
// global from the goroutines these tests spin up.
|
|
loggingConf := config.LoggingConfig{PionLevel: config.DefaultConfig.Logging.PionLevel}
|
|
config.InitLoggerFromConfig(&loggingConf)
|
|
|
|
prometheus.Init("test", livekit.NodeType_SERVER)
|
|
}
|
|
|
|
func setupSingleNodeTest(name string) (*service.LivekitServer, func()) {
|
|
return setupSingleNodeTestWithConfig(name, nil)
|
|
}
|
|
|
|
func setupSingleNodeTestWithConfig(name string, configUpdater func(*config.Config)) (*service.LivekitServer, func()) {
|
|
logger.Infow("----------------STARTING TEST----------------", "test", name)
|
|
s := createSingleNodeServer(configUpdater)
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
|
|
waitForServerToStart(s)
|
|
|
|
return s, func() {
|
|
s.Stop(true)
|
|
logger.Infow("----------------FINISHING TEST----------------", "test", name)
|
|
}
|
|
}
|
|
|
|
func setupMultiNodeTest(name string) (*service.LivekitServer, *service.LivekitServer, func()) {
|
|
return setupMultiNodeTestWithConfig(name, nil)
|
|
}
|
|
|
|
func setupMultiNodeTestWithConfig(name string, configUpdater func(*config.Config)) (*service.LivekitServer, *service.LivekitServer, func()) {
|
|
logger.Infow("----------------STARTING TEST----------------", "test", name)
|
|
s1 := createMultiNodeServer(guid.New(nodeID1), defaultServerPort, configUpdater)
|
|
s2 := createMultiNodeServer(guid.New(nodeID2), secondServerPort, configUpdater)
|
|
go s1.Start()
|
|
go s2.Start()
|
|
|
|
waitForServerToStart(s1)
|
|
waitForServerToStart(s2)
|
|
|
|
return s1, s2, func() {
|
|
s1.Stop(true)
|
|
s2.Stop(true)
|
|
redisClient().FlushAll(context.Background())
|
|
logger.Infow("----------------FINISHING TEST----------------", "test", name)
|
|
}
|
|
}
|
|
|
|
func contextWithToken(token string) context.Context {
|
|
header := make(http.Header)
|
|
testclient.SetAuthorizationToken(header, token)
|
|
tctx, err := twirp.WithHTTPRequestHeaders(context.Background(), header)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tctx
|
|
}
|
|
|
|
func waitForServerToStart(s *service.LivekitServer) {
|
|
// wait till ready
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutils.ConnectTimeout)
|
|
defer cancel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
panic("could not start server after timeout")
|
|
case <-time.After(10 * time.Millisecond):
|
|
if s.IsRunning() {
|
|
// ensure we can connect to it
|
|
res, err := http.Get(fmt.Sprintf("http://localhost:%d", s.HTTPPort()))
|
|
if err == nil && res.StatusCode == http.StatusOK {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func waitUntilConnected(t *testing.T, clients ...*testclient.RTCClient) {
|
|
logger.Infow("waiting for clients to become connected")
|
|
wg := sync.WaitGroup{}
|
|
for i := range clients {
|
|
c := clients[i]
|
|
wg.Go(func() {
|
|
err := c.WaitUntilConnected(5 * time.Second)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
func ensureNotConnected(t *testing.T, clients ...*testclient.RTCClient) {
|
|
logger.Infow("checking if clients connect")
|
|
wg := sync.WaitGroup{}
|
|
for i := range clients {
|
|
c := clients[i]
|
|
wg.Go(func() {
|
|
err := c.WaitUntilConnected(5 * time.Second)
|
|
if err == nil {
|
|
t.Error(fmt.Errorf("expected client to not connect: %s", c.ID()))
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
func createSingleNodeServer(configUpdater func(*config.Config)) *service.LivekitServer {
|
|
var err error
|
|
conf, err := config.NewConfig("", true, nil, nil)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not create config: %v", err))
|
|
}
|
|
conf.Keys = map[string]string{testApiKey: testApiSecret}
|
|
conf.EnableDataTracks = true
|
|
if configUpdater != nil {
|
|
configUpdater(conf)
|
|
}
|
|
|
|
currentNode, err := routing.NewLocalNode(conf)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not create local node: %v", err))
|
|
}
|
|
currentNode.SetNodeID(livekit.NodeID(guid.New(nodeID1)))
|
|
|
|
s, err := service.InitializeServer(conf, currentNode)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not create server: %v", err))
|
|
}
|
|
|
|
roomClient = livekit.NewRoomServiceJSONClient(fmt.Sprintf("http://localhost:%d", defaultServerPort), &http.Client{})
|
|
return s
|
|
}
|
|
|
|
func createMultiNodeServer(nodeID string, port uint32, configUpdater func(*config.Config)) *service.LivekitServer {
|
|
var err error
|
|
conf, err := config.NewConfig("", true, nil, nil)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not create config: %v", err))
|
|
}
|
|
conf.Port = port
|
|
conf.RTC.UDPPort = rtcconfig.PortRange{Start: int(port) + 1}
|
|
conf.RTC.TCPPort = port + 2
|
|
conf.Redis.Address = "localhost:6379"
|
|
conf.Keys = map[string]string{testApiKey: testApiSecret}
|
|
conf.EnableDataTracks = true
|
|
if configUpdater != nil {
|
|
configUpdater(conf)
|
|
}
|
|
|
|
currentNode, err := routing.NewLocalNode(conf)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
currentNode.SetNodeID(livekit.NodeID(nodeID))
|
|
|
|
// redis routing and store
|
|
s, err := service.InitializeServer(conf, currentNode)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not create server: %v", err))
|
|
}
|
|
|
|
roomClient = livekit.NewRoomServiceJSONClient(fmt.Sprintf("http://localhost:%d", port), &http.Client{})
|
|
return s
|
|
}
|
|
|
|
type testRTCServicePath int
|
|
|
|
const (
|
|
testRTCServicePathv0 testRTCServicePath = iota
|
|
testRTCServicePathv0SinglePeerConnection
|
|
testRTCServicePathv1
|
|
)
|
|
|
|
func (t testRTCServicePath) String() string {
|
|
switch t {
|
|
case testRTCServicePathv0:
|
|
return "v0"
|
|
case testRTCServicePathv0SinglePeerConnection:
|
|
return "v0-single-peer-connection"
|
|
case testRTCServicePathv1:
|
|
return "v1"
|
|
default:
|
|
return fmt.Sprintf("unknown: %d", t)
|
|
}
|
|
}
|
|
|
|
var testRTCServicePaths = []testRTCServicePath{
|
|
testRTCServicePathv0,
|
|
testRTCServicePathv0SinglePeerConnection,
|
|
testRTCServicePathv1,
|
|
}
|
|
|
|
func testRTCServicePathToTestClientOptions(testRTCServicePath testRTCServicePath, opts *testclient.Options) {
|
|
if opts == nil {
|
|
return
|
|
}
|
|
|
|
switch testRTCServicePath {
|
|
case testRTCServicePathv0:
|
|
opts.RTCServicePath = "/rtc"
|
|
case testRTCServicePathv0SinglePeerConnection:
|
|
opts.RTCServicePath = "/rtc"
|
|
opts.UseJoinRequestQueryParam = true
|
|
case testRTCServicePathv1:
|
|
opts.RTCServicePath = "/rtc/v1"
|
|
opts.UseJoinRequestQueryParam = true
|
|
default:
|
|
opts.RTCServicePath = "/rtc"
|
|
}
|
|
}
|
|
|
|
// creates a client and runs against server
|
|
func createRTCClient(name string, port int, testRTCServicePath testRTCServicePath, opts *testclient.Options) *testclient.RTCClient {
|
|
var customizer func(token *auth.AccessToken, grants *auth.VideoGrant)
|
|
if opts != nil {
|
|
customizer = opts.TokenCustomizer
|
|
}
|
|
token := joinToken(testRoom, name, customizer)
|
|
|
|
return createRTCClientWithToken(token, port, testRTCServicePath, opts)
|
|
}
|
|
|
|
// creates a client and runs against server
|
|
func createRTCClientWithToken(token string, port int, testRTCServicePath testRTCServicePath, opts *testclient.Options) *testclient.RTCClient {
|
|
if opts == nil {
|
|
opts = &testclient.Options{
|
|
AutoSubscribe: true,
|
|
}
|
|
}
|
|
testRTCServicePathToTestClientOptions(testRTCServicePath, opts)
|
|
ws, err := testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", port), token, opts)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
c, err := testclient.NewRTCClient(ws, opts.UseJoinRequestQueryParam, opts)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
go c.Run()
|
|
|
|
return c
|
|
}
|
|
|
|
func redisClient() *redis.Client {
|
|
return redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
})
|
|
}
|
|
|
|
func joinToken(room, name string, customFn func(token *auth.AccessToken, grants *auth.VideoGrant)) string {
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
SetIdentity(name).
|
|
SetName(name).
|
|
SetMetadata("metadata" + name)
|
|
grant := &auth.VideoGrant{RoomJoin: true, Room: room}
|
|
if customFn != nil {
|
|
customFn(at, grant)
|
|
}
|
|
at.AddGrant(grant)
|
|
t, err := at.ToJWT()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func joinTokenWithGrant(name string, grant *auth.VideoGrant) string {
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
AddGrant(grant).
|
|
SetIdentity(name).
|
|
SetName(name)
|
|
t, err := at.ToJWT()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func createRoomToken() string {
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
AddGrant(&auth.VideoGrant{RoomCreate: true})
|
|
t, err := at.ToJWT()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func adminRoomToken(name string) string {
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
AddGrant(&auth.VideoGrant{RoomAdmin: true, Room: name})
|
|
t, err := at.ToJWT()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func listRoomToken() string {
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
AddGrant(&auth.VideoGrant{RoomList: true})
|
|
t, err := at.ToJWT()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func stopWriters(writers ...testclient.TrackWriter) {
|
|
for _, w := range writers {
|
|
w.Stop()
|
|
}
|
|
}
|
|
|
|
func stopClients(clients ...*testclient.RTCClient) {
|
|
for _, c := range clients {
|
|
c.Stop()
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// vnet media harness
|
|
//
|
|
// Setup specific to driving a real server transport over a virtual network. The
|
|
// pion side lives in pkg/testutils/vnettest, shared with the pkg/sfu media tests.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// newVNetWebRTCConfig builds the server side WebRTCConfig on net. The direction
|
|
// configs come from the production NewWebRTCConfig so the negotiated extensions and
|
|
// feedback stay in step with it; the setting engine is replaced so no real socket or
|
|
// ICE mux is bound.
|
|
func newVNetWebRTCConfig(t *testing.T, net *vnet.Net, bufferFactory *buffer.Factory) *rtc.WebRTCConfig {
|
|
t.Helper()
|
|
|
|
conf, err := config.NewConfig("", true, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// an ephemeral port range instead of the dev mode single port, which would bind a mux
|
|
conf.RTC.TCPPort = 0
|
|
conf.RTC.UDPPort = rtcconfig.PortRange{}
|
|
conf.RTC.ICEPortRangeStart = 50000
|
|
conf.RTC.ICEPortRangeEnd = 60000
|
|
|
|
rtcConf, err := rtc.NewWebRTCConfig(conf)
|
|
require.NoError(t, err)
|
|
require.Nil(t, rtcConf.UDPMux, "test config must not bind a udp mux")
|
|
|
|
rtcConf.SettingEngine = vnettest.NewSettingEngine(net)
|
|
rtcConf.SetBufferFactory(bufferFactory)
|
|
|
|
return rtcConf
|
|
}
|
|
|
|
// stripDeclaredSSRCs removes the a=ssrc lines pion puts in its offer. Browsers doing
|
|
// rid based simulcast do not declare per-layer SSRCs, which is why a repair SSRC has to
|
|
// be learned at all; leaving them in would let the receiver resolve everything from SDP.
|
|
func stripDeclaredSSRCs(offer string) string {
|
|
lines := strings.Split(offer, "\r\n")
|
|
filtered := lines[:0]
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "a=ssrc") {
|
|
continue
|
|
}
|
|
filtered = append(filtered, line)
|
|
}
|
|
return strings.Join(filtered, "\r\n")
|
|
}
|
|
|
|
// sendUntil calls send every 20ms until done reports true or the timeout expires,
|
|
// returning the final state of done.
|
|
func sendUntil(t *testing.T, timeout time.Duration, done func() bool, send func()) bool {
|
|
t.Helper()
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if done() {
|
|
return true
|
|
}
|
|
send()
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
return done()
|
|
}
|