textmachine/backend/internal/terminology/evidence_test.go

82 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package terminology
import (
"strings"
"testing"
"time"
)
// TestEvidenceWindowsCutsTheSameWindowAsKWIC pins the equality the fingerprint's meaning rests on.
//
// ⛔ WHY IT IS PINNED RATHER THAN TRUSTED. EvidenceWindows stopped calling window() for cost — window()
// re-derives the whole rune view per call, which is quadratic over a book — so the two now compute the same
// shape by two routes. Let them drift by one rune and every stored fingerprint on earth stops matching the
// one computed next purchase: a silent, book-wide re-purchase out of a LIFETIME budget, with nothing red.
func TestEvidenceWindowsCutsTheSameWindowAsKWIC(t *testing.T) {
for _, src := range []string{
"他拿出元石,看了看。元石在手中发光。这是元海的东西。",
"元石", // the whole string IS the key: both ends clamp
" 元石 ", // surrounding space: both must TrimSpace
"aa元石bb元石cc", // adjacent occurrences
strings.Repeat("前面。元石。", 5), // many
"ǎǎǎ元石ǎǎǎ", // multi-byte NON-CJK neighbours: byte≠rune arithmetic
} {
for _, width := range []int{1, 3, 40} {
got := EvidenceWindows(src, "元石", width)
// The reference: window() over EVERY occurrence, the loop EvidenceWindows replaced.
var want []string
for from := 0; from < len(src); {
i := strings.Index(src[from:], "元石")
if i < 0 {
break
}
at := from + i
want = append(want, window(src, at, at+len("元石"), width))
from = at + len("元石")
}
if len(got) != len(want) {
t.Fatalf("src=%q width=%d: %d window(s), want %d\n got=%q\nwant=%q", src, width, len(got), len(want), got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("src=%q width=%d window %d: got %q, want %q", src, width, i, got[i], want[i])
}
}
// The control that keeps the equality from being vacuous: there IS a window to compare.
if len(want) == 0 {
t.Fatalf("premise broken: the reference produced no windows for src=%q", src)
}
}
}
}
// TestEvidenceWindowsDoesNotGrowQUADRATICALLYWithTheBook pins the cost, because the cost is what forced the
// rewrite and a later "simplification" back to window() would restore it invisibly: the result would stay
// correct and the $0 pass before every purchase would start taking minutes on a real book.
//
// ⚠ IT ASSERTS A RATIO, NOT A DURATION. A wall-clock threshold on a shared machine is a flake; the shape of
// the curve is the property. Quadratic means ×4 input → ≈×16 time; linear means ≈×4. The gate sits far above
// linear and far below quadratic, so it survives a noisy host and still fails the regression it is for.
func TestEvidenceWindowsDoesNotGrowQUADRATICALLYWithTheBook(t *testing.T) {
if testing.Short() {
t.Skip("cost pin: -short")
}
unit := strings.Repeat("前面的话,还有更多的话。", 9) + "元石。"
measure := func(reps int) (time.Duration, int) {
src := strings.Repeat(unit, reps)
start := time.Now()
n := len(EvidenceWindows(src, "元石", 40))
return time.Since(start), n
}
small, nSmall := measure(2000)
large, nLarge := measure(8000) // ×4 the source and ×4 the occurrences
if nSmall == 0 || nLarge != 4*nSmall {
t.Fatalf("premise broken: occurrences %d → %d, want a clean ×4 so the ratio below is about the source size", nSmall, nLarge)
}
ratio := float64(large) / float64(small)
t.Logf("source ×4 (occurrences %d → %d): %s → %s, ratio ×%.1f (linear ≈4, quadratic ≈16)", nSmall, nLarge, small, large, ratio)
if ratio > 8 {
t.Fatalf("time grew ×%.1f for a ×4 source (%s → %s): the scan is super-linear again, and a $0 pass that runs before EVERY purchase would spend minutes on a real book. The usual cause: cutting each window with a helper that re-derives []rune over the WHOLE source per occurrence",
ratio, small, large)
}
}