| 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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");
|