Skip to main content

max / makenotwork

tagtree: make the oracle's own decisions observable Eighteen mutants inside the oracle bodies survived a whole-file run, all one shape: an assertion inside an oracle only speaks when the crate is wrong, so a mutant that skips one, narrows the input it runs on, or ends a loop early leaves a passing run passing. None were killable where they stood. Each decision is now a named function with a unit test that states what it decides: script_lines (which lines an entry point reads), index_has_room (the bound on the + arm), depths_to_check (how far check_tag walks), ancestor_prefix_applies (check_pair's guard) and assert_escape_shape, which takes the escaped string as an argument so a test can hand it a violation and require the panic. The index-script driver returns the index it built and the ops it ran, which is what makes a deleted dispatch arm observable; the ? / ~ and ! / m arms are separate arms now, so the two op comparisons they hid are gone. check_escaping's wildcard walk consumes slices instead of advancing an index. The arithmetic is what generated the two mutants that did not terminate; each branch now consumes what it looked at, so the walk ends by construction. cargo mutants --file src/oracle.rs on astra: 156 mutants, 148 caught, 0 missed, 0 timeouts, 8 unviable. The oracle.rs exclusion anchors are re-derived against the moved lines, verified by diffing cargo mutants --list with and without the config.
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 22:37 UTC
Signed with PGP, not checked
Commit: 7b965af5a762a4acaff9ee44a99e831d21e983dd
Parent: 57de569
3 files changed, +348 insertions, -102 deletions
@@ -4,6 +4,19 @@
4 4
5 5 ## Unreleased
6 6
7 + ### Changed
8 +
9 + - The oracle's own decisions are named functions with unit tests, rather than
10 + operators and guards inside the check bodies. An assertion inside an oracle only
11 + speaks when the crate is wrong, so a mutant that skipped one, narrowed the lines it
12 + ran on, or stopped a loop early left every passing run passing: 18 such mutants
13 + survived a whole-file mutation run. `script_lines`, `index_has_room`,
14 + `depths_to_check`, `ancestor_prefix_applies` and `assert_escape_shape` are the
15 + decisions pulled out, and the index-script driver now reports the ops it ran so a
16 + deleted dispatch arm is observable. `check_escaping`'s wildcard walk moved from an
17 + index to slice consumption, which removed the two mutants that did not terminate.
18 + `cargo mutants --file src/oracle.rs`: 156 mutants, 148 caught, 0 missed, 0 timeouts.
19 +
7 20 ### Added
8 21
9 22 - `pub mod oracle`: the property set the astra soak tier asserts, and the same set
@@ -93,17 +93,20 @@
93 93 # mutant: 111 of oracle.rs's 149 mutants were caught, and they were caught
94 94 # AT these assertions.
95 95 #
96 - # This covers only the seven entry points. The operators and match arms
97 - # INSIDE the oracle bodies are a different question -- a test could assert
98 - # that the script driver dispatches every opcode -- and those stay visible
99 - # survivors under infra `cb62491e`.
100 - "src/oracle.rs:151:5: replace check_tag_text with \\(\\)",
101 - "src/oracle.rs:204:5: replace check_index_script with \\(\\)",
102 - "src/oracle.rs:213:5: replace check_index_script_bounded with \\(\\)",
103 - "src/oracle.rs:347:5: replace check_tag with \\(\\)",
104 - "src/oracle.rs:509:5: replace check_escaping with \\(\\)",
105 - "src/oracle.rs:625:5: replace check_pair with \\(\\)",
106 - "src/oracle.rs:784:5: replace check_index_state with \\(\\)",
96 + # This covers only the seven entry points. The operators, guards and match
97 + # arms INSIDE the oracle bodies were a different question, and the answer
98 + # was structural rather than an exclusion: each is now a named function --
99 + # `script_lines`, `index_has_room`, `depths_to_check`,
100 + # `ancestor_prefix_applies`, `assert_escape_shape` -- with a unit test that
101 + # states what it decides, and the script driver reports the ops it ran so
102 + # that a deleted arm is observable. Closed under infra `cb62491e`.
103 + "src/oracle.rs:194:5: replace check_tag_text with \\(\\)",
104 + "src/oracle.rs:242:5: replace check_index_script with \\(\\)",
105 + "src/oracle.rs:251:5: replace check_index_script_bounded with \\(\\)",
106 + "src/oracle.rs:461:5: replace check_tag with \\(\\)",
107 + "src/oracle.rs:672:5: replace check_escaping with \\(\\)",
108 + "src/oracle.rs:767:5: replace check_pair with \\(\\)",
109 + "src/oracle.rs:926:5: replace check_index_state with \\(\\)",
107 110
108 111 # `tag.starts_with(SEPARATOR) || tag.ends_with(SEPARATOR)` in `model_valid`.
109 112 # `&&` is equivalent for the accept decision, which is all this function
@@ -114,7 +117,7 @@
114 117 # under `&&` it is rejected there, and `model_valid` answers false either
115 118 # way. `model_valid_rejects_leading_and_trailing_separators` pins the
116 119 # behaviour; it cannot pin the operator, and says so.
117 - "src/oracle.rs:578:35: replace \\|\\| with && in model_valid",
120 + "src/oracle.rs:720:35: replace \\|\\| with && in model_valid",
118 121
119 122 # `config.semantic_depth > 0` guarding model_valid's minimum-depth check.
120 123 # The same mutant as `src/lib.rs:189:30` in `validate_with`, in the model
@@ -125,5 +128,5 @@
125 128 # at least one segment for every non-empty string, so `segs.len() >= 1` always
126 129 # holds by the time this line runs and the extra check cannot fire. When
127 130 # `semantic_depth > 0` the guard is true either way.
128 - "src/oracle.rs:594:30: replace > with >= in model_valid",
131 + "src/oracle.rs:736:30: replace > with >= in model_valid",
129 132 ]
@@ -136,6 +136,49 @@
136 136 /// wrote deliberately use [`check_index_script_bounded`] instead.
137 137 const MAX_OPS: usize = 128;
138 138
139 + /// How many tags one index script may build up.
140 + ///
141 + /// A bound on fuzzer cost, not a property: the corpus should search shapes,
142 + /// not sizes.
143 + const INDEX_CAP: usize = 256;
144 +
145 + /// The lines of one input, as both entry points read them.
146 + ///
147 + /// Splitting, `\r` stripping, the [`MAX_LINE`] filter and the cap are one
148 + /// decision shared by the two targets, so it lives here where a test can reach
149 + /// it. Inline in an entry point it was unobservable: a mutant that widens the
150 + /// filter or drops the cap makes the oracle check a different set of lines, and
151 + /// every check it does still passes.
152 + fn script_lines(text: &str, max_lines: usize) -> Vec<&str> {
153 + text.split('\n')
154 + .map(|l| l.strip_suffix('\r').unwrap_or(l))
155 + .filter(|l| l.len() <= MAX_LINE)
156 + .take(max_lines)
157 + .collect()
158 + }
159 +
160 + /// Whether the `+` arm performs another insert.
161 + ///
162 + /// A named decision rather than an inline `<`, for the same reason: the bound
163 + /// is invisible from inside the oracle, because every assertion holds whatever
164 + /// the index happens to contain.
165 + fn index_has_room(len: usize) -> bool {
166 + len < INDEX_CAP
167 + }
168 +
169 + /// The segment depths [`check_tag`] checks `semantic_prefix` and `free_suffix`
170 + /// at: every depth in the tag, plus the one past its end where both must give
171 + /// up. Named so that the `+ 1` is a thing a test can state; inside the loop it
172 + /// only decided how many passing assertions ran.
173 + fn depths_to_check(segments: usize) -> std::ops::RangeInclusive<usize> {
174 + 1..=segments + 1
175 + }
176 +
177 + /// The tags an index holds, in iteration order.
178 + fn tags_of(index: &TagIndex) -> Vec<String> {
179 + index.iter().map(ToString::to_string).collect()
180 + }
181 +
139 182 // Entry points the fuzz targets call
140 183
141 184 /// Entry point for the `tag` target.
@@ -148,12 +191,7 @@
148 191 ///
149 192 /// On any violated property. That is the point.
150 193 pub fn check_tag_text(text: &str) {
151 - let lines: Vec<&str> = text
152 - .split('\n')
153 - .map(|l| l.strip_suffix('\r').unwrap_or(l))
154 - .filter(|l| l.len() <= MAX_LINE)
155 - .take(MAX_LINES)
156 - .collect();
194 + let lines = script_lines(text, MAX_LINES);
157 195
158 196 for line in &lines {
159 197 for config in CONFIGS {
@@ -210,26 +248,50 @@
210 248 ///
211 249 /// On any violated property.
212 250 pub fn check_index_script_bounded(text: &str, max_ops: usize) {
251 + run_index_script_bounded(text, max_ops);
252 + }
253 +
254 + /// One operation an index script performed.
255 + ///
256 + /// The dispatch is the half of the driver nothing could observe. Every
257 + /// assertion below compares the crate against a model over whatever index the
258 + /// script has built, so an arm that never runs leaves every remaining check
259 + /// passing over a shorter history and the run green. Reporting the ops is what
260 + /// lets a test say which arm ran.
261 + #[derive(Debug, Clone, Copy, PartialEq, Eq)]
262 + enum Op {
263 + Insert,
264 + Remove,
265 + Rebuild,
266 + Suggest,
267 + SuggestFuzzy,
268 + Rename,
269 + Merge,
270 + RemoveSubtree,
271 + }
272 +
273 + /// [`check_index_script_bounded`], returning the index it built and the ops it
274 + /// ran, so that the dispatch is observable to a test.
275 + ///
276 + /// # Panics
277 + ///
278 + /// On any violated property.
279 + fn run_index_script_bounded(text: &str, max_ops: usize) -> (TagIndex, Vec<Op>) {
213 280 let mut index = TagIndex::empty();
281 + let mut ops = Vec::new();
214 282 check_index_state(&index);
215 283
216 - for line in text
217 - .split('\n')
218 - .map(|l| l.strip_suffix('\r').unwrap_or(l))
219 - .filter(|l| l.len() <= MAX_LINE)
220 - .take(max_ops)
221 - {
284 + for line in script_lines(text, max_ops) {
222 285 let Some(op) = line.chars().next() else {
223 286 continue;
224 287 };
225 288 let rest = &line[op.len_utf8()..];
226 289 match op {
227 290 '+' => {
228 - // Bounded so one input cannot spend a minute building a huge
229 - // index. The corpus should search shapes, not sizes.
230 - if index.len() < 256 {
291 + if index_has_room(index.len()) {
231 292 index.insert(rest.to_string());
232 293 }
294 + ops.push(Op::Insert);
233 295 }
234 296 '-' => {
235 297 let expected = index.iter().any(|t| t == rest);
@@ -238,6 +300,7 @@
238 300 expected,
239 301 "remove reported the wrong presence for {rest:?}"
240 302 );
303 + ops.push(Op::Remove);
241 304 }
242 305 '=' => {
243 306 let tags: Vec<String> = if rest.is_empty() {
@@ -246,59 +309,41 @@
246 309 rest.split(',').take(64).map(ToString::to_string).collect()
247 310 };
248 311 index.rebuild(tags);
312 + ops.push(Op::Rebuild);
249 313 }
250 - '?' | '~' => {
314 + '?' => {
251 315 let (input, limit) = split_query(rest);
252 - if op == '?' {
253 - let got = index.suggest(input, limit);
254 - let want = model_suggest(&index, input, limit);
255 - assert_eq!(got, want, "suggest({input:?}, {limit}) diverged");
256 - } else {
257 - let got = index.suggest_fuzzy(input, limit);
258 - let want = model_suggest_fuzzy(&index, input, limit);
259 - assert_eq!(got, want, "suggest_fuzzy({input:?}, {limit}) diverged");
260 - }
261 - // `contains` is a binary search; the linear scan is the check.
262 - assert_eq!(
263 - index.contains(input),
264 - index.iter().any(|t| t == input),
265 - "contains({input:?}) diverged from a linear scan"
266 - );
267 - let (with_status, exact) = index.suggest_with_status(input, limit);
268 - assert_eq!(with_status, index.suggest(input, limit));
269 - assert_eq!(exact, index.contains(input));
316 + let got = index.suggest(input, limit);
317 + let want = model_suggest(&index, input, limit);
318 + assert_eq!(got, want, "suggest({input:?}, {limit}) diverged");
319 + check_query_invariants(&index, input, limit);
320 + ops.push(Op::Suggest);
270 321 }
271 - '!' | 'm' => {
322 + '~' => {
323 + let (input, limit) = split_query(rest);
324 + let got = index.suggest_fuzzy(input, limit);
325 + let want = model_suggest_fuzzy(&index, input, limit);
326 + assert_eq!(got, want, "suggest_fuzzy({input:?}, {limit}) diverged");
327 + check_query_invariants(&index, input, limit);
328 + ops.push(Op::SuggestFuzzy);
329 + }
330 + '!' => {
272 331 let (old, new) = split_rename(rest);
273 - let before: Vec<String> = index.iter().map(ToString::to_string).collect();
274 - let count = if op == '!' {
275 - crate::rename_prefix_bulk(old, new, &mut index)
276 - } else {
277 - crate::merge_tags(old, new, &mut index)
278 - };
279 - let expected = before
280 - .iter()
281 - .filter(|t| rename_prefix(old, new, t).is_some())
282 - .count();
283 - assert_eq!(
284 - count, expected,
285 - "rename_prefix_bulk({old:?}, {new:?}) miscounted"
286 - );
287 - let mut want: Vec<String> = before
288 - .iter()
289 - .map(|t| rename_prefix(old, new, t).unwrap_or_else(|| t.clone()))
290 - .collect();
291 - want.sort_unstable();
292 - want.dedup();
293 - let got: Vec<String> = index.iter().map(ToString::to_string).collect();
294 - assert_eq!(
295 - got, want,
296 - "rename_prefix_bulk({old:?}, {new:?}) wrong tag set"
297 - );
332 + let before = tags_of(&index);
333 + let count = crate::rename_prefix_bulk(old, new, &mut index);
334 + check_rename_result(&index, &before, old, new, count);
335 + ops.push(Op::Rename);
336 + }
337 + 'm' => {
338 + let (old, new) = split_rename(rest);
339 + let before = tags_of(&index);
340 + let count = crate::merge_tags(old, new, &mut index);
341 + check_rename_result(&index, &before, old, new, count);
342 + ops.push(Op::Merge);
298 343 }
299 344 'x' => {
300 345 let prefix = rest.strip_prefix(' ').unwrap_or(rest);
301 - let before: Vec<String> = index.iter().map(ToString::to_string).collect();
346 + let before = tags_of(&index);
302 347 let removed = crate::remove_subtree(prefix, &mut index);
303 348 let want: Vec<String> = before
304 349 .iter()
@@ -306,13 +351,82 @@
306 351 .cloned()
307 352 .collect();
308 353 assert_eq!(removed, before.len() - want.len());
309 - let got: Vec<String> = index.iter().map(ToString::to_string).collect();
310 - assert_eq!(got, want, "remove_subtree({prefix:?}) wrong tag set");
354 + assert_eq!(
355 + tags_of(&index),
356 + want,
357 + "remove_subtree({prefix:?}) wrong tag set"
358 + );
359 + ops.push(Op::RemoveSubtree);
311 360 }
312 361 _ => continue,
313 362 }
314 363 check_index_state(&index);
315 364 }
365 +
366 + (index, ops)
367 + }
368 +
369 + /// The assertions both query arms make, whichever suggestion function was
370 + /// called.
371 + ///
372 + /// Returns whether `input` is in the index. The return value is not what the
373 + /// caller wants -- it is what makes this function's body observable, since a
374 + /// body of pure assertions replaced by `()` is invisible to every test that
375 + /// passes.
376 + ///
377 + /// # Panics
378 + ///
379 + /// On any violated property.
380 + fn check_query_invariants(index: &TagIndex, input: &str, limit: usize) -> bool {
381 + // `contains` is a binary search; the linear scan is the check.
382 + let contains = index.contains(input);
383 + assert_eq!(
384 + contains,
385 + index.iter().any(|t| t == input),
386 + "contains({input:?}) diverged from a linear scan"
387 + );
388 + let (with_status, exact) = index.suggest_with_status(input, limit);
389 + assert_eq!(with_status, index.suggest(input, limit));
390 + assert_eq!(exact, contains);
391 + contains
392 + }
393 +
394 + /// Assert the count and the tag set a prefix rename must produce.
395 + ///
396 + /// Shared by the `!` and `m` arms, which differ only in the function they call.
397 + /// Returns the count the model expected, for the same reason
398 + /// [`check_query_invariants`] returns a bool.
399 + ///
400 + /// # Panics
401 + ///
402 + /// On any violated property.
403 + fn check_rename_result(
404 + index: &TagIndex,
405 + before: &[String],
406 + old: &str,
407 + new: &str,
408 + count: usize,
409 + ) -> usize {
410 + let expected = before
411 + .iter()
412 + .filter(|t| rename_prefix(old, new, t).is_some())
413 + .count();
414 + assert_eq!(
415 + count, expected,
416 + "rename_prefix_bulk({old:?}, {new:?}) miscounted"
417 + );
418 + let mut want: Vec<String> = before
419 + .iter()
420 + .map(|t| rename_prefix(old, new, t).unwrap_or_else(|| t.clone()))
421 + .collect();
422 + want.sort_unstable();
423 + want.dedup();
424 + assert_eq!(
425 + tags_of(index),
426 + want,
427 + "rename_prefix_bulk({old:?}, {new:?}) wrong tag set"
428 + );
429 + expected
316 430 }
317 431
318 432 /// Split `input limit` on the last space. A missing or unparsable limit means
@@ -441,7 +555,7 @@
441 555 if tag.is_empty() { None } else { Some(tag) },
442 556 "free_suffix({tag:?}, 0)"
443 557 );
444 - for n in 1..=segs.len() + 1 {
558 + for n in depths_to_check(segs.len()) {
445 559 assert_eq!(
446 560 semantic_prefix(tag, n),
447 561 prefix_at_depth(tag, n),
@@ -502,34 +616,62 @@
502 616 /// escaping it, so a defect here is wrong search results rather than an
503 617 /// unreachable path.
504 618 ///
619 + /// Whether [`check_pair`] checks that an ancestor is the prefix at its own
620 + /// segment count.
621 + ///
622 + /// The empty string is an ancestor of nothing by the segment model, and
623 + /// `prefix_at_depth` has nothing to answer for it, so the check applies only to
624 + /// a non-empty ancestor. Named because the guard is invisible where it stands:
625 + /// widening it only skips an assertion that would have passed.
626 + fn ancestor_prefix_applies(ancestor: &str, is_ancestor: bool) -> bool {
627 + is_ancestor && !ancestor.is_empty()
628 + }
629 +
630 + /// Assert that `escaped` carries no unescaped `%` or `_`, and that every
631 + /// backslash introduces one of the three escapable characters. That is what
632 + /// makes the string safe to interpolate into a `LIKE ... ESCAPE '\'`.
633 + ///
634 + /// Taking the escaped string as an argument is what makes this checkable: a
635 + /// test can hand it a violation and require the panic, which is the only way an
636 + /// assertion inside an oracle is ever observed. Written over slices rather than
637 + /// an index because each branch consumes what it looked at, so the walk
638 + /// terminates by construction -- the index version generated two mutants that
639 + /// did not terminate at all and two more that skipped bytes invisibly.
640 + ///
641 + /// # Panics
642 + ///
643 + /// On an unescaped wildcard, a trailing backslash, or a backslash before
644 + /// anything but `\`, `%` or `_`.
645 + fn assert_escape_shape(escaped: &str) {
646 + let mut rest = escaped.as_bytes();
647 + while let Some((&byte, tail)) = rest.split_first() {
648 + if byte == b'\\' {
649 + let (&escapee, tail) = tail
650 + .split_first()
651 + .unwrap_or_else(|| panic!("trailing backslash in {escaped:?}"));
652 + assert!(
653 + matches!(escapee, b'\\' | b'%' | b'_'),
654 + "backslash escapes {:?} in {escaped:?}",
655 + escapee as char
656 + );
657 + rest = tail;
658 + } else {
659 + assert!(
660 + byte != b'%' && byte != b'_',
661 + "unescaped wildcard in {escaped:?}"
662 + );
663 + rest = tail;
664 + }
665 + }
666 + }
667 +
505 668 /// # Panics
506 669 ///
507 670 /// On any violated property.
508 671 pub fn check_escaping(s: &str) {
509 672 let escaped = escape_like(s);
510 673
511 - // Every `%` and `_` in the output is introduced by a backslash, and every
512 - // backslash introduces one of the three escapable characters. That is what
513 - // makes the string safe to interpolate into a `LIKE ... ESCAPE '\'`.
514 - let b = escaped.as_bytes();
515 - let mut i = 0;
516 - while i < b.len() {
517 - if b[i] == b'\\' {
518 - assert!(i + 1 < b.len(), "trailing backslash in {escaped:?}");
519 - assert!(
520 - matches!(b[i + 1], b'\\' | b'%' | b'_'),
521 - "backslash escapes {:?} in {escaped:?}",
522 - b[i + 1] as char
523 - );
524 - i += 2;
525 - } else {
526 - assert!(
527 - b[i] != b'%' && b[i] != b'_',
528 - "unescaped wildcard in {escaped:?}"
529 - );
530 - i += 1;
531 - }
532 - }
674 + assert_escape_shape(&escaped);
533 675
534 676 // Round trip: undoing the documented escape rule returns the input exactly.
535 677 let unescaped = unescape_like(&escaped).unwrap_or_else(|| {
@@ -633,7 +775,7 @@
633 775 a_before_b,
634 776 "is_ancestor_of({a:?}, {b:?})"
635 777 );
636 - if a_before_b && !a.is_empty() {
778 + if ancestor_prefix_applies(a, a_before_b) {
637 779 assert_eq!(
638 780 prefix_at_depth(b, sa.len()),
639 781 Some(a),
@@ -939,8 +1081,12 @@
939 1081 //! a test that picks one cannot disagree with the mutant. See
940 1082 //! `_private/docs/meta/test_style.md`, "Tests that cannot disagree".
941 1083
942 - use super::{model_valid, split_query, split_rename};
943 - use crate::TagConfig;
1084 + use super::{
1085 + INDEX_CAP, MAX_LINE, Op, ancestor_prefix_applies, assert_escape_shape,
1086 + check_query_invariants, check_rename_result, depths_to_check, index_has_room, model_valid,
1087 + run_index_script_bounded, script_lines, split_query, split_rename, tags_of,
1088 + };
1089 + use crate::{TagConfig, TagIndex};
944 1090
945 1091 // ── split_query: `input limit`, splitting on the LAST space ──
946 1092
@@ -1073,4 +1219,206 @@
1073 1219 );
1074 1220 assert!(model_valid("genre.rock.metal", &c));
1075 1221 }
1222 + // ── The decisions the oracle bodies used to hide ──
1223 + //
1224 + // Every test below exists for the same reason: an assertion inside an
1225 + // oracle only speaks when the crate is wrong, so a mutant that skips one,
1226 + // narrows the input it runs on, or stops the loop early leaves a passing
1227 + // run passing. None of those are killable where they stand. Each is a named
1228 + // function here instead, and these are the tests that state what it decides.
1229 +
1230 + // ── script_lines: which lines an entry point reads ──
1231 +
1232 + #[test]
1233 + fn script_lines_keeps_a_line_of_exactly_the_maximum_length() {
1234 + // `<=` vs `<`: they disagree only at the limit itself.
1235 + let at_limit = "g".repeat(MAX_LINE);
1236 + let text = format!("genre.rock\n{at_limit}");
1237 + assert_eq!(
1238 + script_lines(&text, 8),
1239 + vec!["genre.rock", at_limit.as_str()]
1240 + );
1241 + }
1242 +
1243 + #[test]
1244 + fn script_lines_drops_a_line_one_byte_over_the_maximum() {
1245 + let over = "g".repeat(MAX_LINE + 1);
1246 + let text = format!("genre.rock\n{over}\ngenre.folk");
1247 + assert_eq!(script_lines(&text, 8), vec!["genre.rock", "genre.folk"]);
1248 + }
1249 +
1250 + #[test]
1251 + fn script_lines_strips_one_carriage_return_from_the_end() {
1252 + assert_eq!(
1253 + script_lines("genre.rock\r\ngenre.folk\r", 8),
1254 + vec!["genre.rock", "genre.folk"]
1255 + );
1256 + }
1257 +
1258 + #[test]
1259 + fn script_lines_stops_at_the_cap() {
1260 + // Three, not one: a cap of one cannot disagree with a mutant that
1261 + // returns the first line, and an empty result is what `vec![]` returns.
1262 + assert_eq!(
1263 + script_lines("genre.rock\ngenre.folk\ngenre.metal\ngenre.jazz", 3),
1264 + vec!["genre.rock", "genre.folk", "genre.metal"]
1265 + );
1266 + }
1267 +
1268 + // ── index_has_room: the bound on the `+` arm ──
1269 +
1270 + #[test]
1271 + fn the_insert_arm_has_room_up_to_the_cap_and_not_at_it() {
1272 + assert!(index_has_room(INDEX_CAP - 1));
1273 + assert!(!index_has_room(INDEX_CAP));
1274 + assert!(!index_has_room(INDEX_CAP + 40));
1275 + }
1276 +
1277 + // ── depths_to_check: how far check_tag walks ──
1278 +
1279 + #[test]
1280 + fn depths_to_check_runs_one_past_the_segment_count() {
1281 + // Three segments rather than one: `+ 1` and `* 1` agree at zero
1282 + // segments and `- 1` differs from both only above one.
1283 + assert_eq!(depths_to_check(3).collect::<Vec<_>>(), vec![1, 2, 3, 4]);
1284 + }
1285 +
1286 + // ── ancestor_prefix_applies: check_pair's guard ──
1287 +
1288 + #[test]
1289 + fn the_ancestor_prefix_check_applies_to_a_real_ancestor_only() {
1290 + assert!(ancestor_prefix_applies("genre", true));
1291 + assert!(
1292 + !ancestor_prefix_applies("", true),
1293 + "the empty string is an ancestor of nothing the model recognises"
1294 + );
1295 + assert!(!ancestor_prefix_applies("genre", false));
1296 + assert!(!ancestor_prefix_applies("", false));
1297 + }
1298 +
1299 + // ── assert_escape_shape: the LIKE-escaping walk ──
1300 +
1301 + #[test]
1302 + fn escape_shape_accepts_all_three_escaped_characters() {
1303 + // All three arms of the `matches!` in one string, so deleting any of
1304 + // them turns this into a panic.
1305 + assert_escape_shape("genre\\%rock\\_metal\\\\folk");
Lines truncated