Skip to content

Commit ab8df3a

Browse files
tests
1 parent 0fb1cc5 commit ab8df3a

1 file changed

Lines changed: 149 additions & 38 deletions

File tree

‎src/tests/unit/test_subscription_thread_dispatcher.cpp‎

Lines changed: 149 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ class FakeMediaTrack : public Track {
4747
: Track(FfiHandle(0), std::move(sid), "track", kind, StreamState::STATE_ACTIVE, false, true) {}
4848
};
4949

50+
/// Minimal frames used to invoke a stored callback directly, so tests can prove
51+
/// which callback a registration slot actually holds.
52+
AudioFrame makeAudioFrame() { return AudioFrame::create(48000, 1, 480); }
53+
VideoFrame makeVideoFrame() { return VideoFrame::create(16, 16, VideoBufferType::RGBA); }
54+
5055
template <typename Predicate>
5156
bool waitFor(Predicate predicate, std::chrono::milliseconds timeout) {
5257
const auto start = std::chrono::steady_clock::now();
@@ -233,23 +238,54 @@ TEST_F(SubscriptionThreadDispatcherTest, ClearNonExistentCallbackIsNoOp) {
233238
EXPECT_NO_THROW(dispatcher.clearOnVideoFrameCallback("nobody", "missing"));
234239
}
235240

236-
TEST_F(SubscriptionThreadDispatcherTest, OverwriteAudioCallbackKeepsSingleEntry) {
241+
TEST_F(SubscriptionThreadDispatcherTest, OverwriteAudioCallbackStoresTheNewCallback) {
237242
SubscriptionThreadDispatcher dispatcher;
238-
std::atomic<int> counter1{0};
239-
std::atomic<int> counter2{0};
243+
std::atomic<int> first{0};
244+
std::atomic<int> second{0};
240245

241-
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&counter1](const AudioFrame&) { counter1++; });
242-
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&counter2](const AudioFrame&) { counter2++; });
246+
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&first](const AudioFrame&) { first++; });
247+
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&second](const AudioFrame&) { second++; });
243248

244249
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u) << "Re-registering with the same key should overwrite, not add";
250+
251+
// Invoke what the slot actually holds: size alone would not catch a setter
252+
// that tore down the reader but forgot to install the new callback.
253+
const CallbackKey key{"alice", "mic-main"};
254+
audioCallbacks(dispatcher)[key].callback(makeAudioFrame());
255+
EXPECT_EQ(first.load(), 0) << "The replaced callback must not be the one stored";
256+
EXPECT_EQ(second.load(), 1);
245257
}
246258

247-
TEST_F(SubscriptionThreadDispatcherTest, OverwriteVideoCallbackKeepsSingleEntry) {
259+
TEST_F(SubscriptionThreadDispatcherTest, OverwriteVideoCallbackStoresTheNewCallback) {
248260
SubscriptionThreadDispatcher dispatcher;
249-
dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {});
250-
dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {});
261+
std::atomic<int> first{0};
262+
std::atomic<int> second{0};
263+
264+
dispatcher.setOnVideoFrameCallback("alice", "cam-main", [&first](const VideoFrame&, std::int64_t) { first++; });
265+
dispatcher.setOnVideoFrameCallback("alice", "cam-main", [&second](const VideoFrame&, std::int64_t) { second++; });
251266

252267
EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u);
268+
269+
const CallbackKey key{"alice", "cam-main"};
270+
videoCallbacks(dispatcher)[key].legacy_callback(makeVideoFrame(), 0);
271+
EXPECT_EQ(first.load(), 0);
272+
EXPECT_EQ(second.load(), 1);
273+
}
274+
275+
TEST_F(SubscriptionThreadDispatcherTest, OverwriteAudioCallbackStoresTheNewStreamOptions) {
276+
SubscriptionThreadDispatcher dispatcher;
277+
AudioStream::Options first_opts;
278+
first_opts.capacity = 4;
279+
AudioStream::Options second_opts;
280+
second_opts.capacity = 32;
281+
282+
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}, first_opts);
283+
dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}, second_opts);
284+
285+
// The options travel with the callback into the next reader, so a stale copy
286+
// would silently rebuild the stream with the wrong queue behavior.
287+
const CallbackKey key{"alice", "mic-main"};
288+
EXPECT_EQ(audioCallbacks(dispatcher)[key].options.capacity, 32u);
253289
}
254290

255291
TEST_F(SubscriptionThreadDispatcherTest, MultipleDistinctCallbacksAreIndependent) {
@@ -664,11 +700,15 @@ TEST_F(SubscriptionThreadDispatcherTest, SetOnAudioWhileReaderActiveReplacesRegi
664700
activeReaders(dispatcher)[key].track_sid = "TR_audio_1";
665701
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
666702

667-
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
703+
std::atomic<int> replacement_invocations{0};
704+
dispatcher.setOnAudioFrameCallback("alice", "mic",
705+
[&replacement_invocations](const AudioFrame&) { replacement_invocations++; });
668706

669707
EXPECT_EQ(activeReaderCount(dispatcher), 0u) << "The stale reader must be extracted so it stops dispatching to the "
670708
"callback it captured by value";
671-
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u);
709+
ASSERT_EQ(audioCallbacks(dispatcher).size(), 1u);
710+
audioCallbacks(dispatcher)[key].callback(makeAudioFrame());
711+
EXPECT_EQ(replacement_invocations.load(), 1) << "The replacement callback must be the one now stored";
672712
}
673713

674714
TEST_F(SubscriptionThreadDispatcherTest, SetOnVideoWhileReaderActiveReplacesRegistrationAndStopsReader) {
@@ -679,10 +719,14 @@ TEST_F(SubscriptionThreadDispatcherTest, SetOnVideoWhileReaderActiveReplacesRegi
679719
activeReaders(dispatcher)[key].track_sid = "TR_video_1";
680720
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
681721

682-
dispatcher.setOnVideoFrameCallback("alice", "cam", [](const VideoFrame&, std::int64_t) {});
722+
std::atomic<int> replacement_invocations{0};
723+
dispatcher.setOnVideoFrameCallback(
724+
"alice", "cam", [&replacement_invocations](const VideoFrame&, std::int64_t) { replacement_invocations++; });
683725

684726
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
685-
EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u);
727+
ASSERT_EQ(videoCallbacks(dispatcher).size(), 1u);
728+
videoCallbacks(dispatcher)[key].legacy_callback(makeVideoFrame(), 0);
729+
EXPECT_EQ(replacement_invocations.load(), 1) << "The replacement callback must be the one now stored";
686730
}
687731

688732
TEST_F(SubscriptionThreadDispatcherTest, SetOnVideoEventWhileReaderActiveReplacesRegistrationAndStopsReader) {
@@ -727,6 +771,25 @@ TEST_F(SubscriptionThreadDispatcherTest, SetOnAudioAfterReplacementRestartsOnNex
727771
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u);
728772
}
729773

774+
TEST_F(SubscriptionThreadDispatcherTest, SetOnVideoAfterReplacementRestartsOnNextSubscribe) {
775+
SubscriptionThreadDispatcher dispatcher;
776+
dispatcher.setOnVideoFrameCallback("alice", "cam", [](const VideoFrame&, std::int64_t) {});
777+
778+
const CallbackKey key{"alice", "cam"};
779+
activeReaders(dispatcher)[key].track_sid = "TR_video_1";
780+
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
781+
782+
dispatcher.setOnVideoFrameCallback("alice", "cam", [](const VideoFrame&, std::int64_t) {});
783+
ASSERT_EQ(activeReaderCount(dispatcher), 0u);
784+
785+
// As in the audio case, the throw from VideoStream::fromTrack on the invalid
786+
// fake handle is the evidence that the guard no longer short-circuits startup.
787+
auto track = std::make_shared<FakeMediaTrack>("TR_video_1", TrackKind::KIND_VIDEO);
788+
EXPECT_ANY_THROW(dispatcher.handleTrackSubscribed("alice", "cam", track));
789+
790+
EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u);
791+
}
792+
730793
TEST_F(SubscriptionThreadDispatcherTest, SetOnAudioWithoutReplacementLeavesSidGuardIntact) {
731794
SubscriptionThreadDispatcher dispatcher;
732795
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
@@ -742,31 +805,94 @@ TEST_F(SubscriptionThreadDispatcherTest, SetOnAudioWithoutReplacementLeavesSidGu
742805
EXPECT_EQ(activeReaderCount(dispatcher), 1u);
743806
}
744807

745-
TEST_F(SubscriptionThreadDispatcherTest, SetOnAudioWithoutActiveReaderOverwritesRegistration) {
808+
// Distinct from ClearAudioCallbackRemovesRegistration, which clears a key that
809+
// has no reader: this covers clearing while a reader is active.
810+
TEST_F(SubscriptionThreadDispatcherTest, ClearAudioCallbackWithActiveReaderStopsReader) {
746811
SubscriptionThreadDispatcher dispatcher;
747812
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
748-
// No reader is active, so re-registering the same key simply overwrites the
749-
// stored callback.
750-
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
751-
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u);
813+
814+
const CallbackKey key{"alice", "mic"};
815+
activeReaders(dispatcher)[key].track_sid = "TR_audio_1";
816+
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
817+
818+
dispatcher.clearOnAudioFrameCallback("alice", "mic");
752819
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
820+
EXPECT_TRUE(audioCallbacks(dispatcher).empty());
753821
}
754822

755-
TEST_F(SubscriptionThreadDispatcherTest, ClearThenSetOnAudioRegistersNewCallback) {
823+
TEST_F(SubscriptionThreadDispatcherTest, ClearVideoCallbackWithActiveReaderStopsReader) {
824+
SubscriptionThreadDispatcher dispatcher;
825+
dispatcher.setOnVideoFrameCallback("alice", "cam", [](const VideoFrame&, std::int64_t) {});
826+
827+
const CallbackKey key{"alice", "cam"};
828+
activeReaders(dispatcher)[key].track_sid = "TR_video_1";
829+
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
830+
831+
dispatcher.clearOnVideoFrameCallback("alice", "cam");
832+
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
833+
EXPECT_TRUE(videoCallbacks(dispatcher).empty());
834+
}
835+
836+
// The reverse of SetOnVideoEventWhileReaderActiveReplacesRegistrationAndStopsReader:
837+
// the legacy setter must displace a stored event callback, not merge with it.
838+
TEST_F(SubscriptionThreadDispatcherTest, SetOnVideoDisplacesStoredEventCallback) {
839+
SubscriptionThreadDispatcher dispatcher;
840+
dispatcher.setOnVideoFrameEventCallback("alice", "cam", [](const VideoFrameEvent&) {});
841+
842+
const CallbackKey key{"alice", "cam"};
843+
activeReaders(dispatcher)[key].track_sid = "TR_video_1";
844+
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
845+
846+
dispatcher.setOnVideoFrameCallback("alice", "cam", [](const VideoFrame&, std::int64_t) {});
847+
848+
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
849+
ASSERT_EQ(videoCallbacks(dispatcher).size(), 1u);
850+
EXPECT_TRUE(static_cast<bool>(videoCallbacks(dispatcher)[key].legacy_callback));
851+
EXPECT_FALSE(static_cast<bool>(videoCallbacks(dispatcher)[key].event_callback));
852+
}
853+
854+
// Replacement must be scoped to its own key; an unrelated subscription's reader
855+
// is extracted by key, so a bug there would tear down the wrong stream.
856+
TEST_F(SubscriptionThreadDispatcherTest, ReplacementLeavesOtherKeysReadersUntouched) {
857+
SubscriptionThreadDispatcher dispatcher;
858+
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
859+
dispatcher.setOnAudioFrameCallback("bob", "mic", [](const AudioFrame&) {});
860+
861+
const CallbackKey alice{"alice", "mic"};
862+
const CallbackKey bob{"bob", "mic"};
863+
activeReaders(dispatcher)[alice].track_sid = "TR_audio_1";
864+
activeReaders(dispatcher)[bob].track_sid = "TR_audio_2";
865+
ASSERT_EQ(activeReaderCount(dispatcher), 2u);
866+
867+
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
868+
869+
EXPECT_EQ(activeReaderCount(dispatcher), 1u);
870+
EXPECT_EQ(activeReaders(dispatcher).count(alice), 0u);
871+
ASSERT_EQ(activeReaders(dispatcher).count(bob), 1u);
872+
EXPECT_EQ(activeReaders(dispatcher)[bob].track_sid, "TR_audio_2") << "Replacing one key must not disturb another";
873+
EXPECT_EQ(audioCallbacks(dispatcher).size(), 2u);
874+
}
875+
876+
// Unsubscribe stops the reader but keeps the registration, so a replacement made
877+
// while unsubscribed is the one that binds on the next subscribe.
878+
TEST_F(SubscriptionThreadDispatcherTest, ReplacementWhileUnsubscribedKeepsRegistrationForNextSubscribe) {
756879
SubscriptionThreadDispatcher dispatcher;
757880
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
758881

759882
const CallbackKey key{"alice", "mic"};
760883
activeReaders(dispatcher)[key].track_sid = "TR_audio_1";
761884
ASSERT_EQ(activeReaderCount(dispatcher), 1u);
762885

763-
// Clearing first is no longer required, but must still work.
764-
dispatcher.clearOnAudioFrameCallback("alice", "mic");
886+
dispatcher.handleTrackUnsubscribed("alice", TrackSource::SOURCE_MICROPHONE, "mic");
765887
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
766-
EXPECT_TRUE(audioCallbacks(dispatcher).empty());
888+
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u) << "Unsubscribe must preserve the registration";
767889

768-
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
769-
EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u);
890+
std::atomic<int> replacement_invocations{0};
891+
dispatcher.setOnAudioFrameCallback("alice", "mic",
892+
[&replacement_invocations](const AudioFrame&) { replacement_invocations++; });
893+
ASSERT_EQ(audioCallbacks(dispatcher).size(), 1u);
894+
audioCallbacks(dispatcher)[key].callback(makeAudioFrame());
895+
EXPECT_EQ(replacement_invocations.load(), 1);
770896
}
771897

772898
// ============================================================================
@@ -783,21 +909,6 @@ TEST_F(SubscriptionThreadDispatcherTest, IsSelfThreadIdentifiesTheCallingThread)
783909
EXPECT_FALSE(isSelfThread(other_id));
784910
}
785911

786-
TEST_F(SubscriptionThreadDispatcherTest, ReplacingReaderWithDefaultThreadIdDisposesCleanly) {
787-
SubscriptionThreadDispatcher dispatcher;
788-
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
789-
790-
// A fabricated reader carries a default-constructed thread_id and a
791-
// non-joinable thread; disposal must treat it as someone else's thread and
792-
// simply drop it rather than taking the self-join path.
793-
const CallbackKey key{"alice", "mic"};
794-
activeReaders(dispatcher)[key].track_sid = "TR_audio_1";
795-
ASSERT_EQ(activeReaders(dispatcher)[key].thread_id, std::thread::id{});
796-
797-
dispatcher.setOnAudioFrameCallback("alice", "mic", [](const AudioFrame&) {});
798-
EXPECT_EQ(activeReaderCount(dispatcher), 0u);
799-
}
800-
801912
// ============================================================================
802913
// SID deduplication: data reader start is skipped for the same SID and
803914
// replaced (stopping the previous reader) for a new SID

0 commit comments

Comments
 (0)