82 lines
3.9 KiB
Go
82 lines
3.9 KiB
Go
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)
|
||
}
|
||
}
|