File
Blob: firmware/crates/radio-core/tests/playlist.rs
| 1 | use radio_core::{ |
| 2 | music::{Catalog, Error, ReadAt}, |
| 3 | now_playing::{Message, NowPlaying, Snapshot}, |
| 4 | playback::Playback, |
| 5 | protocol::Action, |
| 6 | }; |
| 7 | use std::num::NonZeroU32; |
| 8 | |
| 9 | const FIXTURE: &[u8] = include_bytes!("fixtures/playlist-v4.pack"); |
| 10 | |
| 11 | #[test] |
| 12 | fn catalog_preserves_order_unicode_metadata_and_individual_frames() { |
| 13 | let mut source = FIXTURE; |
| 14 | let catalog = Catalog::parse(&mut source).unwrap(); |
| 15 | assert_eq!(catalog.songs().len(), 2); |
| 16 | assert_eq!( |
| 17 | catalog.songs()[0].metadata.as_ref().unwrap().title, |
| 18 | "First café" |
| 19 | ); |
| 20 | assert_eq!( |
| 21 | catalog.songs()[1].metadata.as_ref().unwrap().title, |
| 22 | "第二首" |
| 23 | ); |
| 24 | assert_eq!( |
| 25 | catalog.songs()[1].metadata.as_ref().unwrap().duration_ms, |
| 26 | 60 |
| 27 | ); |
| 28 | let mut packet = [0; 1275]; |
| 29 | assert_eq!( |
| 30 | catalog.read_frame(&mut source, 1, 2, &mut packet).unwrap(), |
| 31 | 3 |
| 32 | ); |
| 33 | assert_eq!(&packet[..3], [0xf8, 0xff, 0xfe]); |
| 34 | assert!(catalog.read_frame(&mut source, 1, 3, &mut packet).is_err()); |
| 35 | assert!(catalog.read_frame(&mut source, 2, 0, &mut packet).is_err()); |
| 36 | } |
| 37 | |
| 38 | #[test] |
| 39 | fn catalog_rejects_partial_corrupt_and_hostile_track_tables() { |
| 40 | for end in 0..FIXTURE.len() { |
| 41 | assert!(Catalog::parse(&mut &FIXTURE[..end]).is_err()); |
| 42 | } |
| 43 | let mut bytes = FIXTURE.to_vec(); |
| 44 | *bytes.last_mut().unwrap() ^= 1; |
| 45 | assert_eq!( |
| 46 | Catalog::parse(&mut bytes.as_slice()).unwrap_err(), |
| 47 | Error::Checksum |
| 48 | ); |
| 49 | for count in [0u32, 33, u32::MAX] { |
| 50 | let mut bytes = FIXTURE.to_vec(); |
| 51 | bytes[12..16].copy_from_slice(&count.to_le_bytes()); |
| 52 | assert!(Catalog::parse(&mut bytes.as_slice()).is_err()); |
| 53 | } |
| 54 | let mut bytes = FIXTURE.to_vec(); |
| 55 | bytes[64..68].copy_from_slice(&u32::MAX.to_le_bytes()); |
| 56 | let crc = crc32fast::hash(&bytes[64..]); |
| 57 | bytes[28..32].copy_from_slice(&crc.to_le_bytes()); |
| 58 | assert!(Catalog::parse(&mut bytes.as_slice()).is_err()); |
| 59 | } |
| 60 | |
| 61 | #[test] |
| 62 | fn catalog_reads_beyond_four_mib_without_loading_the_partition() { |
| 63 | let mut song = vec![0; 64]; |
| 64 | song[..8].copy_from_slice(b"S3MUSIC\0"); |
| 65 | for (offset, value) in [(8, 2u32), (12, 7000), (16, 48000), (20, 2), (24, 20)] { |
| 66 | song[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); |
| 67 | } |
| 68 | for _ in 0..7000 { |
| 69 | song.extend(1275u16.to_le_bytes()); |
| 70 | song.extend([7; 1275]); |
| 71 | } |
| 72 | let crc = crc32fast::hash(&song[64..]); |
| 73 | song[28..32].copy_from_slice(&crc.to_le_bytes()); |
| 74 | struct Storage { |
| 75 | bytes: Vec<u8>, |
| 76 | largest: usize, |
| 77 | } |
| 78 | impl ReadAt for Storage { |
| 79 | fn size(&self) -> usize { |
| 80 | self.bytes.len() |
| 81 | } |
| 82 | fn read_exact(&mut self, offset: usize, out: &mut [u8]) -> Result<(), Error> { |
| 83 | self.largest = self.largest.max(out.len()); |
| 84 | out.copy_from_slice( |
| 85 | self.bytes |
| 86 | .get(offset..offset + out.len()) |
| 87 | .ok_or(Error::Truncated)?, |
| 88 | ); |
| 89 | Ok(()) |
| 90 | } |
| 91 | } |
| 92 | let mut storage = Storage { |
| 93 | bytes: song, |
| 94 | largest: 0, |
| 95 | }; |
| 96 | let catalog = Catalog::parse(&mut storage).unwrap(); |
| 97 | let mut frame = [0; 1275]; |
| 98 | catalog |
| 99 | .read_frame(&mut storage, 0, 6999, &mut frame) |
| 100 | .unwrap(); |
| 101 | assert!(frame.iter().all(|&n| n == 7)); |
| 102 | assert!(storage.largest <= 1275); |
| 103 | assert!(catalog.index_bytes() <= 7000 * 4); |
| 104 | } |
| 105 | |
| 106 | fn player() -> Playback { |
| 107 | Playback::playlist( |
| 108 | [2, 3] |
| 109 | .into_iter() |
| 110 | .map(|n| NonZeroU32::new(n).unwrap()) |
| 111 | .collect(), |
| 112 | 0, |
| 113 | ) |
| 114 | .unwrap() |
| 115 | } |
| 116 | |
| 117 | #[test] |
| 118 | fn automatic_transitions_and_next_preserve_the_transport_clock() { |
| 119 | let mut p = player(); |
| 120 | assert_eq!(p.due(0).unwrap().track_index, 0); |
| 121 | p.due(20000); |
| 122 | let next = p.due(40000).unwrap(); |
| 123 | assert_eq!( |
| 124 | (next.track_index, next.index, next.pts_ms, next.revision), |
| 125 | (1, 0, 40, 1) |
| 126 | ); |
| 127 | p.apply(Action::Next); |
| 128 | let skipped = p.due(60000).unwrap(); |
| 129 | assert_eq!( |
| 130 | ( |
| 131 | skipped.track_index, |
| 132 | skipped.index, |
| 133 | skipped.pts_ms, |
| 134 | skipped.revision |
| 135 | ), |
| 136 | (0, 0, 60, 2) |
| 137 | ); |
| 138 | p.apply(Action::Pause); |
| 139 | p.apply(Action::Next); |
| 140 | let paused = p.due(80000).unwrap(); |
| 141 | assert_eq!( |
| 142 | (paused.track_index, paused.index, paused.paused), |
| 143 | (1, 0, true) |
| 144 | ); |
| 145 | p.apply(Action::Play); |
| 146 | assert_eq!(p.due(100000).unwrap().pts_ms, 100); |
| 147 | } |
| 148 | |
| 149 | #[test] |
| 150 | fn long_stalls_skip_across_song_boundaries_without_bursting() { |
| 151 | let mut p = player(); |
| 152 | p.due(0); |
| 153 | let due = p.due(100_060_000).unwrap(); |
| 154 | assert_eq!((due.track_index, due.index, due.pts_ms), (1, 1, 100060)); |
| 155 | assert!(due.revision > 1000); |
| 156 | assert!(p.due(100_060_001).is_none()); |
| 157 | } |
| 158 | |
| 159 | fn final_frame_player() -> Playback { |
| 160 | let mut p = Playback::playlist( |
| 161 | [2, 3, 4] |
| 162 | .into_iter() |
| 163 | .map(|n| NonZeroU32::new(n).unwrap()) |
| 164 | .collect(), |
| 165 | 0, |
| 166 | ) |
| 167 | .unwrap(); |
| 168 | p.due(0); |
| 169 | let last = p.due(20_000).unwrap(); |
| 170 | assert_eq!((last.track_index, last.index), (0, 1)); |
| 171 | assert_eq!(p.epoch(), last.epoch); |
| 172 | p |
| 173 | } |
| 174 | |
| 175 | #[test] |
| 176 | fn commands_between_the_final_packet_and_next_deadline_target_the_announced_song() { |
| 177 | for (action, track, revision) in [(Action::Next, 1, 1), (Action::Restart, 0, 1)] { |
| 178 | let mut p = final_frame_player(); |
| 179 | p.apply(action); |
| 180 | assert!(p.due(39_999).is_none()); |
| 181 | let due = p.due(40_000).unwrap(); |
| 182 | assert_eq!( |
| 183 | (due.track_index, due.index, due.revision, due.pts_ms), |
| 184 | (track, 0, revision, 40) |
| 185 | ); |
| 186 | } |
| 187 | let mut p = final_frame_player(); |
| 188 | p.apply(Action::Pause); |
| 189 | let held = p.due(40_000).unwrap(); |
| 190 | assert_eq!( |
| 191 | (held.track_index, held.index, held.revision, held.paused), |
| 192 | (0, 1, 0, true) |
| 193 | ); |
| 194 | p.apply(Action::Play); |
| 195 | let next = p.due(60_000).unwrap(); |
| 196 | assert_eq!( |
| 197 | (next.track_index, next.index, next.revision, next.pts_ms), |
| 198 | (1, 0, 1, 60) |
| 199 | ); |
| 200 | |
| 201 | let mut p = final_frame_player(); |
| 202 | p.apply(Action::Pause); |
| 203 | p.apply(Action::Next); |
| 204 | let next = p.due(40_000).unwrap(); |
| 205 | assert_eq!( |
| 206 | (next.track_index, next.index, next.revision, next.paused), |
| 207 | (1, 0, 1, true) |
| 208 | ); |
| 209 | } |
| 210 | |
| 211 | #[test] |
| 212 | fn pending_transition_and_a_stall_advance_once_then_skip_elapsed_packets() { |
| 213 | let mut p = final_frame_player(); |
| 214 | let due = p.due(1_040_000).unwrap(); |
| 215 | assert_eq!( |
| 216 | (due.track_index, due.index, due.revision, due.pts_ms), |
| 217 | (2, 2, 17, 1040) |
| 218 | ); |
| 219 | assert_eq!(p.skipped_frames(), 50); |
| 220 | assert!(p.due(1_040_001).is_none()); |
| 221 | } |
| 222 | |
| 223 | #[test] |
| 224 | fn published_metadata_position_and_duration_follow_the_emitted_song() { |
| 225 | let mut source = FIXTURE; |
| 226 | let catalog = Catalog::parse(&mut source).unwrap(); |
| 227 | let mut snapshot = Snapshot::new(&catalog); |
| 228 | let mut p = player(); |
| 229 | snapshot.record(&catalog, p.due(0).unwrap()).unwrap(); |
| 230 | assert!(!snapshot.record(&catalog, p.due(20_000).unwrap()).unwrap()); |
| 231 | assert_eq!( |
| 232 | ( |
| 233 | snapshot.now_playing.track_index, |
| 234 | snapshot.position_ms, |
| 235 | snapshot.duration_ms |
| 236 | ), |
| 237 | (0, 20, 40) |
| 238 | ); |
| 239 | let next = p.due(40_000).unwrap(); |
| 240 | assert!(snapshot.record(&catalog, next).unwrap()); |
| 241 | assert_eq!(snapshot.now_playing.track.as_ref().unwrap().title, "第二首"); |
| 242 | assert_eq!( |
| 243 | ( |
| 244 | snapshot.now_playing.revision, |
| 245 | snapshot.position_ms, |
| 246 | snapshot.duration_ms |
| 247 | ), |
| 248 | (1, 0, 60) |
| 249 | ); |
| 250 | assert!( |
| 251 | snapshot |
| 252 | .record(&catalog, radio_core::playback::Due { index: 3, ..next }) |
| 253 | .is_err() |
| 254 | ); |
| 255 | assert_eq!(snapshot.position_ms, 0); |
| 256 | } |
| 257 | |
| 258 | #[test] |
| 259 | fn maximal_escaped_metadata_fits_the_transport_data_limit() { |
| 260 | let state = NowPlaying { |
| 261 | revision: u32::MAX, |
| 262 | track_index: 31, |
| 263 | track_count: 32, |
| 264 | track: Some(radio_core::music::Track { |
| 265 | title: "\\".repeat(256), |
| 266 | artist: "\"".repeat(256), |
| 267 | duration_ms: 600000, |
| 268 | }), |
| 269 | }; |
| 270 | let bytes = serde_json::to_vec(&Message { |
| 271 | event: "nowPlaying", |
| 272 | now_playing: &state, |
| 273 | }) |
| 274 | .unwrap(); |
| 275 | assert!(bytes.len() <= 2048); |
| 276 | } |