Skip to content

Commit a0b7602

Browse files
Additional FfiClient tests for error states
1 parent 59276e3 commit a0b7602

2 files changed

Lines changed: 54 additions & 129 deletions

File tree

src/ffi_client.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ void FfiClient::RemoveListener(ListenerId id) {
188188

189189
proto::FfiResponse FfiClient::sendRequest(const proto::FfiRequest& request) const {
190190
// 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
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
192193
if (!isInitialized()) {
193194
throw std::runtime_error("FfiClient::sendRequest failed: LiveKit is not initialized");
194195
}

src/tests/unit/test_ffi_client.cpp

Lines changed: 52 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
/// @file test_ffi_client.cpp
18-
/// @brief Unit tests for FfiClient (the internal singleton that bridges the
19-
/// C++ SDK to the Rust FFI runtime).
20-
///
21-
/// Scope:
22-
/// - Singleton identity
23-
/// - initialize() / shutdown() / isInitialized() state machine
24-
/// - AddListener() / RemoveListener() lifecycle and ID uniqueness
25-
/// - sendRequest() invariants that are reachable without a live FFI
26-
/// roundtrip (empty-serialization failure path)
27-
///
28-
/// Out of scope (covered by integration tests with a live Rust runtime):
29-
/// - Real sendRequest()/sendRequestAsync() responses
30-
/// - Listener invocation from the FFI callback thread
31-
///
32-
/// DISABLED_ tests in this file are stubs for the "isInitialized() gate"
33-
/// design proposal (see project notes). Strip the DISABLED_ prefix once
34-
/// the corresponding gate lands in production.
35-
3617
#include <gtest/gtest.h>
3718
#include <livekit/livekit.h>
3819

@@ -139,7 +120,7 @@ TEST_F(FfiClientTest, AddListenerReturnsUniqueIds) {
139120
}
140121

141122
TEST_F(FfiClientTest, RemoveListenerWithUnknownIdIsSafe) {
142-
EXPECT_NO_THROW(FfiClient::instance().RemoveListener(/*never registered=*/424242));
123+
EXPECT_NO_THROW(FfiClient::instance().RemoveListener(424242));
143124
}
144125

145126
TEST_F(FfiClientTest, RemoveListenerIsIdempotent) {
@@ -160,7 +141,7 @@ TEST_F(FfiClientTest, ListenerRegistrationSurvivesShutdownReinitCycle) {
160141
}
161142

162143
// ---------------------------------------------------------------------------
163-
// sendRequest() — invariants reachable without a live FFI
144+
// These tests ensure FfiClient methods throw in various error conditions
164145
// ---------------------------------------------------------------------------
165146

166147
TEST_F(FfiClientTest, SendRequestThrowsOnEmptyRequest) {
@@ -171,18 +152,7 @@ TEST_F(FfiClientTest, SendRequestThrowsOnEmptyRequest) {
171152
EXPECT_THROW(FfiClient::instance().sendRequest(req), std::runtime_error);
172153
}
173154

174-
TEST_F(FfiClientTest, SendRequestWhenNotInitialized) {
175-
ASSERT_FALSE(FfiClient::instance().isInitialized());
176-
177-
proto::FfiRequest req;
178-
// Populate any oneof so we get past the empty-bytes serialization check
179-
// and hit the proposed init gate instead.
180-
(void)req.mutable_dispose();
181-
182-
// EXPECT_THROW(FfiClient::instance().sendRequest(req), std::runtime_error);
183-
}
184-
185-
TEST_F(FfiClientTest, DISABLED_SendRequestThrowsAfterShutdown) {
155+
TEST_F(FfiClientTest, SendRequestThrowsAfterShutdown) {
186156
ASSERT_TRUE(FfiClient::instance().initialize(false));
187157
FfiClient::instance().shutdown();
188158
ASSERT_FALSE(FfiClient::instance().isInitialized());
@@ -193,146 +163,100 @@ TEST_F(FfiClientTest, DISABLED_SendRequestThrowsAfterShutdown) {
193163
EXPECT_THROW(FfiClient::instance().sendRequest(req), std::runtime_error);
194164
}
195165

196-
// ---------------------------------------------------------------------------
197-
// PROPOSED: *Async helper isInitialized() gates (Tier 1 #3)
198-
//
199-
// Each async helper (connectAsync, publishTrackAsync, captureAudioFrameAsync,
200-
// publishDataAsync, performRpcAsync, publishDataTrackAsync, etc.) should
201-
// surface "not initialized" through its return channel:
202-
// - std::future<T> helpers: the future should resolve with an exception
203-
// (fut.get() throws std::runtime_error).
204-
// - Result<T, E> helpers (subscribeDataTrack, publishDataTrackAsync's
205-
// Result payload): the Result should be a failure with an appropriate
206-
// error code.
207-
//
208-
// Each stub below pins down the *contract* for one helper. They are kept
209-
// DISABLED for the same reason as sendRequest above: without the gate, the
210-
// helper either calls into Rust unsafely or registers a pending waiter that
211-
// will never complete.
212-
//
213-
// As you add each gate, strip the DISABLED_ prefix one test at a time.
214-
// ---------------------------------------------------------------------------
215-
216-
TEST_F(FfiClientTest, DISABLED_ConnectAsyncFailsWhenNotInitialized) {
166+
TEST_F(FfiClientTest, NotInitialized_ConnectAsyncThrows) {
217167
ASSERT_FALSE(FfiClient::instance().isInitialized());
218168

219169
RoomOptions options;
220-
auto fut = FfiClient::instance().connectAsync("wss://localhost:7880", "fake-token", options);
221-
EXPECT_THROW(fut.get(), std::runtime_error);
170+
EXPECT_THROW(FfiClient::instance().connectAsync("wss://localhost:7880", "fake-token", options), std::runtime_error);
222171
}
223172

224-
TEST_F(FfiClientTest, DISABLED_PublishTrackAsyncFailsWhenNotInitialized) {
173+
TEST_F(FfiClientTest, NotInitialized_PublishTrackAsyncThrows) {
225174
ASSERT_FALSE(FfiClient::instance().isInitialized());
226175

227-
// Handle values are placeholders — the gate must fail before they are
228-
// ever forwarded to Rust.
229-
// TrackPublishOptions opts;
230-
// auto fut = FfiClient::instance().publishTrackAsync(/*participant=*/1, /*track=*/2, opts);
231-
// EXPECT_THROW(fut.get(), std::runtime_error);
232-
GTEST_SKIP() << "TODO: enable once publishTrackAsync gate lands";
176+
TrackPublishOptions options;
177+
EXPECT_THROW(FfiClient::instance().publishTrackAsync(1, 2, options), std::runtime_error);
233178
}
234179

235-
TEST_F(FfiClientTest, DISABLED_UnpublishTrackAsyncFailsWhenNotInitialized) {
180+
TEST_F(FfiClientTest, NotInitialized_UnpublishTrackAsyncThrows) {
236181
ASSERT_FALSE(FfiClient::instance().isInitialized());
237-
// auto fut = FfiClient::instance().unpublishTrackAsync(/*participant=*/1, "sid", true);
238-
// EXPECT_THROW(fut.get(), std::runtime_error);
239-
GTEST_SKIP() << "TODO: enable once unpublishTrackAsync gate lands";
182+
183+
EXPECT_THROW(FfiClient::instance().unpublishTrackAsync(1, "sid", true), std::runtime_error);
240184
}
241185

242-
TEST_F(FfiClientTest, DISABLED_PublishDataAsyncFailsWhenNotInitialized) {
186+
TEST_F(FfiClientTest, NotInitialized_PublishDataAsyncThrows) {
243187
ASSERT_FALSE(FfiClient::instance().isInitialized());
244-
// const std::uint8_t payload[1] = {0};
245-
// auto fut = FfiClient::instance().publishDataAsync(
246-
// /*participant=*/1, payload, 1, /*reliable=*/true, /*destinations=*/{}, /*topic=*/"");
247-
// EXPECT_THROW(fut.get(), std::runtime_error);
248-
GTEST_SKIP() << "TODO: enable once publishDataAsync gate lands";
188+
189+
const std::uint8_t payload[1] = {0};
190+
EXPECT_THROW(FfiClient::instance().publishDataAsync(1, payload, 1, true, {}, ""), std::runtime_error);
249191
}
250192

251-
TEST_F(FfiClientTest, DISABLED_PublishSipDtmfAsyncFailsWhenNotInitialized) {
193+
TEST_F(FfiClientTest, NotInitialized_PublishSipDtmfAsyncThrows) {
252194
ASSERT_FALSE(FfiClient::instance().isInitialized());
253-
// auto fut = FfiClient::instance().publishSipDtmfAsync(/*participant=*/1, /*code=*/1, "1", {});
254-
// EXPECT_THROW(fut.get(), std::runtime_error);
255-
GTEST_SKIP() << "TODO: enable once publishSipDtmfAsync gate lands";
195+
196+
EXPECT_THROW(FfiClient::instance().publishSipDtmfAsync(1, 1, "1", {}), std::runtime_error);
256197
}
257198

258-
TEST_F(FfiClientTest, DISABLED_SetLocalMetadataAsyncFailsWhenNotInitialized) {
199+
TEST_F(FfiClientTest, NotInitialized_SetLocalMetadataAsyncThrows) {
259200
ASSERT_FALSE(FfiClient::instance().isInitialized());
260-
// auto fut = FfiClient::instance().setLocalMetadataAsync(/*participant=*/1, "metadata");
261-
// EXPECT_THROW(fut.get(), std::runtime_error);
262-
GTEST_SKIP() << "TODO: enable once setLocalMetadataAsync gate lands";
201+
202+
EXPECT_THROW(FfiClient::instance().setLocalMetadataAsync(1, "metadata"), std::runtime_error);
263203
}
264204

265-
TEST_F(FfiClientTest, DISABLED_CaptureAudioFrameAsyncFailsWhenNotInitialized) {
205+
TEST_F(FfiClientTest, NotInitialized_CaptureAudioFrameAsyncThrows) {
266206
ASSERT_FALSE(FfiClient::instance().isInitialized());
267-
// proto::AudioFrameBufferInfo buf;
268-
// auto fut = FfiClient::instance().captureAudioFrameAsync(/*source=*/1, buf);
269-
// EXPECT_THROW(fut.get(), std::runtime_error);
270-
GTEST_SKIP() << "TODO: enable once captureAudioFrameAsync gate lands";
207+
208+
proto::AudioFrameBufferInfo buf;
209+
EXPECT_THROW(FfiClient::instance().captureAudioFrameAsync(1, buf), std::runtime_error);
271210
}
272211

273-
TEST_F(FfiClientTest, DISABLED_PerformRpcAsyncFailsWhenNotInitialized) {
212+
TEST_F(FfiClientTest, NotInitialized_PerformRpcAsyncThrows) {
274213
ASSERT_FALSE(FfiClient::instance().isInitialized());
275-
// auto fut = FfiClient::instance().performRpcAsync(
276-
// /*participant=*/1, "dest", "method", "payload", std::nullopt);
277-
// EXPECT_THROW(fut.get(), std::runtime_error);
278-
GTEST_SKIP() << "TODO: enable once performRpcAsync gate lands";
214+
215+
EXPECT_THROW(FfiClient::instance().performRpcAsync(1, "dest", "method", "payload", std::nullopt), std::runtime_error);
279216
}
280217

281-
TEST_F(FfiClientTest, DISABLED_GetTrackStatsAsyncFailsWhenNotInitialized) {
218+
TEST_F(FfiClientTest, NotInitialized_GetTrackStatsAsyncThrows) {
282219
ASSERT_FALSE(FfiClient::instance().isInitialized());
283-
// auto fut = FfiClient::instance().getTrackStatsAsync(/*track=*/1);
284-
// EXPECT_THROW(fut.get(), std::runtime_error);
285-
GTEST_SKIP() << "TODO: enable once getTrackStatsAsync gate lands";
220+
221+
EXPECT_THROW(FfiClient::instance().getTrackStatsAsync(1), std::runtime_error);
286222
}
287223

288-
TEST_F(FfiClientTest, DISABLED_PublishDataTrackAsyncReturnsFailureWhenNotInitialized) {
224+
TEST_F(FfiClientTest, NotInitialized_PublishDataTrackAsyncFails) {
289225
ASSERT_FALSE(FfiClient::instance().isInitialized());
290226

291-
// publishDataTrackAsync's future yields a Result<..., PublishDataTrackError>,
292-
// so the convention here is "failure result" rather than a thrown exception.
293-
//
294-
// auto fut = FfiClient::instance().publishDataTrackAsync(/*participant=*/1, "name");
295-
// auto result = fut.get();
296-
// EXPECT_FALSE(result.ok());
297-
// EXPECT_EQ(result.error().code, PublishDataTrackErrorCode::INVALID_HANDLE); // or NOT_INITIALIZED
298-
GTEST_SKIP() << "TODO: enable once publishDataTrackAsync gate lands; "
299-
"decide between INVALID_HANDLE and a new NOT_INITIALIZED code";
227+
auto fut_result = FfiClient::instance().publishDataTrackAsync(1, "name");
228+
auto result = fut_result.get();
229+
EXPECT_FALSE(result.ok());
230+
EXPECT_EQ(result.error().code, PublishDataTrackErrorCode::INTERNAL);
300231
}
301232

302-
TEST_F(FfiClientTest, DISABLED_SubscribeDataTrackReturnsFailureWhenNotInitialized) {
233+
TEST_F(FfiClientTest, NotInitialized_SubscribeDataTrackFails) {
303234
ASSERT_FALSE(FfiClient::instance().isInitialized());
304235

305-
// subscribeDataTrack returns Result<..., SubscribeDataTrackError> directly.
306-
//
307-
// auto result = FfiClient::instance().subscribeDataTrack(/*track=*/1);
308-
// EXPECT_FALSE(result.ok());
309-
// EXPECT_EQ(result.error().code, SubscribeDataTrackErrorCode::INVALID_HANDLE); // or NOT_INITIALIZED
310-
GTEST_SKIP() << "TODO: enable once subscribeDataTrack gate lands; "
311-
"decide between INVALID_HANDLE and a new NOT_INITIALIZED code";
236+
auto result = FfiClient::instance().subscribeDataTrack(1);
237+
EXPECT_FALSE(result.ok());
238+
EXPECT_EQ(result.error().code, SubscribeDataTrackErrorCode::INTERNAL);
312239
}
313240

314-
TEST_F(FfiClientTest, DISABLED_SendStreamHeaderAsyncFailsWhenNotInitialized) {
241+
TEST_F(FfiClientTest, NotInitialized_SendStreamHeaderAsyncThrows) {
315242
ASSERT_FALSE(FfiClient::instance().isInitialized());
316-
// proto::DataStream::Header header;
317-
// auto fut = FfiClient::instance().sendStreamHeaderAsync(/*participant=*/1, header, {}, "sender");
318-
// EXPECT_THROW(fut.get(), std::runtime_error);
319-
GTEST_SKIP() << "TODO: enable once sendStreamHeaderAsync gate lands";
243+
244+
proto::DataStream::Header header;
245+
EXPECT_THROW(FfiClient::instance().sendStreamHeaderAsync(1, header, {}, "sender"), std::runtime_error);
320246
}
321247

322-
TEST_F(FfiClientTest, DISABLED_SendStreamChunkAsyncFailsWhenNotInitialized) {
248+
TEST_F(FfiClientTest, NotInitialized_SendStreamChunkAsyncThrows) {
323249
ASSERT_FALSE(FfiClient::instance().isInitialized());
324-
// proto::DataStream::Chunk chunk;
325-
// auto fut = FfiClient::instance().sendStreamChunkAsync(/*participant=*/1, chunk, {}, "sender");
326-
// EXPECT_THROW(fut.get(), std::runtime_error);
327-
GTEST_SKIP() << "TODO: enable once sendStreamChunkAsync gate lands";
250+
251+
proto::DataStream::Chunk chunk;
252+
EXPECT_THROW(FfiClient::instance().sendStreamChunkAsync(1, chunk, {}, "sender"), std::runtime_error);
328253
}
329254

330-
TEST_F(FfiClientTest, DISABLED_SendStreamTrailerAsyncFailsWhenNotInitialized) {
255+
TEST_F(FfiClientTest, NotInitialized_SendStreamTrailerAsyncThrows) {
331256
ASSERT_FALSE(FfiClient::instance().isInitialized());
332-
// proto::DataStream::Trailer trailer;
333-
// auto fut = FfiClient::instance().sendStreamTrailerAsync(/*participant=*/1, trailer, "sender");
334-
// EXPECT_THROW(fut.get(), std::runtime_error);
335-
GTEST_SKIP() << "TODO: enable once sendStreamTrailerAsync gate lands";
257+
258+
proto::DataStream::Trailer trailer;
259+
EXPECT_THROW(FfiClient::instance().sendStreamTrailerAsync(1, trailer, "sender"), std::runtime_error);
336260
}
337261

338262
} // namespace livekit::test

0 commit comments

Comments
 (0)