diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b42c297..610eb1c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -345,6 +345,7 @@ jobs: --root . \ --gcov-executable "${GCOV_EXECUTABLE}" \ --gcov-ignore-parse-errors=all \ + --filter 'include/' \ --filter 'src/' \ --exclude 'src/tests/' \ --exclude '.*\.pb\.' \ diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index f7906c42..a292a2c7 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -62,6 +62,9 @@ enum class LogSink { /// already initialized. LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info, const LogSink& log_sink = LogSink::kConsole); +/// Returns true if the LiveKit SDK is initialized, false otherwise. +LIVEKIT_API bool isInitialized(); + /// Shut down the LiveKit SDK. /// /// After shutdown, you may call initialize() again. diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index d0b2af55..bd982de1 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -141,6 +141,11 @@ std::optional ExtractAsyncId(const proto::FfiEvent& event) { } // namespace +FfiClient& FfiClient::instance() noexcept { + static FfiClient instance; + return instance; +} + // clang-tidy flags this as a trivial destructor in release mode // due to the assert being pre-processed out // NOLINTNEXTLINE(modernize-use-equals-default) @@ -182,6 +187,13 @@ void FfiClient::RemoveListener(ListenerId id) { } proto::FfiResponse FfiClient::sendRequest(const proto::FfiRequest& request) const { + // The Rust FFI will lazily initialize the FFI client when the first request is sent, + // but if not initialized none of the async operations will work. Guarding against that here. + // Improvement ticket added to the Rust SDK to discuss this + if (!isInitialized()) { + throw std::runtime_error("FfiClient::sendRequest failed: LiveKit is not initialized"); + } + std::string bytes; if (!request.SerializeToString(&bytes) || bytes.empty()) { throw std::runtime_error("failed to serialize FfiRequest"); diff --git a/src/ffi_client.h b/src/ffi_client.h index dc00d565..d16f3d90 100644 --- a/src/ffi_client.h +++ b/src/ffi_client.h @@ -74,10 +74,9 @@ class LIVEKIT_INTERNAL_API FfiClient { FfiClient(FfiClient&&) = delete; FfiClient& operator=(FfiClient&&) = delete; - static FfiClient& instance() noexcept { - static FfiClient instance; - return instance; - } + // Access the singleton instance of the FfiClient + // Note: lazily created, not thread safe + static FfiClient& instance() noexcept; // Must be called before any other FFI usage bool initialize(bool capture_logs); diff --git a/src/livekit.cpp b/src/livekit.cpp index 1bcd9df9..1417213e 100644 --- a/src/livekit.cpp +++ b/src/livekit.cpp @@ -22,14 +22,16 @@ namespace livekit { bool initialize(const LogLevel& level, const LogSink& log_sink) { + // Initializes logger if singleton instance is not already initialized setLogLevel(level); auto& ffi_client = FfiClient::instance(); return ffi_client.initialize(log_sink == LogSink::kCallback); } +bool isInitialized() { return FfiClient::instance().isInitialized(); } + void shutdown() { - auto& ffi_client = FfiClient::instance(); - ffi_client.shutdown(); + FfiClient::instance().shutdown(); detail::shutdownLogger(); } diff --git a/src/room.cpp b/src/room.cpp index d9e96a44..3a7eac35 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -23,6 +23,7 @@ #include "ffi_client.h" #include "livekit/audio_stream.h" #include "livekit/e2ee.h" +#include "livekit/livekit.h" #include "livekit/local_data_track.h" #include "livekit/local_participant.h" #include "livekit/local_track_publication.h" @@ -105,6 +106,11 @@ void Room::setDelegate(RoomDelegate* delegate) { bool Room::Connect(const std::string& url, const std::string& token, const RoomOptions& options) { TRACE_EVENT0("livekit", "Room::Connect"); + if (!livekit::isInitialized()) { + LK_LOG_ERROR("Room::Connect failed: LiveKit is not initialized"); + return false; + } + { const std::scoped_lock g(lock_); if (connection_state_ != ConnectionState::Disconnected) { diff --git a/src/tests/integration/test_room.cpp b/src/tests/integration/test_room.cpp index 011bdf23..29bffcd7 100644 --- a/src/tests/integration/test_room.cpp +++ b/src/tests/integration/test_room.cpp @@ -19,78 +19,8 @@ namespace livekit::test { -class RoomTest : public ::testing::Test { -protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } - - void TearDown() override { livekit::shutdown(); } -}; - -TEST_F(RoomTest, CreateRoom) { - Room room; - // Room should be created without issues - EXPECT_EQ(room.localParticipant(), nullptr) << "Local participant should be null before connect"; -} - -TEST_F(RoomTest, RoomOptionsDefaults) { - RoomOptions options; - - EXPECT_TRUE(options.auto_subscribe) << "auto_subscribe should default to true"; - EXPECT_FALSE(options.dynacast) << "dynacast should default to false"; - EXPECT_FALSE(options.rtc_config.has_value()) << "rtc_config should not have a value by default"; - EXPECT_FALSE(options.encryption.has_value()) << "encryption should not have a value by default"; -} - -TEST_F(RoomTest, RtcConfigDefaults) { - RtcConfig config; - - EXPECT_EQ(config.ice_transport_type, 0); - EXPECT_EQ(config.continual_gathering_policy, 0); - EXPECT_TRUE(config.ice_servers.empty()); -} - -TEST_F(RoomTest, IceServerConfiguration) { - IceServer server; - server.url = "stun:stun.l.google.com:19302"; - server.username = "user"; - server.credential = "pass"; - - EXPECT_EQ(server.url, "stun:stun.l.google.com:19302"); - EXPECT_EQ(server.username, "user"); - EXPECT_EQ(server.credential, "pass"); -} - -TEST_F(RoomTest, RoomWithCustomRtcConfig) { - RoomOptions options; - options.auto_subscribe = false; - options.dynacast = true; - - RtcConfig rtc_config; - rtc_config.ice_servers.push_back({"stun:stun.l.google.com:19302", "", ""}); - rtc_config.ice_servers.push_back({"turn:turn.example.com:3478", "user", "pass"}); - - options.rtc_config = rtc_config; - - EXPECT_FALSE(options.auto_subscribe); - EXPECT_TRUE(options.dynacast); - EXPECT_TRUE(options.rtc_config.has_value()); - EXPECT_EQ(options.rtc_config->ice_servers.size(), 2); -} - -TEST_F(RoomTest, RemoteParticipantsEmptyBeforeConnect) { - Room room; - auto participants = room.remoteParticipants(); - EXPECT_TRUE(participants.empty()) << "Remote participants should be empty before connect"; -} - -TEST_F(RoomTest, RemoteParticipantLookupBeforeConnect) { - Room room; - auto participant = room.remoteParticipant("nonexistent"); - EXPECT_EQ(participant, nullptr) << "Looking up participant before connect should return nullptr"; -} - // Server-dependent tests - require LIVEKIT_URL and LIVEKIT_TOKEN_A env vars -class RoomServerTest : public ::testing::Test { +class RoomTest : public ::testing::Test { protected: void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); @@ -112,12 +42,7 @@ class RoomServerTest : public ::testing::Test { std::string token_; }; -TEST_F(RoomServerTest, ConnectToServer) { - if (!server_available_) { - GTEST_SKIP() << "LIVEKIT_URL and LIVEKIT_TOKEN_A not set, skipping server " - "connection test"; - } - +TEST_F(RoomTest, ConnectToServer) { Room room; RoomOptions options; @@ -129,11 +54,7 @@ TEST_F(RoomServerTest, ConnectToServer) { } } -TEST_F(RoomServerTest, ConnectWithInvalidToken) { - if (!server_available_) { - GTEST_SKIP() << "LIVEKIT_URL not set, skipping invalid token test"; - } - +TEST_F(RoomTest, ConnectWithInvalidToken) { Room room; RoomOptions options; @@ -141,7 +62,7 @@ TEST_F(RoomServerTest, ConnectWithInvalidToken) { EXPECT_FALSE(connected) << "Should fail to connect with invalid token"; } -TEST_F(RoomServerTest, ConnectWithInvalidUrl) { +TEST_F(RoomTest, ConnectWithInvalidUrl) { Room room; RoomOptions options; diff --git a/src/tests/unit/test_audio_source.cpp b/src/tests/unit/test_audio_source.cpp new file mode 100644 index 00000000..bd2fc577 --- /dev/null +++ b/src/tests/unit/test_audio_source.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include +#include + +namespace livekit::test { + +class AudioSourceTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(AudioSourceTest, ConstructAndQueryProperties) { + AudioSource source(48000, 1); + EXPECT_EQ(source.sample_rate(), 48000); + EXPECT_EQ(source.num_channels(), 1); + EXPECT_NE(source.ffi_handle_id(), 0u); + EXPECT_DOUBLE_EQ(source.queuedDuration(), 0.0); +} + +TEST_F(AudioSourceTest, ClearQueueIsSafeOnFreshSource) { + AudioSource source(48000, 2, /*queue_size_ms=*/0); + source.clearQueue(); + EXPECT_DOUBLE_EQ(source.queuedDuration(), 0.0); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_data_stream.cpp b/src/tests/unit/test_data_stream.cpp new file mode 100644 index 00000000..840f5e02 --- /dev/null +++ b/src/tests/unit/test_data_stream.cpp @@ -0,0 +1,59 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include + +namespace livekit::test { + +TEST(DataStreamTest, TextStreamReaderConstructAndInfo) { + TextStreamInfo info; + info.stream_id = "stream-1"; + info.mime_type = "text/plain"; + info.topic = "chat"; + info.timestamp = 42; + info.attachments = {"a.txt"}; + + TextStreamReader reader(info); + EXPECT_EQ(reader.info().stream_id, "stream-1"); + EXPECT_EQ(reader.info().mime_type, "text/plain"); + EXPECT_EQ(reader.info().topic, "chat"); + EXPECT_EQ(reader.info().timestamp, 42); + ASSERT_EQ(reader.info().attachments.size(), 1u); + EXPECT_EQ(reader.info().attachments.front(), "a.txt"); +} + +TEST(DataStreamTest, ByteStreamReaderConstructAndInfo) { + ByteStreamInfo info; + info.stream_id = "stream-2"; + info.mime_type = "application/octet-stream"; + info.topic = "files"; + info.name = "data.bin"; + + ByteStreamReader reader(info); + EXPECT_EQ(reader.info().stream_id, "stream-2"); + EXPECT_EQ(reader.info().name, "data.bin"); +} + +TEST(DataStreamTest, WriterTypesAreDerivedFromBase) { + static_assert(std::is_base_of_v); + static_assert(std::is_base_of_v); + EXPECT_GT(kStreamChunkSize, 0u); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_data_track_error.cpp b/src/tests/unit/test_data_track_error.cpp new file mode 100644 index 00000000..cc41fbd0 --- /dev/null +++ b/src/tests/unit/test_data_track_error.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include "data_track.pb.h" + +namespace livekit::test { + +TEST(DataTrackErrorTest, PublishErrorFromEmptyProto) { + proto::PublishDataTrackError proto_err; + PublishDataTrackError err = PublishDataTrackError::fromProto(proto_err); + EXPECT_EQ(err.code, PublishDataTrackErrorCode::UNKNOWN); +} + +TEST(DataTrackErrorTest, TryPushErrorFromEmptyProto) { + proto::LocalDataTrackTryPushError proto_err; + LocalDataTrackTryPushError err = LocalDataTrackTryPushError::fromProto(proto_err); + EXPECT_EQ(err.code, LocalDataTrackTryPushErrorCode::UNKNOWN); +} + +TEST(DataTrackErrorTest, SubscribeErrorFromEmptyProto) { + proto::SubscribeDataTrackError proto_err; + SubscribeDataTrackError err = SubscribeDataTrackError::fromProto(proto_err); + EXPECT_EQ(err.code, SubscribeDataTrackErrorCode::UNKNOWN); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_data_track_frame.cpp b/src/tests/unit/test_data_track_frame.cpp new file mode 100644 index 00000000..96323b18 --- /dev/null +++ b/src/tests/unit/test_data_track_frame.cpp @@ -0,0 +1,49 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include +#include + +#include "data_track.pb.h" + +namespace livekit::test { + +TEST(DataTrackFrameTest, DefaultConstructed) { + DataTrackFrame frame; + EXPECT_TRUE(frame.payload.empty()); + EXPECT_FALSE(frame.user_timestamp.has_value()); +} + +TEST(DataTrackFrameTest, PayloadAndTimestampConstructor) { + std::vector payload{1, 2, 3}; + DataTrackFrame frame(std::move(payload), 12345u); + ASSERT_EQ(frame.payload.size(), 3u); + EXPECT_EQ(frame.payload[0], 1u); + ASSERT_TRUE(frame.user_timestamp.has_value()); + EXPECT_EQ(*frame.user_timestamp, 12345u); +} + +TEST(DataTrackFrameTest, FromOwnedInfoEmptyProto) { + proto::DataTrackFrame proto_frame; + DataTrackFrame frame = DataTrackFrame::fromOwnedInfo(proto_frame); + EXPECT_TRUE(frame.payload.empty()); + EXPECT_FALSE(frame.user_timestamp.has_value()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_data_track_info.cpp b/src/tests/unit/test_data_track_info.cpp new file mode 100644 index 00000000..0e0eb03c --- /dev/null +++ b/src/tests/unit/test_data_track_info.cpp @@ -0,0 +1,36 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +namespace livekit::test { + +TEST(DataTrackInfoTest, DefaultConstructed) { + DataTrackInfo info; + EXPECT_TRUE(info.name.empty()); + EXPECT_TRUE(info.sid.empty()); + EXPECT_FALSE(info.uses_e2ee); +} + +TEST(DataTrackInfoTest, AggregateInitialization) { + DataTrackInfo info{"name", "sid", true}; + EXPECT_EQ(info.name, "name"); + EXPECT_EQ(info.sid, "sid"); + EXPECT_TRUE(info.uses_e2ee); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_e2ee.cpp b/src/tests/unit/test_e2ee.cpp new file mode 100644 index 00000000..678c8f62 --- /dev/null +++ b/src/tests/unit/test_e2ee.cpp @@ -0,0 +1,45 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include + +namespace livekit::test { + +TEST(E2EETest, KeyProviderOptionsDefaults) { + KeyProviderOptions options; + EXPECT_FALSE(options.shared_key.has_value()); + EXPECT_FALSE(options.ratchet_salt.empty()); + EXPECT_EQ(options.ratchet_window_size, kDefaultRatchetWindowSize); + EXPECT_EQ(options.failure_tolerance, kDefaultFailureTolerance); +} + +TEST(E2EETest, E2EEOptionsDefaults) { + E2EEOptions options; + EXPECT_EQ(options.encryption_type, EncryptionType::GCM); +} + +TEST(E2EETest, FrameCryptorAccessors) { + E2EEManager::FrameCryptor cryptor(/*room_handle=*/0, "alice", /*key_index=*/3, + /*enabled=*/true); + EXPECT_EQ(cryptor.participantIdentity(), "alice"); + EXPECT_EQ(cryptor.keyIndex(), 3); + EXPECT_TRUE(cryptor.enabled()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_ffi_client.cpp b/src/tests/unit/test_ffi_client.cpp new file mode 100644 index 00000000..f6bf9d67 --- /dev/null +++ b/src/tests/unit/test_ffi_client.cpp @@ -0,0 +1,262 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include +#include + +#include "ffi.pb.h" +#include "ffi_client.h" + +namespace livekit::test { + +class FfiClientTest : public ::testing::Test { +protected: + void SetUp() override { + // Ensure the singleton for this test case starts up uninitialized + livekit::shutdown(); + + // This assert helps test the livekit::shutdown() <-> FFI client interface + ASSERT_FALSE(FfiClient::instance().isInitialized()); + } + + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(FfiClientTest, Singleton) { + auto& a = FfiClient::instance(); + auto& b = FfiClient::instance(); + EXPECT_EQ(&a, &b); +} + +// --------------------------------------------------------------------------- +// Initialization state +// --------------------------------------------------------------------------- + +TEST_F(FfiClientTest, DefaultUninitialized) { EXPECT_FALSE(FfiClient::instance().isInitialized()); } + +TEST_F(FfiClientTest, Initialize) { + EXPECT_TRUE(FfiClient::instance().initialize(false)); + EXPECT_TRUE(FfiClient::instance().isInitialized()); +} + +TEST_F(FfiClientTest, InitializeFromSDK) { + EXPECT_TRUE(livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole)); + EXPECT_TRUE(FfiClient::instance().isInitialized()); +} + +TEST_F(FfiClientTest, DoubleInitialize) { + ASSERT_TRUE(FfiClient::instance().initialize(false)); + EXPECT_FALSE(FfiClient::instance().initialize(false)) + << "second initialize() on an already-initialized client must be a no-op"; + EXPECT_TRUE(FfiClient::instance().isInitialized()); +} + +TEST_F(FfiClientTest, Shutdown) { + ASSERT_TRUE(FfiClient::instance().initialize(false)); + ASSERT_TRUE(FfiClient::instance().isInitialized()); + + FfiClient::instance().shutdown(); + EXPECT_FALSE(FfiClient::instance().isInitialized()); +} + +TEST_F(FfiClientTest, ShutdownWithoutInitialize) { + EXPECT_NO_THROW(FfiClient::instance().shutdown()); + EXPECT_FALSE(FfiClient::instance().isInitialized()); +} + +TEST_F(FfiClientTest, RepeatedShutdown) { + FfiClient::instance().initialize(false); + EXPECT_NO_THROW(FfiClient::instance().shutdown()); + EXPECT_NO_THROW(FfiClient::instance().shutdown()); + EXPECT_NO_THROW(FfiClient::instance().shutdown()); +} + +TEST_F(FfiClientTest, ReinitializeAfterShutdown) { + ASSERT_TRUE(FfiClient::instance().initialize(false)); + FfiClient::instance().shutdown(); + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_TRUE(FfiClient::instance().initialize(false)); + EXPECT_TRUE(FfiClient::instance().isInitialized()); +} + +// --------------------------------------------------------------------------- +// AddListener / RemoveListener +// --------------------------------------------------------------------------- + +TEST_F(FfiClientTest, AddListenerReturnsNonZeroId) { + const auto id = FfiClient::instance().AddListener([](const proto::FfiEvent&) {}); + EXPECT_NE(id, 0); + FfiClient::instance().RemoveListener(id); +} + +TEST_F(FfiClientTest, AddListenerReturnsUniqueIds) { + constexpr int kCount = 16; + std::unordered_set ids; + ids.reserve(kCount); + for (int i = 0; i < kCount; ++i) { + const auto id = FfiClient::instance().AddListener([](const proto::FfiEvent&) {}); + EXPECT_TRUE(ids.insert(id).second) << "duplicate listener id: " << id; + } + for (auto id : ids) { + FfiClient::instance().RemoveListener(id); + } +} + +TEST_F(FfiClientTest, RemoveListenerWithUnknownIdIsSafe) { + EXPECT_NO_THROW(FfiClient::instance().RemoveListener(424242)); +} + +TEST_F(FfiClientTest, RemoveListenerIsIdempotent) { + const auto id = FfiClient::instance().AddListener([](const proto::FfiEvent&) {}); + EXPECT_NO_THROW(FfiClient::instance().RemoveListener(id)); + EXPECT_NO_THROW(FfiClient::instance().RemoveListener(id)); +} + +TEST_F(FfiClientTest, ListenerRegistrationSurvivesShutdownReinitCycle) { + FfiClient::instance().initialize(false); + const auto id = FfiClient::instance().AddListener([](const proto::FfiEvent&) {}); + EXPECT_NE(id, 0); + + // shutdown() does not clear the C++-side listener map today; document that + // contract here so a future refactor that changes it is a deliberate choice. + FfiClient::instance().shutdown(); + EXPECT_NO_THROW(FfiClient::instance().RemoveListener(id)); +} + +// --------------------------------------------------------------------------- +// These tests ensure FfiClient methods throw in various error conditions +// --------------------------------------------------------------------------- + +TEST_F(FfiClientTest, SendRequestThrowsOnEmptyRequest) { + // A default-constructed FfiRequest has no oneof populated and serializes + // to zero bytes, which sendRequest treats as a serialization failure. + // This path is reachable regardless of initialization state. + proto::FfiRequest req; + EXPECT_THROW(FfiClient::instance().sendRequest(req), std::runtime_error); +} + +TEST_F(FfiClientTest, SendRequestThrowsAfterShutdown) { + ASSERT_TRUE(FfiClient::instance().initialize(false)); + FfiClient::instance().shutdown(); + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + proto::FfiRequest req; + (void)req.mutable_dispose(); + + EXPECT_THROW(FfiClient::instance().sendRequest(req), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_ConnectAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + RoomOptions options; + EXPECT_THROW(FfiClient::instance().connectAsync("wss://localhost:7880", "fake-token", options), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_PublishTrackAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + TrackPublishOptions options; + EXPECT_THROW(FfiClient::instance().publishTrackAsync(1, 2, options), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_UnpublishTrackAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_THROW(FfiClient::instance().unpublishTrackAsync(1, "sid", true), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_PublishDataAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + const std::uint8_t payload[1] = {0}; + EXPECT_THROW(FfiClient::instance().publishDataAsync(1, payload, 1, true, {}, ""), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_PublishSipDtmfAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_THROW(FfiClient::instance().publishSipDtmfAsync(1, 1, "1", {}), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_SetLocalMetadataAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_THROW(FfiClient::instance().setLocalMetadataAsync(1, "metadata"), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_CaptureAudioFrameAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + proto::AudioFrameBufferInfo buf; + EXPECT_THROW(FfiClient::instance().captureAudioFrameAsync(1, buf), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_PerformRpcAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_THROW(FfiClient::instance().performRpcAsync(1, "dest", "method", "payload", std::nullopt), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_GetTrackStatsAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + EXPECT_THROW(FfiClient::instance().getTrackStatsAsync(1), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_PublishDataTrackAsyncFails) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + auto fut_result = FfiClient::instance().publishDataTrackAsync(1, "name"); + auto result = fut_result.get(); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.error().code, PublishDataTrackErrorCode::INTERNAL); +} + +TEST_F(FfiClientTest, NotInitialized_SubscribeDataTrackFails) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + auto result = FfiClient::instance().subscribeDataTrack(1); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.error().code, SubscribeDataTrackErrorCode::INTERNAL); +} + +TEST_F(FfiClientTest, NotInitialized_SendStreamHeaderAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + proto::DataStream::Header header; + EXPECT_THROW(FfiClient::instance().sendStreamHeaderAsync(1, header, {}, "sender"), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_SendStreamChunkAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + proto::DataStream::Chunk chunk; + EXPECT_THROW(FfiClient::instance().sendStreamChunkAsync(1, chunk, {}, "sender"), std::runtime_error); +} + +TEST_F(FfiClientTest, NotInitialized_SendStreamTrailerAsyncThrows) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + proto::DataStream::Trailer trailer; + EXPECT_THROW(FfiClient::instance().sendStreamTrailerAsync(1, trailer, "sender"), std::runtime_error); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_ffi_handle.cpp b/src/tests/unit/test_ffi_handle.cpp new file mode 100644 index 00000000..a6ca3109 --- /dev/null +++ b/src/tests/unit/test_ffi_handle.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include +#include + +namespace livekit::test { + +TEST(FfiHandleTest, DefaultConstructedIsInvalid) { + FfiHandle handle; + EXPECT_EQ(handle.get(), 0u); + EXPECT_FALSE(handle.valid()); + EXPECT_FALSE(static_cast(handle)); +} + +TEST(FfiHandleTest, ExplicitZeroIsInvalid) { + FfiHandle handle(0); + EXPECT_FALSE(handle.valid()); +} + +TEST(FfiHandleTest, ResetToZeroIsSafe) { + FfiHandle handle; + handle.reset(); + EXPECT_FALSE(handle.valid()); +} + +TEST(FfiHandleTest, ReleaseReturnsCurrentHandleAndClears) { + FfiHandle handle; + const std::uintptr_t released = handle.release(); + EXPECT_EQ(released, 0u); + EXPECT_FALSE(handle.valid()); +} + +TEST(FfiHandleTest, MoveTransfersOwnership) { + FfiHandle src; + FfiHandle dst(std::move(src)); + EXPECT_FALSE(dst.valid()); + EXPECT_FALSE(src.valid()); // NOLINT(bugprone-use-after-move) + + FfiHandle assigned; + assigned = std::move(dst); + EXPECT_FALSE(assigned.valid()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_local_tracks.cpp b/src/tests/unit/test_local_tracks.cpp new file mode 100644 index 00000000..4b23d316 --- /dev/null +++ b/src/tests/unit/test_local_tracks.cpp @@ -0,0 +1,53 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace livekit::test { + +TEST(LocalTracksTest, LocalAudioTrackFactoryIsAddressable) { + using FactoryT = std::shared_ptr (*)(const std::string&, const std::shared_ptr&); + FactoryT factory = &LocalAudioTrack::createLocalAudioTrack; + EXPECT_NE(factory, nullptr); +} + +TEST(LocalTracksTest, LocalVideoTrackFactoryIsAddressable) { + using FactoryT = std::shared_ptr (*)(const std::string&, const std::shared_ptr&); + FactoryT factory = &LocalVideoTrack::createLocalVideoTrack; + EXPECT_NE(factory, nullptr); +} + +TEST(LocalTracksTest, LocalTrackInheritsFromTrack) { + static_assert(std::is_base_of_v); + static_assert(std::is_base_of_v); + SUCCEED(); +} + +TEST(LocalTracksTest, LocalDataTrackIsNoncopyable) { + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_copy_assignable_v); + SUCCEED(); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_remote_audio_track.cpp b/src/tests/unit/test_remote_audio_track.cpp new file mode 100644 index 00000000..d7a7c961 --- /dev/null +++ b/src/tests/unit/test_remote_audio_track.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include "track.pb.h" + +namespace livekit::test { + +TEST(RemoteAudioTrackTest, ConstructFromEmptyOwnedTrack) { + proto::OwnedTrack owned; + RemoteAudioTrack track(owned); + + EXPECT_TRUE(track.sid().empty()); + EXPECT_TRUE(track.name().empty()); + EXPECT_TRUE(track.remote()); + EXPECT_FALSE(track.has_handle()); + + const std::string description = track.to_string(); + EXPECT_FALSE(description.empty()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_remote_video_track.cpp b/src/tests/unit/test_remote_video_track.cpp new file mode 100644 index 00000000..6e5777dd --- /dev/null +++ b/src/tests/unit/test_remote_video_track.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include "track.pb.h" + +namespace livekit::test { + +TEST(RemoteVideoTrackTest, ConstructFromEmptyOwnedTrack) { + proto::OwnedTrack owned; + RemoteVideoTrack track(owned); + + EXPECT_TRUE(track.sid().empty()); + EXPECT_TRUE(track.name().empty()); + EXPECT_TRUE(track.remote()); + EXPECT_FALSE(track.has_handle()); + + const std::string description = track.to_string(); + EXPECT_FALSE(description.empty()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_result.cpp b/src/tests/unit/test_result.cpp index 728c9b1e..e643690f 100644 --- a/src/tests/unit/test_result.cpp +++ b/src/tests/unit/test_result.cpp @@ -47,43 +47,43 @@ struct SimpleError { // Result — success path // --------------------------------------------------------------------------- -TEST(ResultTest, SuccessOkIsTrue) { +TEST(ResultTest, Int_SuccessOkIsTrue) { auto r = Result::success(42); EXPECT_TRUE(r.ok()); } -TEST(ResultTest, SuccessHasErrorIsFalse) { +TEST(ResultTest, Int_SuccessHasErrorIsFalse) { auto r = Result::success(42); EXPECT_FALSE(r.has_error()); } -TEST(ResultTest, SuccessBoolConversionIsTrue) { +TEST(ResultTest, Int_SuccessBoolConversionIsTrue) { auto r = Result::success(42); EXPECT_TRUE(r); } -TEST(ResultTest, SuccessValueMatchesInput) { +TEST(ResultTest, Int_SuccessValueMatchesInput) { auto r = Result::success(99); EXPECT_EQ(r.value(), 99); } -TEST(ResultTest, SuccessConstValueMatchesInput) { +TEST(ResultTest, Int_SuccessConstValueMatchesInput) { const auto r = Result::success(7); EXPECT_EQ(r.value(), 7); } -TEST(ResultTest, SuccessValueCanBeMutated) { +TEST(ResultTest, Int_SuccessValueCanBeMutated) { auto r = Result::success(1); r.value() = 100; EXPECT_EQ(r.value(), 100); } -TEST(ResultTest, SuccessStringValue) { +TEST(ResultTest, Int_SuccessStringValue) { auto r = Result::success("hello"); EXPECT_EQ(r.value(), "hello"); } -TEST(ResultTest, SuccessMoveValueTransfersOwnership) { +TEST(ResultTest, Int_SuccessMoveValueTransfersOwnership) { auto r = Result, SimpleError>::success(std::make_unique(55)); auto ptr = std::move(r).value(); ASSERT_NE(ptr, nullptr); @@ -94,40 +94,40 @@ TEST(ResultTest, SuccessMoveValueTransfersOwnership) { // Result — failure path // --------------------------------------------------------------------------- -TEST(ResultTest, FailureOkIsFalse) { +TEST(ResultTest, Int_FailureOkIsFalse) { auto r = Result::failure(SimpleError{1, "oops"}); EXPECT_FALSE(r.ok()); } -TEST(ResultTest, FailureHasErrorIsTrue) { +TEST(ResultTest, Int_FailureHasErrorIsTrue) { auto r = Result::failure(SimpleError{1, "oops"}); EXPECT_TRUE(r.has_error()); } -TEST(ResultTest, FailureBoolConversionIsFalse) { +TEST(ResultTest, Int_FailureBoolConversionIsFalse) { auto r = Result::failure(SimpleError{1, "oops"}); EXPECT_FALSE(r); } -TEST(ResultTest, FailureErrorCodeMatchesInput) { +TEST(ResultTest, Int_FailureErrorCodeMatchesInput) { auto r = Result::failure(SimpleError{42, "bad"}); EXPECT_EQ(r.error().code, 42); EXPECT_EQ(r.error().message, "bad"); } -TEST(ResultTest, FailureConstErrorMatchesInput) { +TEST(ResultTest, Int_FailureConstErrorMatchesInput) { const auto r = Result::failure(SimpleError{3, "err"}); EXPECT_EQ(r.error().code, 3); } -TEST(ResultTest, FailureMoveErrorTransfersOwnership) { +TEST(ResultTest, Int_FailureMoveErrorTransfersOwnership) { auto r = Result>::failure(std::make_unique(SimpleError{9, "moved"})); auto err = std::move(r).error(); ASSERT_NE(err, nullptr); EXPECT_EQ(err->code, 9); } -TEST(ResultTest, FailureStringError) { +TEST(ResultTest, Int_FailureStringError) { auto r = Result::failure("something went wrong"); EXPECT_EQ(r.error(), "something went wrong"); } @@ -170,22 +170,22 @@ TEST(ResultTest, MoveErrorOnSuccessThrowsLogicError) { // Result — success path // --------------------------------------------------------------------------- -TEST(ResultVoidTest, SuccessOkIsTrue) { +TEST(ResultTest, Void_SuccessOkIsTrue) { auto r = Result::success(); EXPECT_TRUE(r.ok()); } -TEST(ResultVoidTest, SuccessHasErrorIsFalse) { +TEST(ResultTest, Void_SuccessHasErrorIsFalse) { auto r = Result::success(); EXPECT_FALSE(r.has_error()); } -TEST(ResultVoidTest, SuccessBoolConversionIsTrue) { +TEST(ResultTest, Void_SuccessBoolConversionIsTrue) { auto r = Result::success(); EXPECT_TRUE(r); } -TEST(ResultVoidTest, SuccessValueIsCallable) { +TEST(ResultTest, Void_SuccessValueIsCallable) { auto r = Result::success(); EXPECT_NO_THROW(r.value()); } @@ -194,28 +194,28 @@ TEST(ResultVoidTest, SuccessValueIsCallable) { // Result — failure path // --------------------------------------------------------------------------- -TEST(ResultVoidTest, FailureOkIsFalse) { +TEST(ResultTest, Void_FailureOkIsFalse) { auto r = Result::failure(SimpleError{5, "void fail"}); EXPECT_FALSE(r.ok()); } -TEST(ResultVoidTest, FailureHasErrorIsTrue) { +TEST(ResultTest, Void_FailureHasErrorIsTrue) { auto r = Result::failure(SimpleError{5, "void fail"}); EXPECT_TRUE(r.has_error()); } -TEST(ResultVoidTest, FailureBoolConversionIsFalse) { +TEST(ResultTest, Void_FailureBoolConversionIsFalse) { auto r = Result::failure(SimpleError{5, "void fail"}); EXPECT_FALSE(r); } -TEST(ResultVoidTest, FailureErrorMatchesInput) { +TEST(ResultTest, Void_FailureErrorMatchesInput) { auto r = Result::failure(SimpleError{7, "nope"}); EXPECT_EQ(r.error().code, 7); EXPECT_EQ(r.error().message, "nope"); } -TEST(ResultVoidTest, FailureMoveError) { +TEST(ResultTest, Void_FailureMoveError) { auto r = Result::failure("void error"); auto msg = std::move(r).error(); EXPECT_EQ(msg, "void error"); diff --git a/src/tests/unit/test_room.cpp b/src/tests/unit/test_room.cpp new file mode 100644 index 00000000..3bf896c3 --- /dev/null +++ b/src/tests/unit/test_room.cpp @@ -0,0 +1,101 @@ +/* + * Copyright 2025 LiveKit + * + * 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. + */ + +#include +#include + +namespace livekit::test { + +class RoomTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(RoomTest, ConnectWithoutInitialize) { + // Test fixture initializes by default, do this to emulate lack of initialization + livekit::shutdown(); + + Room room; + bool result = room.Connect("wss://localhost:7880", "test", livekit::RoomOptions()); + EXPECT_FALSE(result) << "Connecting without initializing should return false"; +} + +TEST_F(RoomTest, CreateRoom) { + Room room; + // Room should be created without issues + EXPECT_EQ(room.localParticipant(), nullptr) << "Local participant should be null before connect"; +} + +TEST_F(RoomTest, RoomOptionsDefaults) { + RoomOptions options; + + EXPECT_TRUE(options.auto_subscribe) << "auto_subscribe should default to true"; + EXPECT_FALSE(options.dynacast) << "dynacast should default to false"; + EXPECT_FALSE(options.rtc_config.has_value()) << "rtc_config should not have a value by default"; + EXPECT_FALSE(options.encryption.has_value()) << "encryption should not have a value by default"; +} + +TEST_F(RoomTest, RtcConfigDefaults) { + RtcConfig config; + + EXPECT_EQ(config.ice_transport_type, 0); + EXPECT_EQ(config.continual_gathering_policy, 0); + EXPECT_TRUE(config.ice_servers.empty()); +} + +TEST_F(RoomTest, IceServerConfiguration) { + IceServer server; + server.url = "stun:stun.l.google.com:19302"; + server.username = "user"; + server.credential = "pass"; + + EXPECT_EQ(server.url, "stun:stun.l.google.com:19302"); + EXPECT_EQ(server.username, "user"); + EXPECT_EQ(server.credential, "pass"); +} + +TEST_F(RoomTest, RoomWithCustomRtcConfig) { + RoomOptions options; + options.auto_subscribe = false; + options.dynacast = true; + + RtcConfig rtc_config; + rtc_config.ice_servers.push_back({"stun:stun.l.google.com:19302", "", ""}); + rtc_config.ice_servers.push_back({"turn:turn.example.com:3478", "user", "pass"}); + + options.rtc_config = rtc_config; + + EXPECT_FALSE(options.auto_subscribe); + EXPECT_TRUE(options.dynacast); + EXPECT_TRUE(options.rtc_config.has_value()); + EXPECT_EQ(options.rtc_config->ice_servers.size(), 2); +} + +TEST_F(RoomTest, RemoteParticipantsEmptyBeforeConnect) { + Room room; + auto participants = room.remoteParticipants(); + EXPECT_TRUE(participants.empty()) << "Remote participants should be empty before connect"; +} + +TEST_F(RoomTest, RemoteParticipantLookupBeforeConnect) { + Room room; + auto participant = room.remoteParticipant("nonexistent"); + EXPECT_EQ(participant, nullptr) << "Looking up participant before connect should return nullptr"; +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_room_event_types.cpp b/src/tests/unit/test_room_event_types.cpp new file mode 100644 index 00000000..db423142 --- /dev/null +++ b/src/tests/unit/test_room_event_types.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +namespace livekit::test { + +TEST(RoomEventTypesTest, EnumValuesAreReachable) { + EXPECT_NE(ConnectionState::Connected, ConnectionState::Disconnected); + EXPECT_NE(DataPacketKind::Reliable, DataPacketKind::Lossy); + EXPECT_NE(EncryptionState::New, EncryptionState::Ok); + EXPECT_NE(DisconnectReason::Unknown, DisconnectReason::ClientInitiated); + EXPECT_NE(ConnectionQuality::Poor, ConnectionQuality::Excellent); +} + +TEST(RoomEventTypesTest, RoomInfoDataDefaults) { + RoomInfoData info; + EXPECT_TRUE(info.name.empty()); + EXPECT_TRUE(info.metadata.empty()); + EXPECT_FALSE(info.active_recording); + EXPECT_EQ(info.creation_time, 0); +} + +TEST(RoomEventTypesTest, AttributeEntryConstruction) { + AttributeEntry entry("k", "v"); + EXPECT_EQ(entry.key, "k"); + EXPECT_EQ(entry.value, "v"); +} + +TEST(RoomEventTypesTest, TrackPublishOptionsDefaults) { + TrackPublishOptions options; + EXPECT_FALSE(options.packet_trailer_features.user_timestamp); + EXPECT_FALSE(options.packet_trailer_features.frame_id); +} + +TEST(RoomEventTypesTest, UserPacketDataDefaults) { + UserPacketData packet; + EXPECT_TRUE(packet.data.empty()); + EXPECT_FALSE(packet.topic.has_value()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_rpc_error.cpp b/src/tests/unit/test_rpc_error.cpp new file mode 100644 index 00000000..02f020e8 --- /dev/null +++ b/src/tests/unit/test_rpc_error.cpp @@ -0,0 +1,53 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +namespace livekit::test { + +TEST(RpcErrorTest, NumericConstructor) { + RpcError err(1234, "boom", "{\"detail\":\"x\"}"); + EXPECT_EQ(err.code(), 1234u); + EXPECT_EQ(err.message(), "boom"); + EXPECT_EQ(err.data(), "{\"detail\":\"x\"}"); +} + +TEST(RpcErrorTest, EnumConstructor) { + RpcError err(RpcError::ErrorCode::APPLICATION_ERROR, "handler failed"); + EXPECT_EQ(err.code(), static_cast(RpcError::ErrorCode::APPLICATION_ERROR)); + EXPECT_EQ(err.message(), "handler failed"); + EXPECT_TRUE(err.data().empty()); +} + +TEST(RpcErrorTest, BuiltInFactory) { + RpcError err = RpcError::builtIn(RpcError::ErrorCode::CONNECTION_TIMEOUT); + EXPECT_EQ(err.code(), static_cast(RpcError::ErrorCode::CONNECTION_TIMEOUT)); + EXPECT_FALSE(err.message().empty()); + EXPECT_TRUE(err.data().empty()); +} + +TEST(RpcErrorTest, ThrowableAsRuntimeError) { + try { + throw RpcError(RpcError::ErrorCode::SEND_FAILED, "send failed"); + } catch (const std::runtime_error& e) { + SUCCEED() << "caught as std::runtime_error: " << e.what(); + return; + } + FAIL() << "RpcError did not propagate as std::runtime_error"; +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_stats.cpp b/src/tests/unit/test_stats.cpp new file mode 100644 index 00000000..434907e9 --- /dev/null +++ b/src/tests/unit/test_stats.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include + +#include "stats.pb.h" + +namespace livekit::test { + +TEST(StatsTest, ScalarFromProtoOverloads) { + proto::RtcStatsData data; + proto::CodecStats codec; + proto::RtpStreamStats rtp; + proto::ReceivedRtpStreamStats received; + proto::InboundRtpStreamStats inbound; + proto::SentRtpStreamStats sent; + proto::OutboundRtpStreamStats outbound; + proto::RemoteInboundRtpStreamStats remote_inbound; + proto::RemoteOutboundRtpStreamStats remote_outbound; + proto::MediaSourceStats media_source; + proto::AudioSourceStats audio_source; + proto::VideoSourceStats video_source; + proto::AudioPlayoutStats audio_playout; + proto::PeerConnectionStats peer; + proto::DataChannelStats data_channel; + proto::TransportStats transport; + proto::CandidatePairStats candidate_pair; + proto::IceCandidateStats ice_candidate; + proto::CertificateStats certificate; + proto::StreamStats stream; + + (void)fromProto(data); + (void)fromProto(codec); + (void)fromProto(rtp); + (void)fromProto(received); + (void)fromProto(inbound); + (void)fromProto(sent); + (void)fromProto(outbound); + (void)fromProto(remote_inbound); + (void)fromProto(remote_outbound); + (void)fromProto(media_source); + (void)fromProto(audio_source); + (void)fromProto(video_source); + (void)fromProto(audio_playout); + (void)fromProto(peer); + (void)fromProto(data_channel); + (void)fromProto(transport); + (void)fromProto(candidate_pair); + (void)fromProto(ice_candidate); + (void)fromProto(certificate); + (void)fromProto(stream); +} + +TEST(StatsTest, HighLevelRtcStatsFromProto) { + proto::RtcStats rtc; + RtcStats stats = fromProto(rtc); + (void)stats; +} + +TEST(StatsTest, RepeatedRtcStatsFromProto) { + std::vector protos(2); + std::vector stats = fromProto(protos); + EXPECT_EQ(stats.size(), 2u); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_streams.cpp b/src/tests/unit/test_streams.cpp new file mode 100644 index 00000000..114d8c47 --- /dev/null +++ b/src/tests/unit/test_streams.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include +#include +#include +#include + +#include + +namespace livekit::test { + +TEST(StreamOptionsTest, AudioStreamOptionsDefaults) { + AudioStream::Options options; + EXPECT_EQ(options.capacity, 0u); + EXPECT_TRUE(options.noise_cancellation_module.empty()); + EXPECT_TRUE(options.noise_cancellation_options_json.empty()); +} + +TEST(StreamOptionsTest, VideoStreamOptionsDefaults) { + VideoStream::Options options; + EXPECT_EQ(options.capacity, 0u); + EXPECT_EQ(options.format, VideoBufferType::RGBA); +} + +TEST(StreamOptionsTest, DataTrackStreamOptionsDefaults) { + DataTrackStream::Options options; + EXPECT_FALSE(options.buffer_size.has_value()); +} + +TEST(StreamOptionsTest, AudioFrameEventIsConstructible) { + AudioFrameEvent ev; + (void)ev; + SUCCEED(); +} + +TEST(StreamOptionsTest, VideoFrameEventIsConstructible) { + VideoFrameEvent ev; + ev.timestamp_us = 0; + ev.rotation = VideoRotation::VIDEO_ROTATION_0; + EXPECT_FALSE(ev.metadata.has_value()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_video_frame.cpp b/src/tests/unit/test_video_frame.cpp new file mode 100644 index 00000000..5849ea1b --- /dev/null +++ b/src/tests/unit/test_video_frame.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include + +#include +#include + +namespace livekit::test { + +TEST(VideoFrameTest, DefaultConstructed) { + VideoFrame frame; + EXPECT_EQ(frame.dataSize(), 0u); +} + +TEST(VideoFrameTest, ConstructFromBuffer) { + std::vector data(std::size_t{16} * 16 * 4, 0xAB); + VideoFrame frame(16, 16, VideoBufferType::RGBA, std::move(data)); + EXPECT_EQ(frame.width(), 16); + EXPECT_EQ(frame.height(), 16); + EXPECT_EQ(frame.type(), VideoBufferType::RGBA); + EXPECT_EQ(frame.dataSize(), 16u * 16u * 4u); +} + +TEST(VideoFrameTest, FactoryCreate) { + VideoFrame frame = VideoFrame::create(8, 8, VideoBufferType::ARGB); + EXPECT_EQ(frame.width(), 8); + EXPECT_EQ(frame.height(), 8); + EXPECT_EQ(frame.type(), VideoBufferType::ARGB); + EXPECT_GT(frame.dataSize(), 0u); + + const auto planes = frame.planeInfos(); + EXPECT_FALSE(planes.empty()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_video_source.cpp b/src/tests/unit/test_video_source.cpp new file mode 100644 index 00000000..c2d9419b --- /dev/null +++ b/src/tests/unit/test_video_source.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#include +#include +#include + +namespace livekit::test { + +class VideoSourceTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(VideoSourceTest, ConstructAndQueryProperties) { + VideoSource source(640, 480); + EXPECT_EQ(source.width(), 640); + EXPECT_EQ(source.height(), 480); + EXPECT_NE(source.ffi_handle_id(), 0u); +} + +TEST_F(VideoSourceTest, VideoCaptureOptionsDefaults) { + VideoCaptureOptions options; + EXPECT_EQ(options.timestamp_us, 0); + EXPECT_EQ(options.rotation, VideoRotation::VIDEO_ROTATION_0); + EXPECT_FALSE(options.metadata.has_value()); +} + +} // namespace livekit::test