Skip to content

Commit 7d4faf3

Browse files
FfiClient state guarding, additional testing coverage (#125)
1 parent 82ee340 commit 7d4faf3

25 files changed

Lines changed: 1221 additions & 113 deletions

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ jobs:
345345
--root . \
346346
--gcov-executable "${GCOV_EXECUTABLE}" \
347347
--gcov-ignore-parse-errors=all \
348+
--filter 'include/' \
348349
--filter 'src/' \
349350
--exclude 'src/tests/' \
350351
--exclude '.*\.pb\.' \

src/ffi_client.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ std::optional<FfiClient::AsyncId> ExtractAsyncId(const proto::FfiEvent& event) {
141141

142142
} // namespace
143143

144+
FfiClient& FfiClient::instance() noexcept {
145+
static FfiClient instance;
146+
return instance;
147+
}
148+
144149
// clang-tidy flags this as a trivial destructor in release mode
145150
// due to the assert being pre-processed out
146151
// NOLINTNEXTLINE(modernize-use-equals-default)
@@ -182,6 +187,13 @@ void FfiClient::RemoveListener(ListenerId id) {
182187
}
183188

184189
proto::FfiResponse FfiClient::sendRequest(const proto::FfiRequest& request) const {
190+
// The Rust FFI will lazily initialize the FFI client when the first request is sent,
191+
// but if not initialized none of the async operations will work. Guarding against that here.
192+
// Improvement ticket added to the Rust SDK to discuss this
193+
if (!isInitialized()) {
194+
throw std::runtime_error("FfiClient::sendRequest failed: LiveKit is not initialized");
195+
}
196+
185197
std::string bytes;
186198
if (!request.SerializeToString(&bytes) || bytes.empty()) {
187199
throw std::runtime_error("failed to serialize FfiRequest");

src/ffi_client.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ class LIVEKIT_INTERNAL_API FfiClient {
7474
FfiClient(FfiClient&&) = delete;
7575
FfiClient& operator=(FfiClient&&) = delete;
7676

77-
static FfiClient& instance() noexcept {
78-
static FfiClient instance;
79-
return instance;
80-
}
77+
// Access the singleton instance of the FfiClient
78+
// Note: lazily created, not thread safe
79+
static FfiClient& instance() noexcept;
8180

8281
// Must be called before any other FFI usage
8382
bool initialize(bool capture_logs);

src/livekit.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@
2222
namespace livekit {
2323

2424
bool initialize(const LogLevel& level, const LogSink& log_sink) {
25+
// Initializes logger if singleton instance is not already initialized
2526
setLogLevel(level);
2627
auto& ffi_client = FfiClient::instance();
2728
return ffi_client.initialize(log_sink == LogSink::kCallback);
2829
}
2930

31+
bool isInitialized() { return FfiClient::instance().isInitialized(); }
32+
3033
void shutdown() {
31-
auto& ffi_client = FfiClient::instance();
32-
ffi_client.shutdown();
34+
FfiClient::instance().shutdown();
3335
detail::shutdownLogger();
3436
}
3537

src/room.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "ffi_client.h"
2424
#include "livekit/audio_stream.h"
2525
#include "livekit/e2ee.h"
26+
#include "livekit/livekit.h"
2627
#include "livekit/local_data_track.h"
2728
#include "livekit/local_participant.h"
2829
#include "livekit/local_track_publication.h"
@@ -105,6 +106,11 @@ void Room::setDelegate(RoomDelegate* delegate) {
105106
bool Room::Connect(const std::string& url, const std::string& token, const RoomOptions& options) {
106107
TRACE_EVENT0("livekit", "Room::Connect");
107108

109+
if (!FfiClient::instance().isInitialized()) {
110+
LK_LOG_ERROR("Room::Connect failed: LiveKit is not initialized");
111+
return false;
112+
}
113+
108114
{
109115
const std::scoped_lock<std::mutex> g(lock_);
110116
if (connection_state_ != ConnectionState::Disconnected) {

src/tests/integration/test_room.cpp

Lines changed: 4 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,78 +19,8 @@
1919

2020
namespace livekit::test {
2121

22-
class RoomTest : public ::testing::Test {
23-
protected:
24-
void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); }
25-
26-
void TearDown() override { livekit::shutdown(); }
27-
};
28-
29-
TEST_F(RoomTest, CreateRoom) {
30-
Room room;
31-
// Room should be created without issues
32-
EXPECT_EQ(room.localParticipant(), nullptr) << "Local participant should be null before connect";
33-
}
34-
35-
TEST_F(RoomTest, RoomOptionsDefaults) {
36-
RoomOptions options;
37-
38-
EXPECT_TRUE(options.auto_subscribe) << "auto_subscribe should default to true";
39-
EXPECT_FALSE(options.dynacast) << "dynacast should default to false";
40-
EXPECT_FALSE(options.rtc_config.has_value()) << "rtc_config should not have a value by default";
41-
EXPECT_FALSE(options.encryption.has_value()) << "encryption should not have a value by default";
42-
}
43-
44-
TEST_F(RoomTest, RtcConfigDefaults) {
45-
RtcConfig config;
46-
47-
EXPECT_EQ(config.ice_transport_type, 0);
48-
EXPECT_EQ(config.continual_gathering_policy, 0);
49-
EXPECT_TRUE(config.ice_servers.empty());
50-
}
51-
52-
TEST_F(RoomTest, IceServerConfiguration) {
53-
IceServer server;
54-
server.url = "stun:stun.l.google.com:19302";
55-
server.username = "user";
56-
server.credential = "pass";
57-
58-
EXPECT_EQ(server.url, "stun:stun.l.google.com:19302");
59-
EXPECT_EQ(server.username, "user");
60-
EXPECT_EQ(server.credential, "pass");
61-
}
62-
63-
TEST_F(RoomTest, RoomWithCustomRtcConfig) {
64-
RoomOptions options;
65-
options.auto_subscribe = false;
66-
options.dynacast = true;
67-
68-
RtcConfig rtc_config;
69-
rtc_config.ice_servers.push_back({"stun:stun.l.google.com:19302", "", ""});
70-
rtc_config.ice_servers.push_back({"turn:turn.example.com:3478", "user", "pass"});
71-
72-
options.rtc_config = rtc_config;
73-
74-
EXPECT_FALSE(options.auto_subscribe);
75-
EXPECT_TRUE(options.dynacast);
76-
EXPECT_TRUE(options.rtc_config.has_value());
77-
EXPECT_EQ(options.rtc_config->ice_servers.size(), 2);
78-
}
79-
80-
TEST_F(RoomTest, RemoteParticipantsEmptyBeforeConnect) {
81-
Room room;
82-
auto participants = room.remoteParticipants();
83-
EXPECT_TRUE(participants.empty()) << "Remote participants should be empty before connect";
84-
}
85-
86-
TEST_F(RoomTest, RemoteParticipantLookupBeforeConnect) {
87-
Room room;
88-
auto participant = room.remoteParticipant("nonexistent");
89-
EXPECT_EQ(participant, nullptr) << "Looking up participant before connect should return nullptr";
90-
}
91-
9222
// Server-dependent tests - require LIVEKIT_URL and LIVEKIT_TOKEN_A env vars
93-
class RoomServerTest : public ::testing::Test {
23+
class RoomTest : public ::testing::Test {
9424
protected:
9525
void SetUp() override {
9626
livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole);
@@ -112,12 +42,7 @@ class RoomServerTest : public ::testing::Test {
11242
std::string token_;
11343
};
11444

115-
TEST_F(RoomServerTest, ConnectToServer) {
116-
if (!server_available_) {
117-
GTEST_SKIP() << "LIVEKIT_URL and LIVEKIT_TOKEN_A not set, skipping server "
118-
"connection test";
119-
}
120-
45+
TEST_F(RoomTest, ConnectToServer) {
12146
Room room;
12247
RoomOptions options;
12348

@@ -129,19 +54,15 @@ TEST_F(RoomServerTest, ConnectToServer) {
12954
}
13055
}
13156

132-
TEST_F(RoomServerTest, ConnectWithInvalidToken) {
133-
if (!server_available_) {
134-
GTEST_SKIP() << "LIVEKIT_URL not set, skipping invalid token test";
135-
}
136-
57+
TEST_F(RoomTest, ConnectWithInvalidToken) {
13758
Room room;
13859
RoomOptions options;
13960

14061
bool connected = room.Connect(server_url_, "invalid_token", options);
14162
EXPECT_FALSE(connected) << "Should fail to connect with invalid token";
14263
}
14364

144-
TEST_F(RoomServerTest, ConnectWithInvalidUrl) {
65+
TEST_F(RoomTest, ConnectWithInvalidUrl) {
14566
Room room;
14667
RoomOptions options;
14768

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include <livekit/audio_source.h>
19+
#include <livekit/livekit.h>
20+
21+
namespace livekit::test {
22+
23+
class AudioSourceTest : public ::testing::Test {
24+
protected:
25+
void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); }
26+
void TearDown() override { livekit::shutdown(); }
27+
};
28+
29+
TEST_F(AudioSourceTest, ConstructAndQueryProperties) {
30+
AudioSource source(48000, 1);
31+
EXPECT_EQ(source.sample_rate(), 48000);
32+
EXPECT_EQ(source.num_channels(), 1);
33+
EXPECT_NE(source.ffi_handle_id(), 0u);
34+
EXPECT_DOUBLE_EQ(source.queuedDuration(), 0.0);
35+
}
36+
37+
TEST_F(AudioSourceTest, ClearQueueIsSafeOnFreshSource) {
38+
AudioSource source(48000, 2, /*queue_size_ms=*/0);
39+
source.clearQueue();
40+
EXPECT_DOUBLE_EQ(source.queuedDuration(), 0.0);
41+
}
42+
43+
} // namespace livekit::test
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include <livekit/data_stream.h>
19+
20+
#include <type_traits>
21+
22+
namespace livekit::test {
23+
24+
TEST(DataStreamTest, TextStreamReaderConstructAndInfo) {
25+
TextStreamInfo info;
26+
info.stream_id = "stream-1";
27+
info.mime_type = "text/plain";
28+
info.topic = "chat";
29+
info.timestamp = 42;
30+
info.attachments = {"a.txt"};
31+
32+
TextStreamReader reader(info);
33+
EXPECT_EQ(reader.info().stream_id, "stream-1");
34+
EXPECT_EQ(reader.info().mime_type, "text/plain");
35+
EXPECT_EQ(reader.info().topic, "chat");
36+
EXPECT_EQ(reader.info().timestamp, 42);
37+
ASSERT_EQ(reader.info().attachments.size(), 1u);
38+
EXPECT_EQ(reader.info().attachments.front(), "a.txt");
39+
}
40+
41+
TEST(DataStreamTest, ByteStreamReaderConstructAndInfo) {
42+
ByteStreamInfo info;
43+
info.stream_id = "stream-2";
44+
info.mime_type = "application/octet-stream";
45+
info.topic = "files";
46+
info.name = "data.bin";
47+
48+
ByteStreamReader reader(info);
49+
EXPECT_EQ(reader.info().stream_id, "stream-2");
50+
EXPECT_EQ(reader.info().name, "data.bin");
51+
}
52+
53+
TEST(DataStreamTest, WriterTypesAreDerivedFromBase) {
54+
static_assert(std::is_base_of_v<BaseStreamWriter, TextStreamWriter>);
55+
static_assert(std::is_base_of_v<BaseStreamWriter, ByteStreamWriter>);
56+
EXPECT_GT(kStreamChunkSize, 0u);
57+
}
58+
59+
} // namespace livekit::test
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include <livekit/data_track_error.h>
19+
20+
#include "data_track.pb.h"
21+
22+
namespace livekit::test {
23+
24+
TEST(DataTrackErrorTest, PublishErrorFromEmptyProto) {
25+
proto::PublishDataTrackError proto_err;
26+
PublishDataTrackError err = PublishDataTrackError::fromProto(proto_err);
27+
EXPECT_EQ(err.code, PublishDataTrackErrorCode::UNKNOWN);
28+
}
29+
30+
TEST(DataTrackErrorTest, TryPushErrorFromEmptyProto) {
31+
proto::LocalDataTrackTryPushError proto_err;
32+
LocalDataTrackTryPushError err = LocalDataTrackTryPushError::fromProto(proto_err);
33+
EXPECT_EQ(err.code, LocalDataTrackTryPushErrorCode::UNKNOWN);
34+
}
35+
36+
TEST(DataTrackErrorTest, SubscribeErrorFromEmptyProto) {
37+
proto::SubscribeDataTrackError proto_err;
38+
SubscribeDataTrackError err = SubscribeDataTrackError::fromProto(proto_err);
39+
EXPECT_EQ(err.code, SubscribeDataTrackErrorCode::UNKNOWN);
40+
}
41+
42+
} // namespace livekit::test
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include <livekit/data_track_frame.h>
19+
20+
#include <cstdint>
21+
#include <vector>
22+
23+
#include "data_track.pb.h"
24+
25+
namespace livekit::test {
26+
27+
TEST(DataTrackFrameTest, DefaultConstructed) {
28+
DataTrackFrame frame;
29+
EXPECT_TRUE(frame.payload.empty());
30+
EXPECT_FALSE(frame.user_timestamp.has_value());
31+
}
32+
33+
TEST(DataTrackFrameTest, PayloadAndTimestampConstructor) {
34+
std::vector<std::uint8_t> payload{1, 2, 3};
35+
DataTrackFrame frame(std::move(payload), 12345u);
36+
ASSERT_EQ(frame.payload.size(), 3u);
37+
EXPECT_EQ(frame.payload[0], 1u);
38+
ASSERT_TRUE(frame.user_timestamp.has_value());
39+
EXPECT_EQ(*frame.user_timestamp, 12345u);
40+
}
41+
42+
TEST(DataTrackFrameTest, FromOwnedInfoEmptyProto) {
43+
proto::DataTrackFrame proto_frame;
44+
DataTrackFrame frame = DataTrackFrame::fromOwnedInfo(proto_frame);
45+
EXPECT_TRUE(frame.payload.empty());
46+
EXPECT_FALSE(frame.user_timestamp.has_value());
47+
}
48+
49+
} // namespace livekit::test

0 commit comments

Comments
 (0)