Skip to main content

max / shop

kittygfx: cover what the mutation run said nothing observed 29 survivors and 3 timeouts of 130 mutants. Four classes, and only one of them is a coverage gap in the ordinary sense. The bounds as numbers. MAX_IN_FLIGHT_BYTES and MAX_RETAINED_BYTES can be rewritten and the parser stays self-consistent under whatever number is there, so no behavioural test disagrees. Both are now stated a second time, as plain decimals a mutated product has nothing to agree with. Ceilings inside the oracle. b64_len_of and retained_ceiling are upper bounds, and every mutation of them is looser; a correct parser satisfies a looser bound, so a passing run cannot tell. Both are named functions with tests that state the arithmetic. over_budget is the same shape from the other side: the boundary between the two MAX_IN_FLIGHT_BYTES checks is only reachable through 89 MB of base64 otherwise. Oracle bodies that return nothing. Replacing pure assertions with () is invisible to every test that passes. check_body already returned a bool for exactly this reason; check_query_response now returns whether it claimed OK, and check_chunk_equivalence how many chunks it took. The real gaps: the t=t and t=s medium spellings, the m=0 arm and the last_chunk fallback (separated by the one input where the fallback cannot fire), merge_control in the direction that observes it -- fields arriving on a later chunk, not the opener -- pending_bytes, in_flight_bytes_excluding, the clock that orders eviction, and eviction choosing the least recently advanced rather than the oldest opened. The 3 timeouts were non-termination rather than wrong answers, and a test cannot fix one: a test that fails fast still waits on the hung test beside it. Both are closed by construction instead. The chunk walk consumes at least one byte a turn, so termination is a property of the loop rather than of the width handed to it. And check_bodies asserts the in-flight cap per body rather than once at the end, which is worth having on its own: a parser that has stopped evicting gets slower with every body it takes, so checking only at the end spends the whole input finding that out. 35 tests to 55. cargo mutants -p kittygfx on astra: 139 mutants, 137 caught, 0 missed, 0 timeouts, 2 unviable. No exclusions were needed.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_013vDpLixQiknHhfHiGxFWo7
Author: Max Johnson <me@maxj.phd> · 2026-08-31 23:02 UTC
Signed with PGP, not checked
Commit: 60c26d0bee0ba1089db85f36136a05f78d2206a4
Parent: 8bd3f0e
4 files changed, +449 insertions, -35 deletions
M Cargo.lock +17 -17
@@ -770,7 +770,7 @@
770 770
771 771 [[package]]
772 772 name = "kittygfx"
773 - version = "0.1.0"
773 + version = "0.1.1"
774 774 dependencies = [
775 775 "base64",
776 776 "tracing",
@@ -2383,20 +2383,12 @@
2383 2383 ]
2384 2384
2385 2385 [[patch.unused]]
2386 - name = "kberg"
2387 - version = "0.1.0"
2386 + name = "synckit-client"
2387 + version = "0.10.0"
2388 2388
2389 2389 [[patch.unused]]
2390 - name = "ops-status"
2391 - version = "0.1.0"
2392 -
2393 - [[patch.unused]]
2394 - name = "painhours"
2395 - version = "0.1.0"
2396 -
2397 - [[patch.unused]]
2398 - name = "tagtree"
2399 - version = "0.4.1"
2390 + name = "synckit-config"
2391 + version = "0.2.0"
2400 2392
2401 2393 [[patch.unused]]
2402 2394 name = "quasi-axum"
@@ -2439,9 +2431,17 @@
2439 2431 version = "0.7.0"
2440 2432
2441 2433 [[patch.unused]]
2442 - name = "synckit-client"
2443 - version = "0.10.0"
2434 + name = "kberg"
2435 + version = "0.1.0"
2444 2436
2445 2437 [[patch.unused]]
2446 - name = "synckit-config"
2447 - version = "0.2.0"
2438 + name = "ops-status"
2439 + version = "0.1.0"
2440 +
2441 + [[patch.unused]]
2442 + name = "painhours"
2443 + version = "0.1.0"
2444 +
2445 + [[patch.unused]]
2446 + name = "tagtree"
2447 + version = "0.4.1"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "kittygfx"
3 - version = "0.1.0"
3 + version = "0.1.1"
4 4 description = "Rendering-agnostic implementation of the Kitty terminal graphics protocol"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -344,7 +344,7 @@
344 344 if !control.more_chunks && !self.partial.contains_key(&key) {
345 345 // Single-shot — no reassembly needed. Decode into a fresh Vec
346 346 // sized to the payload; the caller owns the result.
347 - if decoded_upper_bound(payload_b64) > MAX_IN_FLIGHT_BYTES {
347 + if over_budget(decoded_upper_bound(payload_b64)) {
348 348 return None;
349 349 }
350 350 let payload = if payload_b64.is_empty() {
@@ -382,7 +382,7 @@
382 382 // Before the resize, not after: the arithmetic below runs over a
383 383 // length the writer chose, and the resize is what commits it.
384 384 let after = held_elsewhere.saturating_add(start).saturating_add(extra);
385 - if entry.overflowed || after > MAX_IN_FLIGHT_BYTES {
385 + if entry.overflowed || over_budget(after) {
386 386 entry.overflowed = true;
387 387 } else {
388 388 entry.payload.resize(start + extra, 0);
@@ -460,6 +460,17 @@
460 460 payload_b64.len().div_ceil(4) * 3
461 461 }
462 462
463 + /// Whether `bytes` of decoded payload is more than the in-flight budget allows.
464 + ///
465 + /// A named decision rather than two inline `>` comparisons, because the
466 + /// boundary is only reachable through 89 MB of base64 otherwise: the mutant
467 + /// that widens it to `>=` differs from the original at exactly
468 + /// [`MAX_IN_FLIGHT_BYTES`] and nowhere else. Here a test can state the boundary
469 + /// directly, and both callers get it from one place.
470 + fn over_budget(bytes: usize) -> bool {
471 + bytes > MAX_IN_FLIGHT_BYTES
472 + }
473 +
463 474 /// Split at the first `sep`. Returns `(before, None)` when no `sep` byte is
464 475 /// present — the caller uses that to distinguish "empty payload after `;`"
465 476 /// from "no `;` at all".
@@ -558,6 +569,239 @@
558 569 }
559 570 }
560 571
572 + // ---- the bounds as numbers, and the accounting that reads them ------
573 + //
574 + // A cap the parser stays self-consistent under is invisible to every
575 + // behavioural test: the parser enforces whatever number is there. These
576 + // state the numbers and the arithmetic a second time, which is the only
577 + // way a change to either is a disagreement rather than a new baseline.
578 +
579 + #[test]
580 + fn the_in_flight_budget_is_sixty_four_mebibytes() {
581 + // Written as a plain decimal so that a mutant rewriting the product in
582 + // the source has nothing here to agree with.
583 + assert_eq!(MAX_IN_FLIGHT_BYTES, 67_108_864);
584 + }
585 +
586 + #[test]
587 + fn the_budget_boundary_admits_exactly_the_budget() {
588 + // `>` vs `>=` disagree at this one value and nowhere else, and the
589 + // input that would reach it through feed() is 89 MB of base64.
590 + assert!(!over_budget(MAX_IN_FLIGHT_BYTES));
591 + assert!(over_budget(MAX_IN_FLIGHT_BYTES + 1));
592 + assert!(!over_budget(MAX_IN_FLIGHT_BYTES - 1));
593 + }
594 +
595 + /// A parser holding one unfinished chunked transmission of `payload`.
596 + fn parser_holding(payload: &[u8]) -> Parser {
597 + let mut p = Parser::new();
598 + let body = format!("Ga=T,f=32,s=64,v=64,i=7,m=1;{}", b64(payload));
599 + assert!(
600 + p.feed(body.as_bytes()).is_none(),
601 + "an opening chunk emits nothing"
602 + );
603 + p
604 + }
605 +
606 + #[test]
607 + fn pending_bytes_counts_the_map_spine_and_every_payload() {
608 + let p = parser_holding(&[0xAB; 3000]);
609 + // The arithmetic stated twice: a mutant that turns the product into a
610 + // sum, or the whole body into a constant, disagrees with this.
611 + let expected = p.partial.capacity() * std::mem::size_of::<(PartialKey, Partial)>()
612 + + p.partial
613 + .values()
614 + .map(|x| x.payload.capacity())
615 + .sum::<usize>();
616 + assert_eq!(p.pending_bytes(), expected);
617 + assert!(
618 + p.pending_bytes() >= 3000,
619 + "a transmission holding 3000 bytes cannot report fewer"
620 + );
621 + }
622 +
623 + #[test]
624 + fn in_flight_bytes_excluding_leaves_out_the_named_transmission_only() {
625 + let mut p = Parser::new();
626 + for (id, len) in [(7u32, 3000usize), (9, 6000)] {
627 + let body = format!("Ga=T,f=32,s=64,v=64,i={id},m=1;{}", b64(&vec![0xCD; len]));
628 + assert!(p.feed(body.as_bytes()).is_none());
629 + }
630 + // Two transmissions of different sizes, so excluding the wrong one, or
631 + // returning a constant, gives a different answer from all of these.
632 + let seven = p.in_flight_bytes_excluding(PartialKey::ById(7));
633 + let nine = p.in_flight_bytes_excluding(PartialKey::ById(9));
634 + assert_eq!(seven, 6000, "excluding 7 must leave 9's bytes");
635 + assert_eq!(nine, 3000, "excluding 9 must leave 7's bytes");
636 + assert_eq!(
637 + p.in_flight_bytes_excluding(PartialKey::Anon),
638 + 9000,
639 + "excluding a key that is not there leaves both"
640 + );
641 + }
642 +
643 + #[test]
644 + fn the_map_never_holds_more_than_the_in_flight_cap() {
645 + // Eviction is what keeps an id-space attack bounded, and nothing else
646 + // in the suite makes the map grow past the cap.
647 + let mut p = Parser::new();
648 + let over = MAX_IN_FLIGHT_TRANSMISSIONS + 24;
649 + for id in 0..over {
650 + let body = format!("Ga=T,f=32,s=8,v=8,i={id},m=1;{}", b64(&[0x11; 12]));
651 + assert!(p.feed(body.as_bytes()).is_none());
652 + assert!(
653 + p.pending_transmissions() <= MAX_IN_FLIGHT_TRANSMISSIONS,
654 + "{} transmissions in flight after opening {}",
655 + p.pending_transmissions(),
656 + id + 1
657 + );
658 + }
659 + assert_eq!(
660 + p.pending_transmissions(),
661 + MAX_IN_FLIGHT_TRANSMISSIONS,
662 + "{over} openings must leave exactly the cap behind"
663 + );
664 + }
665 +
666 + // ---- control fields nothing was reading -----------------------------
667 +
668 + #[test]
669 + fn every_medium_spelling_parses_to_its_own_variant() {
670 + // The media the parser does not act on are still parsed, and a deleted
671 + // arm makes one of them read as "no medium given", which the query
672 + // answer treats as Direct and says OK to.
673 + for (spelling, want) in [
674 + ("d", Medium::Direct),
675 + ("f", Medium::File),
676 + ("t", Medium::TempFile),
677 + ("s", Medium::Shm),
678 + ] {
679 + let c = parse_control(format!("a=q,t={spelling}").as_bytes())
680 + .unwrap_or_else(|| panic!("t={spelling} did not parse"));
681 + assert_eq!(c.medium, Some(want), "t={spelling}");
682 + }
683 + assert_eq!(
684 + parse_control(b"a=q,t=z").unwrap().medium,
685 + None,
686 + "an unknown medium is None, not a default"
687 + );
688 + }
689 +
690 + #[test]
691 + fn a_body_with_no_m_field_is_its_own_last_chunk() {
692 + // The fallback at the end of parse_control. Without it a single-shot
693 + // body reports itself unfinished.
694 + let c = parse_control(b"a=T,f=32,s=1,v=1").unwrap();
695 + assert!(c.last_chunk, "a body with no m= is complete by itself");
696 + assert!(!c.more_chunks);
697 + }
698 +
699 + #[test]
700 + fn m_zero_sets_last_chunk_even_when_m_one_came_first() {
701 + // The only input that separates the `m=0` arm from the fallback: the
702 + // fallback cannot fire, because more_chunks is set.
703 + let c = parse_control(b"a=T,m=1,m=0").unwrap();
704 + assert!(c.last_chunk, "m=0 says last chunk in its own right");
705 + assert!(c.more_chunks);
706 + }
707 +
708 + #[test]
709 + fn the_clock_ticks_once_per_accepted_chunk() {
710 + // The clock is what orders the map for eviction. Frozen, every entry
711 + // carries the same last_advanced and eviction picks by hash order
712 + // instead of by age -- which no assertion about *how many* entries
713 + // survive can see.
714 + let mut p = Parser::new();
715 + for id in 0..5u32 {
716 + let body = format!("Ga=T,f=32,s=8,v=8,i={id},m=1;{}", b64(&[0x22; 12]));
717 + assert!(p.feed(body.as_bytes()).is_none());
718 + }
719 + assert_eq!(p.clock, 5, "one tick per chunk accepted, and no more");
720 + }
721 +
722 + #[test]
723 + fn eviction_drops_the_least_recently_advanced_transmission() {
724 + // Not the oldest-opened: a transmission that is still being fed is
725 + // live, and dropping it in favour of one that has sat untouched is the
726 + // behaviour the clock exists to prevent.
727 + let mut p = Parser::new();
728 + for id in 0..MAX_IN_FLIGHT_TRANSMISSIONS as u32 {
729 + let body = format!("Ga=T,f=32,s=8,v=8,i={id},m=1;{}", b64(&[0x33; 12]));
730 + assert!(p.feed(body.as_bytes()).is_none());
731 + }
732 + // Advance the one opened first, so it is no longer the stalest.
733 + assert!(
734 + p.feed(format!("Gi=0,m=1;{}", b64(&[0x44; 12])).as_bytes())
735 + .is_none()
736 + );
737 + // Opening one more has to evict, and 1 is now the stalest.
738 + assert!(
739 + p.feed(format!("Ga=T,f=32,s=8,v=8,i=99,m=1;{}", b64(&[0x55; 12])).as_bytes())
740 + .is_none()
741 + );
742 + assert!(
743 + p.partial.contains_key(&PartialKey::ById(0)),
744 + "the transmission that was still being fed must survive"
745 + );
746 + assert!(
747 + !p.partial.contains_key(&PartialKey::ById(1)),
748 + "the least recently advanced one is what goes"
749 + );
750 + assert_eq!(p.pending_transmissions(), MAX_IN_FLIGHT_TRANSMISSIONS);
751 + }
752 +
753 + #[test]
754 + fn a_later_chunk_can_carry_fields_the_opener_left_out() {
755 + // merge_control fills the STORED control from the current chunk, so
756 + // this is the direction that observes it: an opener that named no
757 + // format, and a closer that does.
758 + let payload = [0x6Eu8; 96];
759 + let encoded = b64(&payload);
760 + let (head, tail) = encoded.split_at(16);
761 + let mut p = Parser::new();
762 + assert!(p.feed(format!("Ga=T,i=12,m=1;{head}").as_bytes()).is_none());
763 + let cmd = p
764 + .feed(format!("Gi=12,f=32,s=4,v=8,C=1,m=0;{tail}").as_bytes())
765 + .expect("the closing chunk completes the transmission");
766 + let (control, got) = transmit(cmd);
767 + assert_eq!(got, payload);
768 + assert_eq!(
769 + control.format,
770 + Some(Format::Rgba),
771 + "f= arrived on the closing chunk and must be kept"
772 + );
773 + assert_eq!(control.width_px, Some(4));
774 + assert_eq!(control.height_px, Some(8));
775 + assert!(control.no_cursor_move);
776 + }
777 +
778 + #[test]
779 + fn a_continuation_chunk_inherits_the_opening_control() {
780 + // merge_control is what carries format and geometry from the opening
781 + // chunk to the command; a later chunk repeats only `i=` and `m=`.
782 + let payload = [0x7Fu8; 96];
783 + let encoded = b64(&payload);
784 + let (head, tail) = encoded.split_at(16);
785 + let mut p = Parser::new();
786 + assert!(
787 + p.feed(format!("Ga=T,f=32,s=4,v=8,C=1,i=11,m=1;{head}").as_bytes())
788 + .is_none()
789 + );
790 + let cmd = p
791 + .feed(format!("Gi=11,m=0;{tail}").as_bytes())
792 + .expect("the closing chunk completes the transmission");
793 + let (control, got) = transmit(cmd);
794 + assert_eq!(got, payload);
795 + assert_eq!(
796 + control.format,
797 + Some(Format::Rgba),
798 + "f= came from the opener"
799 + );
800 + assert_eq!(control.width_px, Some(4), "s= came from the opener");
801 + assert_eq!(control.height_px, Some(8), "v= came from the opener");
802 + assert!(control.no_cursor_move, "C= came from the opener");
803 + }
804 +
561 805 // ---- control-field parsing ---------------------------------------
562 806
563 807 #[test]
@@ -60,13 +60,17 @@
60 60
61 61 /// Panics if `control`'s query answer is not one a client can use.
62 62 ///
63 + /// Returns whether the answer claimed OK, for the same reason [`check_body`]
64 + /// returns a bool: a body of pure assertions replaced by `()` is invisible to
65 + /// every test that passes, so the oracle has to hand back something.
66 + ///
63 67 /// # Panics
64 68 ///
65 69 /// By design. It is an oracle, and a panic is how it reports.
66 70 // The reply is tens of bytes long, so the bytecount crate would be a
67 71 // dependency bought for nothing.
68 72 #[allow(clippy::naive_bytecount)]
69 - pub fn check_query_response(control: &Control) {
73 + pub fn check_query_response(control: &Control) -> bool {
70 74 let reply = query_response(control);
71 75 assert!(
72 76 reply.starts_with(b"\x1b_G"),
@@ -100,30 +104,53 @@
100 104 "query answered {text:?} for format {:?} medium {:?}",
101 105 control.format, control.medium
102 106 );
107 + claims_ok
103 108 }
104 109
105 110 /// Panics if the same payload sent as chunks does not arrive as the same bytes.
106 111 ///
112 + /// Returns how many chunks it took, for the same reason as the rest of this
113 + /// module: an oracle that returns nothing cannot be observed to have run.
114 + ///
107 115 /// # Panics
108 116 ///
109 117 /// By design.
110 - fn check_chunk_equivalence(action: char, payload: &[u8]) {
118 + /// How wide a chunk to cut the encoded payload into: roughly a third of it,
119 + /// rounded to a whole number of base64 quanta.
120 + ///
121 + /// Chunk boundaries are multiples of four, which is what the protocol requires
122 + /// of a sender: each chunk is decoded on its own, so a boundary inside a quantum
123 + /// would not be decodable by any implementation.
124 + ///
125 + /// The floor of one quantum is load-bearing rather than defensive. The walk
126 + /// below consumes `chunk_len` bytes per turn, so a width of zero does not
127 + /// terminate, and the arithmetic here is the only thing that could produce one.
128 + /// Named and floored, it is a thing a test can state instead of a mutant that
129 + /// spends five minutes not answering.
130 + fn chunk_len_for(encoded_len: usize) -> usize {
131 + let quanta = encoded_len.div_ceil(4);
132 + (4 * quanta.div_ceil(3)).max(4)
133 + }
134 +
135 + fn check_chunk_equivalence(action: char, payload: &[u8]) -> usize {
111 136 if payload.is_empty() {
112 - return;
137 + return 0;
113 138 }
114 139 let encoded = B64.encode(payload);
115 - // Chunk boundaries are multiples of four, which is what the protocol
116 - // requires of a sender: each chunk is decoded on its own, so a boundary
117 - // inside a quantum would not be decodable by any implementation.
118 - let quanta = encoded.len().div_ceil(4);
119 - let chunk_len = 4 * quanta.div_ceil(3).max(1);
140 + let chunk_len = chunk_len_for(encoded.len());
120 141
121 142 let mut p = Parser::new();
122 143 let mut first = true;
123 144 let mut rest = encoded.as_str();
124 145 let mut got = None;
146 + let mut chunks = 0;
125 147 while !rest.is_empty() {
126 - let take = chunk_len.min(rest.len());
148 + chunks += 1;
149 + // At least one byte, whatever the width says. The walk's termination
150 + // is then a property of the loop rather than of the arithmetic above
151 + // it, so a width that comes back wrong is a failed assertion below
152 + // instead of a run that never ends.
153 + let take = chunk_len.clamp(1, rest.len());
127 154 let (head, tail) = rest.split_at(take);
128 155 rest = tail;
129 156 let more = if rest.is_empty() { "0" } else { "1" };
@@ -162,6 +189,7 @@
162 189 0,
163 190 "a completed transmission was left in flight"
164 191 );
192 + chunks
165 193 }
166 194
167 195 /// Feed one APC body to a fresh parser and hold what comes out to everything
@@ -175,6 +203,25 @@
175 203 /// # Panics
176 204 ///
177 205 /// By design, on any violation.
206 + /// The base64 characters in `body`: everything after the first `;`.
207 + ///
208 + /// Named because the bound it feeds is an upper one, and every mutation of this
209 + /// arithmetic makes it looser. A looser upper bound is still satisfied by a
210 + /// correct decode, so nothing in a passing run disagrees with a mutant here.
211 + fn b64_len_of(body: &[u8]) -> usize {
212 + body.iter()
213 + .position(|&b| b == b';')
214 + .map_or(0, |i| body.len() - i - 1)
215 + }
216 +
217 + /// The most a parser may retain after `input_len` bytes of input.
218 + ///
219 + /// Same reason as [`b64_len_of`]: it is a ceiling, so every mutation raises it
220 + /// and a well-behaved parser stays under both.
221 + fn retained_ceiling(input_len: usize) -> usize {
222 + RETAINED_BASE_BYTES + input_len.saturating_mul(MAX_RETAINED_PER_INPUT_BYTE)
223 + }
224 +
178 225 pub fn check_body(body: &[u8]) -> bool {
179 226 let mut p = Parser::new();
180 227 let Some(cmd) = p.feed(body) else {
@@ -189,15 +236,14 @@
189 236 );
190 237
191 238 match &cmd {
192 - Command::Query { control } => check_query_response(control),
239 + Command::Query { control } => {
240 + check_query_response(control);
241 + }
193 242 Command::Transmit { control, payload } | Command::FrameAppend { control, payload } => {
194 243 // base64 carries three bytes in every four characters, so a decoded
195 244 // payload longer than that is the accumulator's own arithmetic
196 245 // disagreeing with itself.
197 - let b64_len = body
198 - .iter()
199 - .position(|&b| b == b';')
200 - .map_or(0, |i| body.len() - i - 1);
246 + let b64_len = b64_len_of(body);
201 247 assert!(
202 248 payload.len() <= b64_len.div_ceil(4) * 3,
203 249 "decoded {} bytes out of {b64_len} base64 characters",
@@ -245,10 +291,19 @@
245 291 // Every body is also checked on its own, where a completed command can
246 292 // be held to the properties that need one.
247 293 check_body(piece);
294 + // Per body, not only at the end. The cap is what keeps the map from
295 + // growing with the id space, and a parser that has stopped enforcing
296 + // it gets slower with every body it takes: checked once at the end,
297 + // the oracle spends the whole input finding that out.
298 + assert!(
299 + session.pending_transmissions() <= crate::MAX_IN_FLIGHT_TRANSMISSIONS,
300 + "{} transmissions in flight after {fed} bodies, over the cap",
301 + session.pending_transmissions()
302 + );
248 303 }
249 304
250 305 let retained = session.pending_bytes();
251 - let ceiling = RETAINED_BASE_BYTES + input.len().saturating_mul(MAX_RETAINED_PER_INPUT_BYTE);
306 + let ceiling = retained_ceiling(input.len());
252 307 assert!(
253 308 retained <= ceiling,
254 309 "parser holds {retained} bytes after {} bytes of input, over the {ceiling} ceiling",
@@ -265,3 +320,118 @@
265 320 );
266 321 fed
267 322 }
323 +
324 + #[cfg(test)]
325 + mod tests {
326 + //! Unit tests for the oracle's own decisions.
327 + //!
328 + //! The oracle asserts things about the crate; nothing asserted anything
329 + //! about the oracle. Every function below is either a ceiling the crate is
330 + //! held to, or a piece of arithmetic that decides how much of the input the
331 + //! oracle looks at. Neither can be observed through a passing run: a looser
332 + //! ceiling is still satisfied by a correct parser, and a narrower walk still
333 + //! agrees with everything it did check.
334 +
335 + use super::{
336 + MAX_RETAINED_BYTES, RETAINED_BASE_BYTES, b64_len_of, check_body, check_chunk_equivalence,
337 + check_query_response, chunk_len_for, retained_ceiling,
338 + };
339 + use crate::{Control, Format, Medium};
340 + use base64::{Engine, engine::general_purpose::STANDARD as B64};
341 +
342 + #[test]
343 + fn the_absolute_retention_ceiling_is_two_budgets_and_a_slack() {
344 + // Written out, so a mutant rewriting the expression in the source has
345 + // nothing here to agree with.
346 + assert_eq!(MAX_RETAINED_BYTES, 134_283_264);
347 + assert_eq!(RETAINED_BASE_BYTES, 4096);
348 + }
349 +
350 + #[test]
351 + fn the_per_input_ceiling_is_the_base_plus_four_per_byte() {
352 + assert_eq!(retained_ceiling(1000), 8096);
353 + assert_eq!(retained_ceiling(0), RETAINED_BASE_BYTES);
354 + }
355 +
356 + #[test]
357 + fn the_chunk_width_is_a_third_of_the_payload_in_whole_quanta() {
358 + // 128 encoded characters is 32 quanta; a third of that, rounded up, is
359 + // 11 quanta of four bytes each.
360 + assert_eq!(chunk_len_for(128), 44);
361 + }
362 +
363 + #[test]
364 + fn the_chunk_width_never_falls_below_one_quantum() {
365 + // The walk consumes chunk_len bytes a turn, so a zero here does not
366 + // terminate. This is the floor that makes that unrepresentable.
367 + assert_eq!(chunk_len_for(0), 4);
368 + assert_eq!(chunk_len_for(4), 4);
369 + assert!(chunk_len_for(3) >= 4);
370 + }
371 +
372 + #[test]
373 + fn the_base64_length_is_what_follows_the_first_semicolon() {
374 + assert_eq!(b64_len_of(b"a=T,f=32;QUJD"), 4);
375 + assert_eq!(
376 + b64_len_of(b"a=T,f=32"),
377 + 0,
378 + "a body with no payload marker carries no base64"
379 + );
380 + assert_eq!(
381 + b64_len_of(b"a=T;QUJD;RUZH"),
382 + 9,
383 + "the FIRST semicolon is the separator; later ones are payload"
384 + );
385 + }
386 +
387 + #[test]
388 + fn chunk_equivalence_reports_how_many_chunks_it_took() {
389 + // 96 bytes encode to 128 characters, which is three chunks of 44.
390 + assert_eq!(check_chunk_equivalence('T', &[0x5A; 96]), 3);
391 + assert_eq!(
392 + check_chunk_equivalence('T', &[]),
393 + 0,
394 + "an empty payload is not chunked at all"
395 + );
396 + }
397 +
398 + #[test]
399 + fn check_body_reports_whether_a_command_came_out() {
400 + let body = format!("Ga=T,f=32,s=2,v=2;{}", B64.encode([0x11u8; 16]));
401 + assert!(check_body(body.as_bytes()), "a valid transmit yields one");
402 + assert!(
403 + !check_body(b"Gnot-a-command"),
404 + "a body that parses to nothing yields none"
405 + );
406 + }
407 +
408 + #[test]
409 + fn the_query_answer_says_ok_only_for_a_medium_the_host_can_serve() {
410 + let direct = Control {
411 + action: 'q',
412 + format: Some(Format::Rgba),
413 + medium: Some(Medium::Direct),
414 + id: Some(4242),
415 + ..Control::default()
416 + };
417 + assert!(check_query_response(&direct), "inline base64 is servable");
418 +
419 + let from_file = Control {
420 + medium: Some(Medium::File),
421 + ..direct.clone()
422 + };
423 + assert!(
424 + !check_query_response(&from_file),
425 + "a file transfer must not be answered OK"
426 + );
427 +
428 + let no_format = Control {
429 + format: None,
430 + ..direct
431 + };
432 + assert!(
433 + !check_query_response(&no_format),
434 + "a query naming no format is not something to say OK to"
435 + );
436 + }
437 + }