Skip to main content

max / shop

shop-vt: cover the state machine the mutation run said nothing observed A first cargo-mutants run over shop-vt reported 87 survivors of 191. The parser had 27 tests and they concentrated on Ground, CSI dispatch and the two body caps; the DCS states had no test of any kind, and neither did the four accounting functions the soak oracle reads. 29 new tests. The classes they close: - The bounds as numbers. MAX_STRING_BYTES could become 8 + 1024 + 1024 and the parser stays self-consistent under it, which is why no behavioural test reaches it. - Params::{len, is_empty, clear, footprint} and Parser::{in_ground, buffered_bytes, clear}. All public or oracle-facing, none called by a test, so each could return a constant. footprint and buffered_bytes state their arithmetic twice -- once in the source, once in the test -- so a change to either side disagrees. - The slot accounting for subparameters, which only shows at the cap. - Every DCS state: hook, passthrough, both parameter states, both routes into DcsIgnore, and coming back out of it. A DcsIgnore that never ends swallows the rest of the stream and nothing said otherwise. - The C0, intermediate and out-of-order-byte arms across escape, csi and dcs, and the inverted `!` on every slot-overflow guard -- dropping it marks every ordinary sequence as ignored. .cargo/mutants.toml carries the class no test can kill, with the reason each: the eight Perform default bodies (a `let _ = args;` discard IS `()`), the match arms whose action is `{}` and whose fall-through is the same `{}`, and the ESC-in-Escape clear, which is unreachable holding anything to clear because every route into Escape clears on the way in.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01DwpiantpUgohzML4xr6KeQ
Author: Max Johnson <me@maxj.phd> · 2026-08-31 15:28 UTC
Signed with PGP, not checked
Commit: 48783efcd228a3061b31025301d9b8b768fe4fbd
Parent: 6d89817
2 files changed, +471 insertions, -0 deletions
@@ -1160,4 +1160,404 @@
1160 1160 assert!(!r.0.iter().any(|e| e.starts_with("apc(")), "got {:?}", r.0);
1161 1161 assert!(p.buffered_bytes() < 8 * 1024);
1162 1162 }
1163 +
1164 + // ---- The bounds, as numbers ---------------------------------------
1165 +
1166 + /// Every one of these is written as an arithmetic expression, and an
1167 + /// expression nothing asserts is a number nothing pins: mutation found that
1168 + /// `8 * 1024 * 1024` could become `8 + 1024 + 1024` and no test noticed.
1169 + /// The parser stays self-consistent under that change, which is exactly why
1170 + /// its own behavioural tests cannot catch it.
1171 + #[test]
1172 + fn the_bounds_are_the_numbers_the_module_documents() {
1173 + assert_eq!(MAX_PARAMS, 32, "vte's number, so we are a drop-in for it");
1174 + assert_eq!(MAX_STRING_BYTES, 8_388_608, "8 MiB");
1175 + assert_eq!(MAX_OSC_PARAMS, 1024);
1176 + assert_eq!(INITIAL_BODY_CAPACITY, 2048);
1177 + }
1178 +
1179 + // ---- Params, directly ----------------------------------------------
1180 +
1181 + /// `len` and `is_empty` are public and nothing called them, so the whole
1182 + /// accessor pair could return a constant unnoticed. `clear` is the other
1183 + /// half: a parameter list that does not empty carries one sequence's
1184 + /// parameters into the next.
1185 + #[test]
1186 + fn params_len_and_is_empty_track_the_open_groups() {
1187 + let mut p = Params::default();
1188 + assert!(p.is_empty());
1189 + assert_eq!(p.len(), 0);
1190 +
1191 + assert!(p.new_param());
1192 + assert!(!p.is_empty());
1193 + assert_eq!(p.len(), 1);
1194 +
1195 + assert!(p.new_param());
1196 + assert_eq!(p.len(), 2);
1197 +
1198 + p.clear();
1199 + assert!(p.is_empty());
1200 + assert_eq!(p.len(), 0);
1201 + }
1202 +
1203 + /// A subparameter costs a slot exactly as a parameter does, or `38:2::R:G:B`
1204 + /// repeated is a cap that does not bind. The expression under test is
1205 + /// `slots += 1`; `slots *= 1` leaves it at its opening value forever, which
1206 + /// nothing observes without pushing the list to the cap.
1207 + #[test]
1208 + fn subparameters_consume_slots_so_the_cap_still_binds() {
1209 + let mut p = Params::default();
1210 + assert!(p.new_param(), "the first slot");
1211 + for i in 1..MAX_PARAMS {
1212 + assert!(p.new_subparam(), "slot {} of {MAX_PARAMS}", i + 1);
1213 + }
1214 + assert!(
1215 + !p.new_subparam(),
1216 + "slot {} is past the cap and must be refused",
1217 + MAX_PARAMS + 1
1218 + );
1219 + }
1220 +
1221 + /// `footprint` feeds [`Parser::buffered_bytes`], which is what the soak
1222 + /// oracle asserts an amplification ceiling against. A footprint that
1223 + /// under-reports is an oracle that cannot fire.
1224 + ///
1225 + /// The expected value is written out here rather than read from the
1226 + /// function, so the arithmetic is stated twice and a change to either side
1227 + /// disagrees.
1228 + #[test]
1229 + fn footprint_counts_the_outer_vec_and_every_group() {
1230 + let p = Params::default();
1231 + assert_eq!(p.footprint(), 0, "an unused list holds nothing");
1232 +
1233 + let mut p = Params::default();
1234 + assert!(p.new_param());
1235 + assert!(p.new_subparam());
1236 + assert!(p.new_param());
1237 +
1238 + let outer = p.inner.capacity();
1239 + let groups: usize = p.inner.iter().map(Vec::capacity).sum();
1240 + assert!(outer > 0 && groups > 0, "the case has to be non-degenerate");
1241 + assert_eq!(
1242 + p.footprint(),
1243 + outer * std::mem::size_of::<Vec<u16>>() + groups * std::mem::size_of::<u16>()
1244 + );
1245 + }
1246 +
1247 + // ---- Parser accounting ---------------------------------------------
1248 +
1249 + /// `in_ground` is how a caller knows a chunk boundary is safe to cut on,
1250 + /// and it is what the fuzz oracle checks after a terminated sequence. A
1251 + /// constant `true` makes both of those say yes mid-sequence.
1252 + #[test]
1253 + fn in_ground_is_false_while_a_sequence_is_open() {
1254 + let mut p = Parser::new();
1255 + let mut r = Rec::default();
1256 + assert!(p.in_ground(), "a fresh parser holds nothing");
1257 +
1258 + p.advance(&mut r, b"\x1b[1");
1259 + assert!(!p.in_ground(), "mid-CSI");
1260 +
1261 + p.advance(&mut r, b"m");
1262 + assert!(p.in_ground(), "the sequence terminated");
1263 + }
1264 +
1265 + /// The same twice-stated arithmetic as `footprint`, for the total the soak
1266 + /// oracle actually reads.
1267 + #[test]
1268 + fn buffered_bytes_sums_every_buffer_the_parser_holds() {
1269 + let mut p = Parser::new();
1270 + let mut r = Rec::default();
1271 + // An open OSC with fields, so all four terms are non-zero.
1272 + p.advance(&mut r, b"\x1b]52;c;aGVsbG8=");
1273 +
1274 + let expected = p.osc_buf.capacity()
1275 + + p.apc_buf.capacity()
1276 + + p.osc_params.capacity() * std::mem::size_of::<(usize, usize)>()
1277 + + p.params.footprint();
1278 + assert!(expected > 1, "the case has to distinguish 0 and 1");
1279 + assert_eq!(p.buffered_bytes(), expected);
1280 + }
1281 +
1282 + /// `clear` empties the intermediates, the ignore flag and the parameters
1283 + /// when a fresh sequence starts. Without it one sequence's parameters are
1284 + /// dispatched as the next one's.
1285 + #[test]
1286 + fn a_fresh_sequence_does_not_inherit_the_last_ones_parameters() {
1287 + assert_eq!(
1288 + run(b"\x1b[1;2m\x1b[m"),
1289 + vec![
1290 + "csi(params=[[1], [2]], intermediates=[], ignore=false, action='m')",
1291 + "csi(params=[], intermediates=[], ignore=false, action='m')",
1292 + ]
1293 + );
1294 + }
1295 +
1296 + // ---- Ground --------------------------------------------------------
1297 +
1298 + /// CAN and SUB abort a sequence and are executed in Ground like any other
1299 + /// C0. They are their own match arm, so deleting it drops them silently.
1300 + #[test]
1301 + fn can_and_sub_execute_in_ground() {
1302 + assert_eq!(run(b"\x18"), vec!["exec(0x18)"]);
1303 + assert_eq!(run(b"\x1a"), vec!["exec(0x1a)"]);
1304 + }
1305 +
1306 + // ---- Escape --------------------------------------------------------
1307 +
1308 + #[test]
1309 + fn a_c0_inside_escape_executes_without_ending_the_sequence() {
1310 + assert_eq!(
1311 + run(b"\x1b\rB"),
1312 + vec![
1313 + "exec(0x0d)",
1314 + "esc(intermediates=[], ignore=false, byte=0x42)"
1315 + ]
1316 + );
1317 + }
1318 +
1319 + #[test]
1320 + fn an_escape_intermediate_reaches_the_dispatch() {
1321 + assert_eq!(
1322 + run(b"\x1b(B"),
1323 + vec!["esc(intermediates=[40], ignore=false, byte=0x42)"]
1324 + );
1325 + }
1326 +
1327 + /// A second intermediate accumulates rather than replacing the first.
1328 + #[test]
1329 + fn escape_intermediates_accumulate() {
1330 + assert_eq!(
1331 + run(b"\x1b($B"),
1332 + vec!["esc(intermediates=[40, 36], ignore=false, byte=0x42)"]
1333 + );
1334 + }
1335 +
1336 + #[test]
1337 + fn a_c0_inside_an_escape_intermediate_executes() {
1338 + assert_eq!(
1339 + run(b"\x1b(\rB"),
1340 + vec![
1341 + "exec(0x0d)",
1342 + "esc(intermediates=[40], ignore=false, byte=0x42)"
1343 + ]
1344 + );
1345 + }
1346 +
1347 + /// SOS (0x58) and PM (0x5E) open a string the parser treats exactly as APC
1348 + /// does: consumed, and delivered through the same callback. They share an
1349 + /// arm with nothing, so deleting it leaves the parser sitting in Escape
1350 + /// eating the body as escape finals.
1351 + #[test]
1352 + fn sos_and_pm_open_a_string_and_dispatch_it() {
1353 + assert_eq!(
1354 + run(b"\x1bXhi\x1b\\"),
1355 + vec![
1356 + "apc([104, 105])",
1357 + "esc(intermediates=[], ignore=false, byte=0x5c)"
1358 + ]
1359 + );
1360 + assert_eq!(
1361 + run(b"\x1b^hi\x1b\\"),
1362 + vec![
1363 + "apc([104, 105])",
1364 + "esc(intermediates=[], ignore=false, byte=0x5c)"
1365 + ]
1366 + );
1367 + }
1368 +
1369 + // ---- CSI -----------------------------------------------------------
1370 +
1371 + #[test]
1372 + fn a_c0_inside_csi_entry_executes() {
1373 + assert_eq!(
1374 + run(b"\x1b[\rm"),
1375 + vec![
1376 + "exec(0x0d)",
1377 + "csi(params=[], intermediates=[], ignore=false, action='m')"
1378 + ]
1379 + );
1380 + }
1381 +
1382 + /// A colon opening a CSI opens a subparameter group, so the sequence
1383 + /// carries one empty slot rather than none. Deleting the arm makes the
1384 + /// colon vanish and the dispatch carry no parameters at all.
1385 + #[test]
1386 + fn a_leading_colon_opens_a_subparameter_slot() {
1387 + assert_eq!(
1388 + run(b"\x1b[:m"),
1389 + vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"]
1390 + );
1391 + }
1392 +
1393 + /// The overflow flag is set when a slot is REFUSED, not when one is taken.
1394 + /// Dropping the `!` inverts that, and every ordinary sequence starts
1395 + /// reporting itself as ignored.
1396 + #[test]
1397 + fn opening_a_slot_that_succeeds_does_not_mark_the_sequence_ignored() {
1398 + assert_eq!(
1399 + run(b"\x1b[:m"),
1400 + vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"]
1401 + );
1402 + assert_eq!(
1403 + run(b"\x1b[;m"),
1404 + vec!["csi(params=[[]], intermediates=[], ignore=false, action='m')"]
1405 + );
1406 + }
1407 +
1408 + #[test]
1409 + fn a_c0_inside_csi_param_executes() {
1410 + assert_eq!(
1411 + run(b"\x1b[1\rm"),
1412 + vec![
1413 + "exec(0x0d)",
1414 + "csi(params=[[1]], intermediates=[], ignore=false, action='m')"
1415 + ]
1416 + );
1417 + }
1418 +
1419 + /// A private marker arriving after a parameter is malformed: the DEC
1420 + /// parser sends the sequence to CsiIgnore, so nothing dispatches.
1421 + #[test]
1422 + fn a_private_marker_after_a_parameter_abandons_the_sequence() {
1423 + assert_eq!(run(b"\x1b[1<m"), Vec::<String>::new());
1424 + }
1425 +
1426 + #[test]
1427 + fn a_c0_inside_a_csi_intermediate_executes() {
1428 + assert_eq!(
1429 + run(b"\x1b[ \rm"),
1430 + vec![
1431 + "exec(0x0d)",
1432 + "csi(params=[], intermediates=[32], ignore=false, action='m')"
1433 + ]
1434 + );
1435 + }
1436 +
1437 + /// A parameter byte after an intermediate is out of order, so the sequence
1438 + /// is abandoned rather than dispatched with the bytes rearranged.
1439 + #[test]
1440 + fn a_parameter_after_an_intermediate_abandons_the_sequence() {
1441 + assert_eq!(run(b"\x1b[ 1m"), Vec::<String>::new());
1442 + }
1443 +
1444 + /// CsiIgnore still executes C0s, and still ends on a final byte -- ending
1445 + /// is the part that matters, because a parser stuck in CsiIgnore swallows
1446 + /// the rest of the stream.
1447 + #[test]
1448 + fn an_abandoned_csi_still_executes_c0s_and_still_ends() {
1449 + assert_eq!(run(b"\x1b[1<\rma"), vec!["exec(0x0d)", "print('a')"]);
1450 + }
1451 +
1452 + // ---- DCS -----------------------------------------------------------
1453 +
1454 + #[test]
1455 + fn a_bare_dcs_hooks_and_unhooks() {
1456 + assert_eq!(
1457 + run(b"\x1bPq\x1b\\"),
1458 + vec![
1459 + "hook(params=[], intermediates=[], ignore=false, action='q')",
1460 + "unhook",
1461 + "esc(intermediates=[], ignore=false, byte=0x5c)",
1462 + ]
1463 + );
1464 + }
1465 +
1466 + #[test]
1467 + fn dcs_passthrough_delivers_the_body_and_a_bell_terminates_it() {
1468 + assert_eq!(
1469 + run(b"\x1bPqAB\x07"),
1470 + vec![
1471 + "hook(params=[], intermediates=[], ignore=false, action='q')",
1472 + "put(0x41)",
1473 + "put(0x42)",
1474 + "unhook",
1475 + ]
1476 + );
1477 + }
1478 +
1479 + /// The digit arithmetic in both DCS parameter states, which is the same
1480 + /// `b - b'0'` the CSI states use and was covered in neither.
1481 + #[test]
1482 + fn dcs_parameters_parse_as_numbers() {
1483 + assert_eq!(
1484 + run(b"\x1bP1q\x1b\\")[0],
1485 + "hook(params=[[1]], intermediates=[], ignore=false, action='q')"
1486 + );
1487 + assert_eq!(
1488 + run(b"\x1bP12q\x1b\\")[0],
1489 + "hook(params=[[12]], intermediates=[], ignore=false, action='q')"
1490 + );
1491 + assert_eq!(
1492 + run(b"\x1bP1;2q\x1b\\")[0],
1493 + "hook(params=[[1], [2]], intermediates=[], ignore=false, action='q')"
1494 + );
1495 + }
1496 +
1497 + #[test]
1498 + fn a_dcs_intermediate_reaches_the_hook() {
1499 + assert_eq!(
1500 + run(b"\x1bP$q\x1b\\")[0],
1501 + "hook(params=[], intermediates=[36], ignore=false, action='q')"
1502 + );
1503 + assert_eq!(
1504 + run(b"\x1bP1$q\x1b\\")[0],
1505 + "hook(params=[[1]], intermediates=[36], ignore=false, action='q')"
1506 + );
1507 + assert_eq!(
1508 + run(b"\x1bP$$q\x1b\\")[0],
1509 + "hook(params=[], intermediates=[36, 36], ignore=false, action='q')"
1510 + );
1511 + }
1512 +
1513 + #[test]
1514 + fn a_dcs_private_marker_reaches_the_hook() {
1515 + assert_eq!(
1516 + run(b"\x1bP?q\x1b\\")[0],
1517 + "hook(params=[], intermediates=[63], ignore=false, action='q')"
1518 + );
1519 + }
1520 +
1521 + /// A separator opening a DCS opens an empty parameter, and does not mark
1522 + /// the sequence ignored -- the same inverted-`!` shape as CSI.
1523 + #[test]
1524 + fn a_leading_dcs_separator_opens_an_empty_parameter() {
1525 + assert_eq!(
1526 + run(b"\x1bP;q\x1b\\")[0],
1527 + "hook(params=[[]], intermediates=[], ignore=false, action='q')"
1528 + );
1529 + }
1530 +
1531 + /// Every route into DcsIgnore. A colon is illegal in a DCS parameter list
1532 + /// and a private marker is illegal after one, so both abandon the sequence:
1533 + /// no hook, and therefore no passthrough of whatever followed.
1534 + #[test]
1535 + fn an_illegal_dcs_prelude_abandons_the_sequence() {
1536 + for seq in [
1537 + &b"\x1bP:q\x1b\\"[..], // colon in DcsEntry
1538 + &b"\x1bP1:q\x1b\\"[..], // colon in DcsParam
1539 + &b"\x1bP1<q\x1b\\"[..], // private marker after a parameter
1540 + &b"\x1bP$1q\x1b\\"[..], // parameter after an intermediate
1541 + ] {
1542 + let log = run(seq);
1543 + assert!(
1544 + !log.iter().any(|e| e.starts_with("hook(")),
1545 + "{seq:?} must not hook, got {log:?}"
1546 + );
1547 + }
1548 + }
1549 +
1550 + /// DcsIgnore ends on ESC and nothing else, and the parser comes back out of
1551 + /// it. A DcsIgnore that never ends swallows the rest of the stream; one
1552 + /// that ends on every byte ends in the middle of the discarded prelude.
1553 + #[test]
1554 + fn an_abandoned_dcs_ends_on_the_terminator_and_prints_again() {
1555 + assert_eq!(
1556 + run(b"\x1bP:q\x1b\\a"),
1557 + vec![
1558 + "esc(intermediates=[], ignore=false, byte=0x5c)",
1559 + "print('a')"
1560 + ]
1561 + );
1562 + }
1163 1563 }
@@ -1,0 +1,71 @@
1 + # Mutants that NO test can kill, with the reason each one is unreachable by any
2 + # test that could be written.
3 + #
4 + # THE BAR IS IMPOSSIBILITY, NOT COST. A mutant that a test could kill, where
5 + # nobody has written that test, does not belong here however unappealing the
6 + # test looks. It stays a visible survivor with a GoingsOn task against it. The
7 + # two classes are easy to conflate and the difference is the whole value of the
8 + # number: an exclusion list that also absorbs "not worth it" reports zero while
9 + # real coverage gaps sit underneath it.
10 + #
11 + # The patterns are regexes, so `+`, `*`, `|` and `.` need escaping. A bare `||`
12 + # is an empty alternation and silently excludes every mutant in the crate.
13 +
14 + exclude_re = [
15 + # --- Default trait method bodies -------------------------------------
16 + #
17 + # Every `Perform` method has a default body of the form `let _ = args;`,
18 + # which exists only to name the arguments so an implementor that wants none
19 + # of them compiles without warnings. Replacing that body with `()` is the
20 + # same program: the discard has no effect and the method returns `()`
21 + # either way. What the parser calls is always the implementor's override,
22 + # and no test can observe a default that does nothing being replaced by
23 + # nothing.
24 + "replace Perform::print with \\(\\)",
25 + "replace Perform::execute with \\(\\)",
26 + "replace Perform::csi_dispatch with \\(\\)",
27 + "replace Perform::esc_dispatch with \\(\\)",
28 + "replace Perform::osc_dispatch with \\(\\)",
29 + "replace Perform::hook with \\(\\)",
30 + "replace Perform::put with \\(\\)",
31 + "replace Perform::apc_dispatch with \\(\\)",
32 +
33 + # --- Match arms that fall through to an identical `_ => {}` -----------
34 + #
35 + # The state handlers are written to mirror the DEC parser's transition
36 + # tables arm for arm, including the arms whose action is "nothing". Those
37 + # arms document a byte range the spec calls out; deleting one lets the same
38 + # bytes reach the handler's own `_ => {}`, which does the same nothing. The
39 + # arms are kept because a reader checking this file against vt100.net needs
40 + # to find them, and excluded because no observation distinguishes them.
41 + #
42 + # 0x7F (DEL) is ignored in every state that names it, which is the reason
43 + # this list is mostly 0x7F.
44 + "delete match arm 0x80\\.\\.= 0xBF in Parser::ground",
45 + "delete match arm 0x7F in Parser::escape$",
46 + "delete match arm 0x7F in Parser::escape_intermediate",
47 + "delete match arm 0x7F in Parser::csi_entry",
48 + "delete match arm 0x7F in Parser::csi_param",
49 + "delete match arm 0x7F in Parser::csi_intermediate",
50 + "delete match arm 0x7F in Parser::dcs_entry",
51 + "delete match arm 0x7F in Parser::dcs_param",
52 + "delete match arm 0x7F in Parser::dcs_intermediate",
53 +
54 + # The three DCS prelude states discard C0 controls rather than executing
55 + # them (unlike the CSI states, which execute). The arm's body is `{}` and
56 + # the fall-through is `{}`.
57 + "delete match arm 0x00\\.\\.= 0x17 \\| 0x19 \\| 0x1C\\.\\.= 0x1F in Parser::dcs_entry",
58 + "delete match arm 0x00\\.\\.= 0x17 \\| 0x19 \\| 0x1C\\.\\.= 0x1F in Parser::dcs_param",
59 + "delete match arm 0x00\\.\\.= 0x17 \\| 0x19 \\| 0x1C\\.\\.= 0x1F in Parser::dcs_intermediate",
60 +
61 + # --- Clearing state that is already clear -----------------------------
62 + #
63 + # `ESC ESC` in the Escape state. Every route into Escape calls `clear()` on
64 + # the way in (Ground, Utf8, DcsIgnore, DcsPassthrough, OscString and
65 + # ApcString all do), and the only arm inside Escape that writes
66 + # intermediates or parameters is 0x20..=0x2F, which leaves Escape as it
67 + # writes. So the parser is never in Escape holding anything to clear, and
68 + # deleting the arm leaves the byte to a `_ => {}` that also holds the state
69 + # still. Kept because the DEC table names the transition.
70 + "delete match arm 0x1B in Parser::escape$",
71 + ]