max / shop
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 file changed,
+185 insertions,
-0 deletions
| @@ -248,3 +248,188 @@ | |||
| 248 | 248 | into.no_cursor_move = true; | |
| 249 | 249 | } | |
| 250 | 250 | } | |
| 251 | + | ||
| 252 | + | #[cfg(test)] | |
| 253 | + | mod tests { | |
| 254 | + | use super::*; | |
| 255 | + | use base64::Engine; | |
| 256 | + | ||
| 257 | + | fn b64(bytes: &[u8]) -> String { | |
| 258 | + | B64.encode(bytes) | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | fn transmit(cmd: Command) -> (Control, Vec<u8>) { | |
| 262 | + | match cmd { | |
| 263 | + | Command::Transmit { control, payload } => (control, payload), | |
| 264 | + | other => panic!("expected Transmit, got {other:?}"), | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // ---- control-field parsing --------------------------------------- | |
| 269 | + | ||
| 270 | + | #[test] | |
| 271 | + | fn parses_default_action_and_missing_semicolon() { | |
| 272 | + | // No `;` means control-only, empty payload; default action is 'T'. | |
| 273 | + | let mut p = Parser::new(); | |
| 274 | + | let (control, payload) = transmit( | |
| 275 | + | p.feed(b"Gf=32,s=1,v=1") | |
| 276 | + | .expect("no chunk pending — must dispatch"), | |
| 277 | + | ); | |
| 278 | + | assert_eq!(control.action, 'T'); | |
| 279 | + | assert_eq!(control.format, Some(Format::Rgba)); | |
| 280 | + | assert_eq!(control.width_px, Some(1)); | |
| 281 | + | assert_eq!(control.height_px, Some(1)); | |
| 282 | + | assert!(payload.is_empty()); | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | #[test] | |
| 286 | + | fn parses_full_control_fields() { | |
| 287 | + | let mut p = Parser::new(); | |
| 288 | + | let body = format!("Ga=T,f=100,t=d,i=42,I=7,p=3,s=100,v=50,c=8,r=4,C=1,q=1;{}", b64(b"data")); | |
| 289 | + | let (c, payload) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 290 | + | assert_eq!(c.action, 'T'); | |
| 291 | + | assert_eq!(c.format, Some(Format::Png)); | |
| 292 | + | assert_eq!(c.medium, Some(Medium::Direct)); | |
| 293 | + | assert_eq!(c.id, Some(42)); | |
| 294 | + | assert_eq!(c.number, Some(7)); | |
| 295 | + | assert_eq!(c.placement, Some(3)); | |
| 296 | + | assert_eq!(c.width_px, Some(100)); | |
| 297 | + | assert_eq!(c.height_px, Some(50)); | |
| 298 | + | assert_eq!(c.cell_cols, Some(8)); | |
| 299 | + | assert_eq!(c.cell_rows, Some(4)); | |
| 300 | + | assert!(c.no_cursor_move); | |
| 301 | + | assert_eq!(c.quiet, 1); | |
| 302 | + | assert_eq!(payload, b"data"); | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | #[test] | |
| 306 | + | fn tolerates_missing_g_prefix() { | |
| 307 | + | // `Parser::feed` strips a leading `G` if present. Callers that | |
| 308 | + | // pre-stripped it should still work. | |
| 309 | + | let mut p = Parser::new(); | |
| 310 | + | let body = format!("a=T,f=32,s=1,v=1;{}", b64(b"xyz")); | |
| 311 | + | let (control, payload) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 312 | + | assert_eq!(control.action, 'T'); | |
| 313 | + | assert_eq!(payload, b"xyz"); | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | #[test] | |
| 317 | + | fn unknown_action_returns_none() { | |
| 318 | + | let mut p = Parser::new(); | |
| 319 | + | assert!(p.feed(b"Ga=z,f=32;YWJj").is_none()); | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | #[test] | |
| 323 | + | fn malformed_control_returns_none() { | |
| 324 | + | // No `=` in a field. | |
| 325 | + | let mut p = Parser::new(); | |
| 326 | + | assert!(p.feed(b"Ga=T,broken;YWJj").is_none()); | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | #[test] | |
| 330 | + | fn malformed_base64_returns_none() { | |
| 331 | + | let mut p = Parser::new(); | |
| 332 | + | assert!(p.feed(b"Ga=T,f=32;this-is-not-base64!!!").is_none()); | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | // ---- formats ----------------------------------------------------- | |
| 336 | + | ||
| 337 | + | #[test] | |
| 338 | + | fn format_24_is_rgb() { | |
| 339 | + | let mut p = Parser::new(); | |
| 340 | + | let body = format!("Ga=T,f=24,s=1,v=1;{}", b64(&[1, 2, 3])); | |
| 341 | + | let (c, _) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 342 | + | assert_eq!(c.format, Some(Format::Rgb)); | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | #[test] | |
| 346 | + | fn format_32_is_rgba() { | |
| 347 | + | let mut p = Parser::new(); | |
| 348 | + | let body = format!("Ga=T,f=32,s=1,v=1;{}", b64(&[1, 2, 3, 4])); | |
| 349 | + | let (c, _) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 350 | + | assert_eq!(c.format, Some(Format::Rgba)); | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | #[test] | |
| 354 | + | fn format_100_is_png() { | |
| 355 | + | let mut p = Parser::new(); | |
| 356 | + | let body = format!("Ga=T,f=100;{}", b64(b"\x89PNG")); | |
| 357 | + | let (c, _) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 358 | + | assert_eq!(c.format, Some(Format::Png)); | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | // ---- chunked reassembly ------------------------------------------ | |
| 362 | + | ||
| 363 | + | #[test] | |
| 364 | + | fn single_chunk_no_m_dispatches_immediately() { | |
| 365 | + | let mut p = Parser::new(); | |
| 366 | + | let body = format!("Ga=T,f=32,s=1,v=1;{}", b64(b"solo")); | |
| 367 | + | let (_, payload) = transmit(p.feed(body.as_bytes()).unwrap()); | |
| 368 | + | assert_eq!(payload, b"solo"); | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | #[test] | |
| 372 | + | fn two_chunks_reassemble_by_id() { | |
| 373 | + | let mut p = Parser::new(); | |
| 374 | + | let a = format!("Ga=T,f=32,s=2,v=1,i=7,m=1;{}", b64(b"HEAD")); | |
| 375 | + | let b = format!("Gi=7,m=0;{}", b64(b"TAIL")); | |
| 376 | + | assert!(p.feed(a.as_bytes()).is_none(), "first chunk should not dispatch"); | |
| 377 | + | let (c, payload) = transmit(p.feed(b.as_bytes()).unwrap()); | |
| 378 | + | assert_eq!(c.id, Some(7)); | |
| 379 | + | // format from the first chunk must persist through merge. | |
| 380 | + | assert_eq!(c.format, Some(Format::Rgba)); | |
| 381 | + | assert_eq!(c.width_px, Some(2)); | |
| 382 | + | assert_eq!(payload, b"HEADTAIL"); | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | #[test] | |
| 386 | + | fn three_chunks_reassemble() { | |
| 387 | + | let mut p = Parser::new(); | |
| 388 | + | let a = format!("Ga=T,f=32,i=1,m=1;{}", b64(b"AAA")); | |
| 389 | + | let b = format!("Gi=1,m=1;{}", b64(b"BBB")); | |
| 390 | + | let c = format!("Gi=1,m=0;{}", b64(b"CCC")); | |
| 391 | + | assert!(p.feed(a.as_bytes()).is_none()); | |
| 392 | + | assert!(p.feed(b.as_bytes()).is_none()); | |
| 393 | + | let (_, payload) = transmit(p.feed(c.as_bytes()).unwrap()); | |
| 394 | + | assert_eq!(payload, b"AAABBBCCC"); | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | #[test] | |
| 398 | + | fn concurrent_ids_do_not_cross_contaminate() { | |
| 399 | + | let mut p = Parser::new(); | |
| 400 | + | // Interleave two independent chunked transmissions. | |
| 401 | + | let a1 = format!("Ga=T,f=32,i=1,m=1;{}", b64(b"HELLO")); | |
| 402 | + | let b1 = format!("Ga=T,f=32,i=2,m=1;{}", b64(b"WORLD")); | |
| 403 | + | let a2 = format!("Gi=1,m=0;{}", b64(b"!")); | |
| 404 | + | let b2 = format!("Gi=2,m=0;{}", b64(b"?")); | |
| 405 | + | ||
| 406 | + | assert!(p.feed(a1.as_bytes()).is_none()); | |
| 407 | + | assert!(p.feed(b1.as_bytes()).is_none()); | |
| 408 | + | let (ca, pa) = transmit(p.feed(a2.as_bytes()).unwrap()); | |
| 409 | + | let (cb, pb) = transmit(p.feed(b2.as_bytes()).unwrap()); | |
| 410 | + | assert_eq!(ca.id, Some(1)); | |
| 411 | + | assert_eq!(cb.id, Some(2)); | |
| 412 | + | assert_eq!(pa, b"HELLO!"); | |
| 413 | + | assert_eq!(pb, b"WORLD?"); | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | #[test] | |
| 417 | + | fn chunked_by_number_field() { | |
| 418 | + | // Use I= (client-assigned number) instead of i=. | |
| 419 | + | let mut p = Parser::new(); | |
| 420 | + | let a = format!("Ga=T,f=32,I=99,m=1;{}", b64(b"XX")); | |
| 421 | + | let b = format!("GI=99,m=0;{}", b64(b"YY")); | |
| 422 | + | assert!(p.feed(a.as_bytes()).is_none()); | |
| 423 | + | let (_, payload) = transmit(p.feed(b.as_bytes()).unwrap()); | |
| 424 | + | assert_eq!(payload, b"XXYY"); | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // ---- delete ------------------------------------------------------ | |
| 428 | + | ||
| 429 | + | #[test] | |
| 430 | + | fn delete_returns_delete_command() { | |
| 431 | + | let mut p = Parser::new(); | |
| 432 | + | let cmd = p.feed(b"Ga=d,d=A").expect("must dispatch"); | |
| 433 | + | assert!(matches!(cmd, Command::Delete { .. })); | |
| 434 | + | } | |
| 435 | + | } |