1mod consumer;
29mod decoder;
30mod encoder;
31mod producer;
32
33pub use consumer::Consumer;
34pub use decoder::{ConsumerConfig, Decoder};
35pub use encoder::{Encoder, Pending, ProducerConfig};
36pub use producer::Producer;
37
38#[cfg(test)]
39mod test {
40 use std::task::Poll;
41
42 use serde_json::{Value, json};
43
44 use super::*;
45
46 fn producer(config: ProducerConfig) -> (Producer<Value>, moq_net::track::Subscriber) {
47 let track = moq_net::broadcast::Info::new()
48 .produce()
49 .create_track("test", None)
50 .unwrap();
51 let consumer = track.subscribe(None);
52 (Producer::new(track, config), consumer)
53 }
54
55 fn compressed() -> ProducerConfig {
56 ProducerConfig::default().with_compression(true)
57 }
58
59 fn consumer(track: moq_net::track::Subscriber, compression: bool) -> Consumer<Value> {
60 Consumer::new(track, ConsumerConfig::default().with_compression(compression))
61 }
62
63 fn drain(mut consumer: Consumer<Value>) -> Vec<Value> {
65 let waiter = kio::Waiter::noop();
66 let mut out = Vec::new();
67 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
68 out.push(value);
69 }
70 out
71 }
72
73 #[test]
74 fn plaintext_roundtrip_in_order() {
75 let (mut producer, track) = producer(ProducerConfig::default());
76 for n in 0..5 {
77 producer.append(&json!({ "n": n })).unwrap();
78 }
79 producer.finish().unwrap();
80
81 let records = drain(consumer(track, false));
82 assert_eq!(records, (0..5).map(|n| json!({ "n": n })).collect::<Vec<_>>());
83 }
84
85 #[test]
86 fn compressed_roundtrip_in_order() {
87 let (mut producer, track) = producer(compressed());
88 for n in 0..20 {
89 producer.append(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
90 }
91 producer.finish().unwrap();
92
93 let records = drain(consumer(track, true));
94 assert_eq!(records.len(), 20);
95 assert_eq!(records[7], json!({ "group": 7, "pts": 14_000 }));
96 }
97
98 #[test]
99 fn all_records_ride_one_group() {
100 let (mut producer, track) = producer(compressed());
101 for n in 0..50 {
102 producer.append(&json!({ "n": n })).unwrap();
103 }
104 producer.finish().unwrap();
105
106 assert_eq!(track.latest(), Some(0));
108 assert_eq!(drain(consumer(track, true)).len(), 50);
109 }
110
111 #[test]
112 fn live_consumer_sees_each_record() {
113 let (mut producer, track) = producer(compressed());
114 let mut consumer = consumer(track, true);
115 let waiter = kio::Waiter::noop();
116
117 for n in 0..3 {
118 producer.append(&json!({ "n": n })).unwrap();
119 match consumer.poll_next(&waiter) {
120 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "n": n })),
121 other => panic!("expected record, got {other:?}"),
122 }
123 }
124 assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
125 producer.finish().unwrap();
126 }
127
128 #[test]
129 fn shared_window_shrinks_repetitive_records() {
130 let (mut producer, mut track) = producer(compressed());
131 for n in 0..8 {
132 producer.append(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
133 }
134 producer.finish().unwrap();
135
136 let waiter = kio::Waiter::noop();
137 let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
138 panic!("expected a group");
139 };
140 let mut sizes = Vec::new();
141 while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
142 sizes.push(frame.payload.len());
143 }
144 assert_eq!(sizes.len(), 8);
145 let raw = serde_json::to_vec(&json!({ "group": 7, "pts": 14_000 })).unwrap().len();
146 assert!(
147 *sizes.last().unwrap() < raw / 2,
148 "windowed record {} should be far below its raw size {raw}",
149 sizes.last().unwrap()
150 );
151 }
152
153 #[test]
156 fn a_rejected_record_does_not_open_a_group() {
157 let track = moq_net::broadcast::Info::new()
159 .produce()
160 .create_track("test", None)
161 .unwrap();
162 let subscriber = track.subscribe(None);
163 let mut producer = Producer::<std::collections::BTreeMap<(u8, u8), u8>>::new(track, ProducerConfig::default());
164
165 let mut bad = std::collections::BTreeMap::new();
166 bad.insert((1, 2), 3);
167 assert!(producer.append(&bad).is_err());
168
169 assert_eq!(subscriber.latest(), None, "a rejected record opened a group");
170 }
171
172 fn rejecting_track() -> moq_net::track::Producer {
177 let mut info = moq_net::track::Info::default();
178 info.timescale = moq_net::Timescale::new((1u64 << 62) - 1).unwrap();
179
180 moq_net::broadcast::Info::new()
181 .produce()
182 .create_track("test", Some(info))
183 .unwrap()
184 }
185
186 #[test]
189 fn a_rejected_record_does_not_strand_an_empty_group() {
190 let track = rejecting_track();
191 let mut subscriber = track.subscribe(None);
192 let mut producer = Producer::<Value>::new(track, ProducerConfig::default());
193
194 assert!(producer.append(&json!({ "n": 1 })).is_err());
195
196 let waiter = kio::Waiter::noop();
197 let Poll::Ready(Ok(Some(mut group))) = subscriber.poll_next_group(&waiter) else {
198 panic!("the group was published, so a subscriber sees it");
199 };
200 assert!(
201 matches!(group.poll_read_frame(&waiter), Poll::Ready(Ok(None))),
202 "the empty group must be closed, not left open for a subscriber to wait in"
203 );
204 }
205
206 #[test]
210 fn a_rejected_record_leaves_the_encoder_able_to_retry() {
211 let track = rejecting_track();
212 let mut producer = Producer::<Value>::new(track, ProducerConfig::default().with_compression(true));
213
214 assert!(matches!(producer.append(&json!({ "n": 1 })), Err(crate::Error::Net(_))));
215
216 assert!(
219 matches!(producer.append(&json!({ "n": 2 })), Err(crate::Error::Net(_))),
220 "the encoder latched a desync instead of retrying into a fresh group"
221 );
222 }
223
224 #[test]
225 fn embedded_newlines_survive() {
226 let (mut producer, track) = producer(compressed());
229 let value = json!({ "s": "line1\nline2\ttab", "u": "a\u{000a}b" });
230 for _ in 0..4 {
231 producer.append(&value).unwrap();
232 }
233 producer.finish().unwrap();
234
235 let records = drain(consumer(track, true));
236 assert_eq!(records, vec![value.clone(), value.clone(), value.clone(), value]);
237 }
238}