1pub mod consumer;
34mod decoder;
35mod encoder;
36pub mod producer;
37
38pub use consumer::Consumer;
39pub use decoder::Decoder;
40pub use encoder::{Config, Encoder, Pending};
41pub use producer::Producer;
42
43#[cfg(test)]
44mod test {
45 use std::task::Poll;
46
47 use serde_json::{Value, json};
48
49 use super::*;
50 use crate::Compression;
51
52 fn producer(config: Config) -> (Producer<Value>, moq_net::track::Subscriber) {
53 let track = moq_net::broadcast::Info::new()
54 .produce()
55 .create_track("test", None)
56 .unwrap();
57 let consumer = track.subscribe(None);
58 (Producer::new(track, config), consumer)
59 }
60
61 #[test]
63 fn demand_follows_subscribers() {
64 let (producer, consumer) = producer(Config::default());
65 let demand = producer.demand();
66 let waiter = kio::Waiter::noop();
67 assert!(matches!(demand.poll_used(&waiter), Poll::Ready(Ok(()))));
68
69 drop(consumer);
70 assert!(matches!(demand.poll_unused(&waiter), Poll::Ready(Ok(()))));
71 assert!(demand.poll_used(&waiter).is_pending());
72
73 let _consumer = producer.consume();
74 assert!(matches!(demand.poll_used(&waiter), Poll::Ready(Ok(()))));
75 assert!(demand.poll_unused(&waiter).is_pending());
76 }
77
78 fn compressed() -> Config {
79 Config {
80 compression: Compression::Deflate,
81 }
82 }
83
84 fn consume(track: moq_net::track::Subscriber, compression: bool) -> Consumer<Value> {
85 Consumer::new(
86 track,
87 Config {
88 compression: if compression {
89 Compression::Deflate
90 } else {
91 Compression::None
92 },
93 },
94 )
95 }
96
97 fn drain(mut consumer: Consumer<Value>) -> Vec<Value> {
99 let waiter = kio::Waiter::noop();
100 let mut out = Vec::new();
101 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
102 out.push(value);
103 }
104 out
105 }
106
107 #[test]
108 fn plaintext_roundtrip_in_order() {
109 let (mut producer, track) = producer(Config::default());
110 for n in 0..5 {
111 producer.append(&json!({ "n": n })).unwrap();
112 }
113 producer.finish().unwrap();
114
115 let records = drain(consume(track, false));
116 assert_eq!(records, (0..5).map(|n| json!({ "n": n })).collect::<Vec<_>>());
117 }
118
119 #[test]
120 fn compressed_roundtrip_in_order() {
121 let (mut producer, track) = producer(compressed());
122 for n in 0..20 {
123 producer.append(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
124 }
125 producer.finish().unwrap();
126
127 let records = drain(consume(track, true));
128 assert_eq!(records.len(), 20);
129 assert_eq!(records[7], json!({ "group": 7, "pts": 14_000 }));
130 }
131
132 #[test]
133 fn all_records_ride_one_group() {
134 let (mut producer, track) = producer(compressed());
135 for n in 0..50 {
136 producer.append(&json!({ "n": n })).unwrap();
137 }
138 producer.finish().unwrap();
139
140 assert_eq!(track.latest(), Some(0));
142 assert_eq!(drain(consume(track, true)).len(), 50);
143 }
144
145 #[test]
146 fn live_consumer_sees_each_record() {
147 let (mut producer, track) = producer(compressed());
148 let mut consumer = consume(track, true);
149 let waiter = kio::Waiter::noop();
150
151 for n in 0..3 {
152 producer.append(&json!({ "n": n })).unwrap();
153 match consumer.poll_next(&waiter) {
154 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "n": n })),
155 other => panic!("expected record, got {other:?}"),
156 }
157 }
158 assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
159 producer.finish().unwrap();
160 }
161
162 #[test]
163 fn shared_window_shrinks_repetitive_records() {
164 let (mut producer, track) = producer(compressed());
165 for n in 0..8 {
166 producer.append(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
167 }
168 producer.finish().unwrap();
169
170 let waiter = kio::Waiter::noop();
171 let mut track = track.ordered();
172 let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
173 panic!("expected a group");
174 };
175 let mut sizes = Vec::new();
176 while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
177 sizes.push(frame.payload.len());
178 }
179 assert_eq!(sizes.len(), 8);
180 let raw = serde_json::to_vec(&json!({ "group": 7, "pts": 14_000 })).unwrap().len();
181 assert!(
182 *sizes.last().unwrap() < raw / 2,
183 "windowed record {} should be far below its raw size {raw}",
184 sizes.last().unwrap()
185 );
186 }
187
188 #[test]
192 fn a_rejected_record_does_not_open_a_group() {
193 let track = moq_net::broadcast::Info::new()
195 .produce()
196 .create_track("test", None)
197 .unwrap();
198 let mut subscriber = track.subscribe(None);
199 let mut producer = Producer::<std::collections::BTreeMap<(u8, u8), u8>>::new(track, Config::default());
200
201 let mut bad = std::collections::BTreeMap::new();
202 bad.insert((1, 2), 3);
203 assert!(producer.append(&bad).is_err());
204
205 assert_eq!(subscriber.latest(), None, "a rejected record opened a group");
206
207 let waiter = kio::Waiter::noop();
208 assert!(
209 matches!(subscriber.poll_recv_group(&waiter), Poll::Ready(Err(_))),
210 "the log is missing a record, so the track must end rather than stay writable"
211 );
212 }
213
214 fn rejecting_track() -> moq_net::track::Producer {
219 let mut info = moq_net::track::Info::default();
220 info.timescale = moq_net::Timescale::new((1u64 << 62) - 1).unwrap();
221
222 moq_net::broadcast::Info::new()
223 .produce()
224 .create_track("test", Some(info))
225 .unwrap()
226 }
227
228 #[test]
232 fn a_failed_write_aborts_the_track() {
233 let track = rejecting_track();
234 let mut subscriber = track.subscribe(None);
235 let mut producer = Producer::<Value>::new(track, Config::default());
236
237 assert!(matches!(producer.append(&json!({ "n": 1 })), Err(crate::Error::Net(_))));
238
239 let waiter = kio::Waiter::noop();
240 assert!(
241 matches!(subscriber.poll_recv_group(&waiter), Poll::Ready(Err(_))),
242 "a truncated log must surface an error rather than read as a completed one"
243 );
244 }
245
246 #[test]
250 fn a_failed_write_ends_the_track() {
251 let track = rejecting_track();
252 let mut producer = Producer::<Value>::new(track, compressed());
253
254 assert!(matches!(producer.append(&json!({ "n": 1 })), Err(crate::Error::Net(_))));
255
256 assert!(
259 matches!(producer.append(&json!({ "n": 2 })), Err(crate::Error::Net(_))),
260 "a second append must fail on the ended track rather than open another group"
261 );
262
263 let waiter = kio::Waiter::noop();
266 assert!(matches!(
267 producer.consume().poll_recv_group(&waiter),
268 Poll::Ready(Err(_))
269 ));
270 }
271
272 #[test]
275 fn appending_after_finish_fails_without_aborting() {
276 let (mut producer, _track) = producer(compressed());
277 producer.append(&json!({ "n": 0 })).unwrap();
278 producer.finish().unwrap();
279
280 assert!(producer.append(&json!({ "n": 1 })).is_err());
281 assert_eq!(drain(consume(producer.consume(), true)), vec![json!({ "n": 0 })]);
282 }
283
284 #[test]
289 fn a_second_group_is_reported_while_the_first_is_open() {
290 let track = moq_net::broadcast::Info::new()
291 .produce()
292 .create_track("test", None)
293 .unwrap();
294
295 let subscription = moq_net::track::Subscription::default().with_max_age(std::time::Duration::from_secs(30));
298 let subscriber = track.subscribe(subscription);
299
300 let mut first = track.append_group().unwrap();
302 first
303 .write_frame(moq_net::Timestamp::now(), br#"{"n":0}"#.as_slice())
304 .unwrap();
305 let mut second = track.append_group().unwrap();
306 second
307 .write_frame(moq_net::Timestamp::now(), br#"{"n":1}"#.as_slice())
308 .unwrap();
309
310 let mut consumer = consume(subscriber, false);
311 let waiter = kio::Waiter::noop();
312
313 assert!(matches!(
314 consumer.poll_next(&waiter),
315 Poll::Ready(Ok(Some(value))) if value == json!({ "n": 0 })
316 ));
317 assert!(matches!(
318 consumer.poll_next(&waiter),
319 Poll::Ready(Err(crate::Error::Rolled))
320 ));
321
322 first
324 .write_frame(moq_net::Timestamp::now(), br#"{"n":2}"#.as_slice())
325 .unwrap();
326 assert!(matches!(
327 consumer.poll_next(&waiter),
328 Poll::Ready(Err(crate::Error::Rolled))
329 ));
330 }
331
332 #[test]
333 fn embedded_newlines_survive() {
334 let (mut producer, track) = producer(compressed());
337 let value = json!({ "s": "line1\nline2\ttab", "u": "a\u{000a}b" });
338 for _ in 0..4 {
339 producer.append(&value).unwrap();
340 }
341 producer.finish().unwrap();
342
343 let records = drain(consume(track, true));
344 assert_eq!(records, vec![value.clone(), value.clone(), value.clone(), value]);
345 }
346}