open-standards/wai/wai-rs/tests/carousel.rs
Transaction Science 869fd65efc wai: a schedule names Raptor (RFC 5053), the code MBMS download uses
Carousel §4.1, §5.5: an entry's fec may be raptor (FEC Encoding ID 1), with RFC
5053's F, T, Z, N and Al, held to its fields and to its block sizes, 4 to 8,192
source symbols, with ESIs in 16 bits. 3GPP TS 26.346 §7.2.2 requires a receiver
of MBMS download to support the code.

Carousel §5.3: RFC 5053 states no bound on a decoder's failure. An independent
implementation failed on 14 to 31 blocks in 100 at K + 2, so a Raptor block is
held at K and a margin set by measurement, 8, 16 or 28 at overhead 0, 1 or 2,
and failure_one_in is null. tests/carousel.rs decodes each Raptor object a case
holds available with that implementation, a new development dependency, holds a
fixed sample of it to the measurement, and keeps the measurement as an ignored
test.

no-code stays the default, and carousel §11 keeps the IETF's disclosures on RFC
5053 for implementers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:09:38 -05:00

325 lines
16 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `wai.bearer.carousel` (extensions/carousel.md): the committed corpus `live-conformance/carousel/`
//! regenerates byte for byte from its fixed seeds, holds no other file, every case in it gets the
//! outcome it states, §9 lists exactly the reasons the reference reports, jwp-receipts §3.1 lists
//! the class codes at their steps, and Appendix A is the reference's table of K′. Against an
//! independent implementation of RFC 6330: the table of K′ and the partition agree for every block
//! size, and each RaptorQ object of the corpus that a case holds available at a pass decodes from
//! what a receiver within its loss bound holds by that pass's end. Against an independent
//! implementation of RFC 5053, each Raptor object does the same.
#![cfg(feature = "carousel")]
use std::collections::BTreeMap;
use std::path::PathBuf;
use raptorq::{Decoder, Encoder, EncodingPacket, ObjectTransmissionInformation};
use serde_json::Value;
use wai::carousel::{corpus, k_prime, missed, CODES, K_PRIME};
use wai::claim_receipt::schedule::{partition, K_PRIME_MAX};
use wai::claim_receipt::{Esi, Oti, ScheduleEntry};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn committed() -> BTreeMap<String, String> {
let dir = wai_dir().join("live-conformance/carousel");
std::fs::read_dir(&dir)
.expect("the committed corpus")
.flatten()
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.path()))
.filter(|(n, _)| n != "README.md")
.map(|(n, p)| (n, std::fs::read_to_string(p).expect("a text file")))
.collect()
}
fn extension() -> String {
std::fs::read_to_string(wai_dir().join("extensions/carousel.md")).expect("the extension")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
let files = committed();
let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
let missing: Vec<&String> = generated.keys().filter(|k| !files.contains_key(*k)).collect();
let differ: Vec<&String> = generated.iter().filter(|(k, v)| files.get(*k).is_some_and(|f| f != *v)).map(|(k, _)| k).collect();
assert!(extra.is_empty() && missing.is_empty() && differ.is_empty(), "extra {extra:?}, missing {missing:?}, differ {differ:?}: run `wai_carousel_vectors gen`");
}
#[test]
fn every_case_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// §9 of the extension lists exactly the codes `CODES` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = extension();
let at = text.find("\n## 9. Reasons").expect("a §9");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> =
text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = CODES.iter().map(|c| c.to_string()).collect();
assert_eq!(listed, want, "§9's table and CODES differ");
}
/// jwp-receipts §3.1's table lists the class's codes: its rules at step 4, its checks against the
/// group and its objects at step 6.
#[test]
fn jwp_receipts_lists_the_class_codes() {
let text = std::fs::read_to_string(wai_dir().join("extensions/jwp-receipts.md")).expect("jwp-receipts");
for code in CODES.iter().filter(|c| c.starts_with("schedule_") && **c != "schedule_contradicted" && **c != "schedule_on_bidirectional") {
let step = if ["schedule_not_scheduled", "schedule_objects_malformed", "schedule_oti_invalid", "schedule_symbols_invalid", "schedule_passes_invalid"].contains(code) {
4
} else {
6
};
assert!(text.contains(&format!("| {step} | `{code}` |")), "jwp-receipts §3.1 does not list {code} at step {step}");
}
}
/// Appendix A holds exactly `K_PRIME`, in order.
#[test]
fn appendix_a_is_the_table_of_k_prime() {
let text = extension();
let a = text.find("<!-- k-prime:begin -->").expect("the table's start");
let b = text.find("<!-- k-prime:end -->").expect("the table's end");
let listed: Vec<u32> = text[a..b].split(|c: char| !c.is_ascii_digit()).filter(|s| !s.is_empty()).map(|s| s.parse().expect("a number")).collect();
assert_eq!(listed, K_PRIME.to_vec());
}
/// The table and the partition agree with an independent implementation of RFC 6330 for every
/// block size it admits.
#[test]
fn k_prime_and_partition_agree_with_an_independent_rfc_6330() {
for k in 1..=K_PRIME_MAX as u32 {
assert_eq!(k_prime(u64::from(k)), Some(u64::from(raptorq::extended_source_block_symbols(k))), "K' of {k}");
}
for (i, j) in [(50, 3), (14, 1), (5, 1), (56_403 * 255, 255), (16, 2), (15, 2), (1, 1)] {
let (il, is, jl, js) = partition(i, j);
assert_eq!((il as u32, is as u32, jl as u32, js as u32), raptorq::partition(i as u32, j as u32), "Partition[{i}, {j}]");
}
}
fn hex(s: &str) -> Vec<u8> {
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect()
}
/// What a receiver within `loss_ppm` holds of `e`'s object by the end of pass `pass`, fed to an
/// independent RFC 6330 decoder until it decodes: in each pass, of each block's m symbols, the
/// first ⌊L · m / 10^6⌋ in ESI order are lost (in the first pass, source symbols, so the
/// decoder must decode). Under `repeat` a later pass is taken to lose the same symbols again,
/// so only the first is fed.
fn receive(e: &ScheduleEntry, data: &[u8], loss_ppm: u64, pass: u64) -> Option<Vec<u8>> {
let Oti::RaptorQ { transfer_length, symbol_size, source_blocks, sub_blocks, alignment } = e.oti else { return None };
let config = ObjectTransmissionInformation::new(transfer_length, symbol_size as u16, source_blocks as u8, sub_blocks as u16, alignment as u8);
let encoder = Encoder::new(data, config);
let mut decoder = Decoder::new(config);
let passes = if e.esi == Esi::Advance { pass } else { 1 };
for block in encoder.get_block_encoders() {
let source = block.source_packets();
let k = source.len() as u64;
let m = k + e.repair;
let x = missed(loss_ppm, m) as usize;
for j in 1..=passes {
let start = if e.esi == Esi::Advance { (j - 1) * m } else { 0 };
let mut sent: Vec<EncodingPacket> = (start..(start + m).min(k)).map(|esi| source[esi as usize].clone()).collect();
let repair_from = start.max(k);
if start + m > repair_from {
sent.extend(block.repair_packets((repair_from - k) as u32, (start + m - repair_from) as u32));
}
assert_eq!(sent.len() as u64, m);
for p in sent.into_iter().skip(x) {
if let Some(out) = decoder.decode(p) {
return Some(out);
}
}
}
}
None
}
/// [`receive`] under an independent RFC 5053 decoder, block by block (RFC 5053 §5.3.1.2): `None`
/// where a block does not decode from what the receiver holds.
fn receive_raptor(e: &ScheduleEntry, data: &[u8], loss_ppm: u64, pass: u64) -> Option<Vec<u8>> {
let Oti::Raptor { transfer_length, symbol_size: t, source_blocks: z, .. } = e.oti else { return None };
let kt = transfer_length.div_ceil(t);
let (kl, ks, zl, zs) = partition(kt, z);
let mut padded = data.to_vec();
padded.resize((kt * t) as usize, 0);
let advance = e.esi == Esi::Advance;
let mut out = Vec::with_capacity(padded.len());
let mut at = 0usize;
for k in std::iter::repeat_n(kl, zl as usize).chain(std::iter::repeat_n(ks, zs as usize)) {
let length = (k * t) as usize;
let block = &padded[at..at + length];
at += length;
let mut encoder = raptor_code::SourceBlockEncoder::new(block, k as usize).expect("a block RFC 5053 admits");
assert_eq!(u64::from(encoder.nb_source_symbols()), k);
let mut decoder = raptor_code::SourceBlockDecoder::new(k as usize);
let m = k + e.repair;
let x = missed(loss_ppm, m);
for j in 1..=(if advance { pass } else { 1 }) {
let start = if advance { (j - 1) * m } else { 0 };
for esi in start + x..start + m {
decoder.push_encoding_symbol(&encoder.fountain(esi as u32), esi as u32);
}
}
if !decoder.fully_specified() {
return None;
}
out.extend(decoder.decode(length)?);
}
out.truncate(transfer_length as usize);
Some(out)
}
/// Of `trials` sets of `k` + `margin` distinct encoding symbols of a block of `k`, chosen uniformly
/// at random from a fixed seed, how many an independent RFC 5053 decoder cannot solve.
fn raptor_failures(k: usize, margin: usize, trials: usize, seed: u64) -> usize {
let block: Vec<u8> = (0..k * 4).map(|i| (i * 131 + 7) as u8).collect();
let mut encoder = raptor_code::SourceBlockEncoder::new(&block, k).expect("a block RFC 5053 admits");
let n = k + 3 * margin + 40;
let symbols: Vec<Vec<u8>> = (0..n as u32).map(|esi| encoder.fountain(esi)).collect();
let mut state = seed;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as usize
};
(0..trials)
.filter(|_| {
let mut esis: Vec<usize> = (0..n).collect();
for i in 0..k + margin {
let j = i + next() % (n - i);
esis.swap(i, j);
}
let mut decoder = raptor_code::SourceBlockDecoder::new(k);
for &esi in &esis[..k + margin] {
decoder.push_encoding_symbol(&symbols[esi], esi as u32);
}
!decoder.fully_specified()
})
.count()
}
/// Carousel §5.3 holds a Raptor block at K and a margin of this text's, RFC 5053 stating no bound.
/// An independent decoder, on a fixed sample, fails on many blocks at K + 2 and on few at each
/// margin. This is a measurement of that sample, as §5.3 says, and no bound.
#[test]
fn raptor_margins_keep_decoding_failures_rare() {
use wai::carousel::RAPTOR_MARGIN;
for k in [16, 64] {
let seed = 0x9E37_79B9_7F4A_7C15 ^ k as u64;
let at_two = raptor_failures(k, 2, 400, seed);
assert!(at_two >= 40, "K = {k}: {at_two} of 400 at K + 2");
let first = raptor_failures(k, RAPTOR_MARGIN[0] as usize, 2_000, seed);
assert!(first <= 20, "K = {k}: {first} of 2,000 at K + {}", RAPTOR_MARGIN[0]);
let second = raptor_failures(k, RAPTOR_MARGIN[1] as usize, 10_000, seed);
assert!(second <= 1, "K = {k}: {second} of 10,000 at K + {}", RAPTOR_MARGIN[1]);
let third = raptor_failures(k, RAPTOR_MARGIN[2] as usize, 10_000, seed);
assert_eq!(third, 0, "K = {k}: of 10,000 at K + {}", RAPTOR_MARGIN[2]);
}
}
/// The measurement carousel §5.3 quotes, by block size and margin, from fixed seeds. It takes some
/// minutes in a release build: `cargo test --release --test carousel -- --ignored --nocapture`.
#[test]
#[ignore = "a measurement of some minutes, run by hand in a release build; carousel §5.3 quotes it"]
fn raptor_margin_measurement() {
let sample: &[(usize, &[(usize, usize)])] = &[
(2, &[(4, 6_000), (16, 6_000), (64, 6_000), (1_024, 600)]),
(8, &[(4, 1_200_000), (8, 1_200_000), (16, 1_200_000), (64, 1_200_000), (256, 240_000), (1_024, 42_000), (2_048, 18_000), (8_192, 1_800)]),
(16, &[(4, 1_800_000), (8, 1_800_000), (16, 1_800_000), (64, 1_800_000), (256, 360_000), (1_024, 60_000), (2_048, 18_000), (8_192, 1_800)]),
(28, &[(4, 6_000_000), (8, 6_000_000), (16, 6_000_000), (32, 6_000_000), (64, 6_000_000), (128, 3_000_000), (256, 1_200_000), (1_024, 120_000), (2_048, 24_000), (8_192, 1_800)]),
];
for (margin, sizes) in sample {
for (k, trials) in *sizes {
let (k, margin, trials) = (*k, *margin, *trials);
let failures: usize = std::thread::scope(|s| {
let runs: Vec<_> = (0..6u64)
.map(|t| s.spawn(move || raptor_failures(k, margin, trials / 6, 0x9E37_79B9_7F4A_7C15 ^ (((k as u64) << 24) | ((margin as u64) << 8) | (t + 1)))))
.collect();
runs.into_iter().map(|r| r.join().expect("a run")).sum()
});
println!("K {k}, K + {margin}: {failures} of {trials}");
}
}
}
#[test]
fn each_raptor_object_decodes_from_what_a_receiver_within_its_bound_holds() {
let files = committed();
let doc: Value = serde_json::from_str(&files["vectors.json"]).expect("the corpus");
let mut decoded = 0;
for c in doc["verdicts"].as_array().expect("verdict cases") {
let Some(lines) = c["outcome"]["lines"].as_array() else { continue };
let loss = c["loss_ppm"].as_u64().expect("a loss bound");
let schedules: Vec<Value> = c["claims"]
.as_array()
.expect("claims")
.iter()
.map(|w| serde_json::from_str::<Value>(w.as_str().expect("a claim")).expect("json"))
.filter(|v| v["class"] == "schedule")
.collect();
for line in lines {
let Some(pass) = line["pass"].as_u64() else { continue };
let leaf = line["leaf_index"].as_u64().expect("a leaf") as usize;
let Some(data) = c["objects"][leaf]["envelope"].as_str().map(hex) else { continue };
let entry = schedules
.iter()
.filter(|s| s["body"]["bearer_id"] == line["bearer_id"])
.flat_map(|s| s["body"]["objects"].as_array().expect("entries").iter())
.rfind(|o| o["leaf_index"] == line["leaf_index"])
.map(|o| ScheduleEntry::from_value(o).expect("an entry that stands"))
.expect("the entry of a line");
if !matches!(entry.oti, Oti::Raptor { .. }) {
continue;
}
let got = receive_raptor(&entry, &data, loss, pass);
assert_eq!(got.as_deref(), Some(&data[..]), "{} object {leaf} on {} by pass {pass}", c["name"], line["bearer_id"]);
decoded += 1;
}
}
assert!(decoded >= 10, "only {decoded} objects decoded");
}
#[test]
fn each_raptorq_object_decodes_from_what_a_receiver_within_its_bound_holds() {
let files = committed();
let doc: Value = serde_json::from_str(&files["vectors.json"]).expect("the corpus");
let mut decoded = 0;
for c in doc["verdicts"].as_array().expect("verdict cases") {
let Some(lines) = c["outcome"]["lines"].as_array() else { continue };
let loss = c["loss_ppm"].as_u64().expect("a loss bound");
let schedules: Vec<Value> = c["claims"]
.as_array()
.expect("claims")
.iter()
.map(|w| serde_json::from_str::<Value>(w.as_str().expect("a claim")).expect("json"))
.filter(|v| v["class"] == "schedule")
.collect();
for line in lines {
let Some(pass) = line["pass"].as_u64() else { continue };
let leaf = line["leaf_index"].as_u64().expect("a leaf") as usize;
let Some(data) = c["objects"][leaf]["envelope"].as_str().map(hex) else { continue };
let entry = schedules
.iter()
.filter(|s| s["body"]["bearer_id"] == line["bearer_id"])
.flat_map(|s| s["body"]["objects"].as_array().expect("entries").iter())
// The schedule that stands is the last of its instance (§6.2 step 4).
.rfind(|o| o["leaf_index"] == line["leaf_index"])
.map(|o| ScheduleEntry::from_value(o).expect("an entry that stands"))
.expect("the entry of a line");
if !matches!(entry.oti, Oti::RaptorQ { .. }) {
continue;
}
let got = receive(&entry, &data, loss, pass);
assert_eq!(got.as_deref(), Some(&data[..]), "{} object {leaf} on {} by pass {pass}", c["name"], line["bearer_id"]);
decoded += 1;
}
}
assert!(decoded >= 10, "only {decoded} objects decoded");
}