Skip to content
File

Blob: archive/fft-benchmark/firmware/crates/radio-core/tests/contracts.rs

rust199 lines
1use radio_core::{
2 music::{Error as MusicError, Pack},
3 playback::Playback,
4 protocol::{self, Action, Command},
5 signaling::{self, Channels, Started},
6};
7use std::num::NonZeroU32;
8 
9// Independent fixture: Python struct.pack('<8sIIIIII32x', ...) and zlib.crc32
10// over one 3-byte Opus silence packet with spectrum bytes 0..31. No paid media.
11fn fixture() -> Vec<u8> {
12 include_bytes!("fixtures/silence.pack").to_vec()
13}
14 
15#[test]
16fn pack_reads_known_crc_and_ignores_erased_partition_tail() {
17 let mut bytes = fixture();
18 bytes.extend([255; 100]);
19 let pack = Pack::parse(&bytes).unwrap();
20 let frame = pack.frame(0).unwrap();
21 assert_eq!(frame.opus, [0xf8, 0xff, 0xfe]);
22 assert_eq!(frame.bands[31], 31);
23 assert!(pack.frame(1).is_none());
24}
25 
26#[test]
27fn every_truncated_fixture_is_rejected_without_panicking() {
28 let bytes = fixture();
29 for end in 0..bytes.len() {
30 assert!(
31 Pack::parse(&bytes[..end]).is_err(),
32 "accepted truncated length {end}"
33 );
34 }
35}
36 
37#[test]
38fn corrupted_payload_and_hostile_record_lengths_are_rejected() {
39 let mut bytes = fixture();
40 bytes[100] ^= 1;
41 assert_eq!(Pack::parse(&bytes).unwrap_err(), MusicError::Checksum);
42 for size in [0u16, 1276, u16::MAX] {
43 let mut bytes = fixture();
44 bytes[64..66].copy_from_slice(&size.to_le_bytes());
45 assert_eq!(Pack::parse(&bytes).unwrap_err(), MusicError::PacketSize);
46 }
47 for count in [0u32, 30_001, u32::MAX] {
48 let mut bytes = fixture();
49 bytes[12..16].copy_from_slice(&count.to_le_bytes());
50 assert_eq!(Pack::parse(&bytes).unwrap_err(), MusicError::FrameCount);
51 }
52}
53 
54#[test]
55fn arbitrary_small_inputs_do_not_panic() {
56 let mut random = 123u32;
57 for length in 0..1024 {
58 let bytes: Vec<u8> = (0..length)
59 .map(|_| {
60 random ^= random << 13;
61 random ^= random >> 17;
62 random ^= random << 5;
63 random as u8
64 })
65 .collect();
66 let _ = Pack::parse(&bytes);
67 let _ = Command::parse(&bytes);
68 }
69}
70 
71fn player() -> Playback {
72 Playback::new(NonZeroU32::new(5).unwrap(), 1_000_000)
73}
74 
75#[test]
76fn stall_skips_audio_without_bursting_and_wraps_track() {
77 let mut player = player();
78 assert_eq!(player.due(1_000_000).unwrap().index, 0);
79 let due = player.due(1_141_000).unwrap();
80 assert_eq!((due.index, due.pts_ms), (2, 140));
81 assert_eq!(player.skipped_frames(), 6);
82 assert!(player.due(1_141_001).is_none());
83 assert_eq!(player.due(1_160_000).unwrap().index, 3);
84}
85 
86#[test]
87fn pause_holds_position_while_rtp_clock_advances() {
88 let mut player = player();
89 player.due(1_000_000);
90 player.apply(Action::Pause);
91 let a = player.due(1_020_000).unwrap();
92 let b = player.due(1_080_000).unwrap();
93 assert_eq!((a.index, b.index, b.pts_ms), (1, 1, 80));
94 assert!(a.paused && b.paused);
95 player.apply(Action::Play);
96 let c = player.due(1_100_000).unwrap();
97 assert_eq!((c.index, c.pts_ms, c.paused), (1, 100, false));
98}
99 
100#[test]
101fn restart_resets_song_but_never_resets_transport_clock() {
102 let mut player = player();
103 player.due(1_000_000);
104 player.due(1_020_000);
105 player.apply(Action::Pause);
106 player.apply(Action::Restart);
107 let due = player.due(1_040_000).unwrap();
108 assert_eq!((due.index, due.pts_ms, due.paused), (0, 40, false));
109}
110 
111#[test]
112fn spectrum_has_independent_little_endian_wire_oracle() {
113 let due = radio_core::playback::Due {
114 index: 17,
115 pts_ms: 0x12345678,
116 paused: false,
117 spectrum: true,
118 };
119 let bytes = protocol::spectrum(due, &[42; 32]);
120 assert_eq!(
121 &bytes[..12],
122 &[1, 0, 0, 0, 0x78, 0x56, 0x34, 0x12, 0x54, 1, 0, 0]
123 );
124 assert!(bytes[12..].iter().all(|&v| v == 42));
125 let paused = protocol::spectrum(
126 radio_core::playback::Due {
127 paused: true,
128 ..due
129 },
130 &[42; 32],
131 );
132 assert_eq!(paused[1], 1);
133 assert!(paused[12..].iter().all(|&v| v == 0));
134}
135 
136#[test]
137fn commands_enforce_color_and_keep_maintenance_out_of_network_protocol() {
138 assert!(Command::parse(br#"{"led":[0,255,17],"command_id":"abc"}"#).is_ok());
139 for bytes in [
140 br#"{"led":[-1,0,0]}"#.as_slice(),
141 br#"{"led":[256,0,0]}"#,
142 br#"{"led":[1.5,0,0]}"#,
143 br#"{"led":[0,0]}"#,
144 br#"{"cmd":"restart_device","led":[0,0,0]}"#,
145 br#"{"action":"erase"}"#,
146 br#"{"action":"pause","action":"play"}"#,
147 br#"{"led":[0,0,0]} trailing"#,
148 ] {
149 assert!(Command::parse(bytes).is_err());
150 }
151 assert!(Command::parse(&[b' '; 513]).is_err());
152 let oversized_id = format!(r#"{{"action":"pause","command_id":"{}"}}"#, "x".repeat(65));
153 assert!(Command::parse(oversized_id.as_bytes()).is_err());
154}
155 
156#[test]
157fn publisher_validation_redacts_sdp_and_rejects_wrong_response_types() {
158 let raw = r#"{"generation":"g1","sessionDescription":{"type":"answer","sdp":"v=0\r\na=ice-pwd:secret"}}"#;
159 let started: Started = serde_json::from_str(raw).unwrap();
160 assert!(!format!("{started:?}").contains("secret"));
161 assert!(started.validate().is_ok());
162 let wrong: Started = serde_json::from_str(&raw.replace("answer", "offer")).unwrap();
163 assert!(wrong.validate().is_err());
164}
165 
166#[test]
167fn channel_allocation_is_checked_by_name_not_response_order() {
168 let reversed = r#"{"channels":[{"dataChannelName":"spectrum","id":4},{"dataChannelName":"robot","id":2}]}"#;
169 assert!(
170 serde_json::from_str::<Channels>(reversed)
171 .unwrap()
172 .validate()
173 .is_ok()
174 );
175 assert!(
176 serde_json::from_str::<Channels>(&reversed.replace("\"id\":2", "\"id\":6"))
177 .unwrap()
178 .validate()
179 .is_err()
180 );
181 assert!(
182 serde_json::from_str::<Channels>(&reversed.replace("spectrum", "robot"))
183 .unwrap()
184 .validate()
185 .is_err()
186 );
187}
188 
189#[test]
190fn recovery_distinguishes_transient_failure_and_revoked_generation() {
191 assert!(signaling::retryable(-1));
192 assert!(signaling::retryable(429));
193 assert!(signaling::retryable(503));
194 assert!(!signaling::retryable(401));
195 assert!(!signaling::needs_recovery(503, 5_000_000));
196 assert!(signaling::needs_recovery(503, 56_000_000));
197 assert!(signaling::needs_recovery(409, 1));
198}