storytold/wordcraft#3692: review — proof timing tests get a margin a busy CI machine can't break

Wall-clock bounds of 1-2 s failed on loaded runners. Each bound is now 10 s, at least 10x the
fixed code's time and well under the old code's on the same input (measured by running the
pre-fix code):

- self-continuing rules: 40,000 rules (was 16,000); ~1 ms now, old checker 14 s at 16,000.
- many strips per suffix: 200,000 rules (was 30,000); 0.4 s now, old index 3 s at 30,000 and
  11 min at 300,000.
- forms of self-continuing rules: the MAX_FORMS count catches the old code (9 million forms);
  the time bound is a backstop.
- overlong Turkish words: ~20 ms now; without the caps it ran past 5 minutes.
- capped Hunspell reads: no clock; the "not a regular file" error is the proof (the old reader
  never returned from /dev/zero).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-10-26 00:17:25 +00:00
1 parent 935c10d3d0
commit 06af26ed67
3 files changed
+18 -12

No files matched your search

+14 -8
View File
@@ -1310,11 +1310,13 @@ pub(crate) mod tests {
#[test]
fn self_continuing_rules_check_in_linear_time() {
// Thousands of empty-append rules that continue with their own flag: each check used to
// follow every rule from every rule, millions of steps a word (#3613).
let d = many_rules(16_000, |_| "0 0/A .".into());
// follow every rule from every rule, millions of steps a word (#3613). The checks take
// about a millisecond; the old checker took 14 s for two of them at 16,000 rules, so at
// 40,000 (quadratic: minutes) the 10 s bound leaves a busy machine a wide margin.
let d = many_rules(40_000, |_| "0 0/A .".into());
let t = std::time::Instant::now();
assert!(d.check("kot") && !d.check("pies") && !d.check("kotx"));
assert!(t.elapsed().as_secs_f64() < 2.0, "{:?}", t.elapsed());
assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed());
}
#[test]
@@ -1324,7 +1326,9 @@ pub(crate) mod tests {
let distinct = many_rules(3000, |i| format!("0 x{i}/A ."));
let t = std::time::Instant::now();
let (a, b) = (same.forms("kot"), distinct.forms("kot"));
assert!(t.elapsed().as_secs_f64() < 2.0, "{:?}", t.elapsed());
// The cap on the count is what tells the old code (9 million forms each) apart; the
// generous time bound (tens of ms here) only backs up the rule-visit cap.
assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed());
assert!(a.len() <= MAX_FORMS && b.len() <= MAX_FORMS, "{} {}", a.len(), b.len());
assert!(b.iter().any(|f| f == "kotx0x1"), "chained forms are still listed");
}
@@ -1332,11 +1336,13 @@ pub(crate) mod tests {
#[test]
fn many_strips_per_suffix_index_in_linear_time() {
// One append with thousands of strips: the index used to scan the append's groups for
// each rule.
// each rule. Building 200,000 rules takes well under a second; the old index took 3 s at
// 30,000 and over 10 minutes at 300,000.
let n = 200_000;
let t = std::time::Instant::now();
let d = many_rules(30_000, |i| format!("k{i} y ."));
assert!(t.elapsed().as_secs_f64() < 2.0, "{:?}", t.elapsed());
assert_eq!(d.sfx_index.get("y").map(Vec::len), Some(30_000));
let d = many_rules(n, |i| format!("k{i} y ."));
assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed());
assert_eq!(d.sfx_index.get("y").map(Vec::len), Some(n));
assert!(!d.contains("koty"));
}
+2 -3
View File
@@ -329,13 +329,12 @@ mod tests {
// One byte over the cap (sparse: nothing is written) is refused without reading it.
let big = dir.join("big.dic");
std::fs::File::create(&big).unwrap().set_len(MAX_FILE + 1).unwrap();
let t = std::time::Instant::now();
assert!(load(&aff, &big).unwrap_err().contains("too large"));
// A directory, and on Unix an endless device, are not dictionary files.
// A directory, and on Unix an endless device, are not dictionary files. (No clock: a
// reader that read `/dev/zero` would never return, so the error itself is the proof.)
assert!(load(&aff, &dir).unwrap_err().contains("not a regular file"));
#[cfg(unix)]
assert!(load(&aff, Path::new("/dev/zero")).unwrap_err().contains("not a regular file"));
assert!(t.elapsed().as_secs_f64() < 1.0, "{:?}", t.elapsed());
// Turkish reads through the same reader.
let (tr_aff, tr_dic) = (dir.join("tr_TR.aff"), dir.join("tr_TR.dic"));
std::fs::write(&tr_aff, "SET UTF-8\nSFX 1 Y 1\nSFX 1 0 lar .\n").unwrap();
+2 -1
View File
@@ -298,7 +298,8 @@ mod tests {
assert!(suggest_with(&d, &long, 8).is_empty());
assert!(!d.contains(&long) && !d.contains(&format!("kitap{long}")));
assert!(!super::super::is_correct_with(&d, &long));
assert!(t.elapsed().as_secs_f64() < 1.0, "{:?}", t.elapsed());
// Tens of milliseconds with the caps; without them this ran for over 5 minutes.
assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed());
}
#[test]