Files
troupe-standup/wizard-work/beam/test_trie.py
T
spark 12f956a515 volt: Q70 -- beam's trie cells now run in CHECK (139 -> 151 tests) and count_prefix counts distinct words, not lifetime inserts (rc 0, 8/8 mutants killed)
Two faults, both re-measured in a foreign clone at dbc2c4f and at HEAD, neither
visible from the commit message:

1. test_trie.py had four module-level functions invoked only from __main__, so
   `python3 -m unittest test_trie` printed "Ran 0 tests / NO TESTS RAN" while
   `python3 test_trie.py` printed "ALL OK", and the lane gate never named trie:
   grep -ci trie over its -v transcript = 0, and the gate's count was 136 at
   dbc2c4f / 139 at HEAD -- unchanged by the file landing. The four cells are now
   nine unittest.TestCase methods (12 cells) and `bash wizard-work/beam/CHECK`
   runs 151 tests, rc 0, with 12 trie cells in the transcript.

2. count_prefix was incremented on insert and never decremented, so it
   contradicted the module's own delete: ['cat','car','card','dog',''] then
   delete('car') read 3 where words_with_prefix('ca') returns 2; ['a','ab','abc']
   then delete('a') read 3 vs 2; insert+delete 'zzz' read 1 with len(t) 0;
   insert('abc') twice read 2 with len(t) 1. It now counts DISTINCT words --
   incremented only when the word was not already an end-of-word, decremented
   along the path on a real delete -- so count_prefix(p) ==
   len(words_with_prefix(p)) by construction and by test, not by recomputation.
   The item allowed either reading; the docstring's own words and the existence
   of an independent enumeration are why this one.

Cells and controls (scratch/q70/, harness q70_mutants.py): the shipped pre-fix
blob is rc 1 with 8 of 12 cells red; seven one-place mutants (duplicate insert
inflates, delete does not decrement, count_prefix +1, prune drops the is_end
guard, insert counts only the last node, delete never prunes, count_prefix
ignores the walk) are 7/7 rc 1; clean module rc 0. Gate also rc 0 from a foreign
cwd with a hostile PYTHONPATH, under python3 -O, and with /root/.hermes masked
by tmpfs (0 entries); rc 1 with the in-tree module deleted.

The module pin is measured, not decorative: with it removed, cells pointed at a
plausible foreign trie module grade GREEN (rc 0, Ran 11) -- and with it in place
the same environment raises ImportError naming both paths. My first cut compared
realpath on both sides and could not tell a symlinked module from its target;
abspath was measured into place and the residual (a symlink at the expected path)
is written down in the artifact rather than implied away.

Not claimed: the 1200-character recursion boundary in __len__, words_with_prefix
and delete is unchanged, verdict-identical to the pre-fix module; no timing.

Files: 2 changed (229 79), plus wizard-work/volt/q70-2026-10-26.md. No host
paths added (git grep -c -e /root/troupe-standup -e /root/.hermes = 0 on all
three). Nothing from inbox/ quoted. Beam's files edited under Q70's own "anyone
may take it".

CHECK rc 0 at this commit: `bash wizard-work/beam/CHECK` -> Ran 151 tests, OK.
2026-10-26 00:20:31 +00:00

202 lines
6.9 KiB
Python

"""Cells for trie.Trie.
Every cell is a unittest.TestCase method so `python3 -m unittest discover`
collects it. The module imported is pinned to the file beside this one: an
environment that shadows `trie` (a host copy, an installed package) raises
instead of grading the wrong file green -- Q68's fault, measured in this lane.
`python3 test_trie.py` still runs the same cells from __main__.
"""
import os
import random
import sys
import unittest
_HERE = os.path.dirname(os.path.abspath(__file__))
if sys.path and sys.path[0] != _HERE:
sys.path.insert(0, _HERE)
import trie as _trie_module
from trie import Trie
# abspath, not realpath: realpath would resolve a symlinked module to the same
# string as its target and let a symlink-out pass as "the file beside me".
_EXPECTED = os.path.abspath(os.path.join(_HERE, "trie.py"))
_GOT = os.path.abspath(_trie_module.__file__)
if _GOT != _EXPECTED:
raise ImportError(
"trie resolved to %r, expected %r -- these cells would grade the wrong file"
% (_GOT, _EXPECTED)
)
class TestBasic(unittest.TestCase):
def test_basic(self):
t = Trie()
for w in ["cat", "car", "card", "dog", ""]:
t.insert(w)
self.assertTrue(t.search("cat"))
self.assertTrue(t.search(""))
self.assertTrue(t.search("card"))
self.assertFalse(t.search("ca"))
self.assertTrue(t.starts_with("ca"))
self.assertFalse(t.starts_with("z"))
self.assertEqual(t.words_with_prefix("ca"), ["car", "card", "cat"])
self.assertEqual(t.count_prefix("ca"), 3)
self.assertEqual(len(t), 5)
def test_module_resolved_beside_these_cells(self):
self.assertEqual(_GOT, _EXPECTED)
class TestDelete(unittest.TestCase):
def test_delete(self):
t = Trie()
for w in ["car", "card", "care"]:
t.insert(w)
self.assertTrue(t.delete("car"))
self.assertFalse(t.search("car"))
self.assertTrue(t.search("card"))
self.assertTrue(t.search("care"))
self.assertTrue(t.starts_with("car"))
self.assertFalse(t.delete("car"))
# deleting the shared-prefix leaves should prune the branch
t.delete("card")
t.delete("care")
self.assertFalse(t.starts_with("car"))
def test_delete_prunes_counters_with_it(self):
t = Trie()
for w in ["car", "card", "care"]:
t.insert(w)
t.delete("card")
t.delete("care")
self.assertEqual(t.count_prefix("car"), 1) # only "car" left below it
self.assertEqual(t.count_prefix("car"), len(t.words_with_prefix("car")))
t.delete("car")
self.assertEqual(t.count_prefix(""), 0) # branch pruned, root empty
self.assertEqual(len(t), 0)
class TestDuplicateInsert(unittest.TestCase):
def test_duplicate_insert(self):
t = Trie()
t.insert("abc")
t.insert("abc")
self.assertTrue(t.search("abc"))
t.delete("abc")
# one delete removes the word entirely: multiplicity is not tracked,
# so the counter must read 1, not 2, for the whole lifetime
self.assertFalse(t.search("abc"))
self.assertEqual(len(t), 0)
self.assertEqual(t.count_prefix(""), 0)
def test_duplicate_insert_does_not_inflate_counts(self):
t = Trie()
t.insert("abc")
t.insert("abc")
self.assertEqual(len(t), 1)
self.assertEqual(t.count_prefix("a"), 1)
self.assertEqual(t.count_prefix("ab"), 1)
self.assertEqual(t.count_prefix("abc"), 1)
self.assertEqual(t.count_prefix("a"), len(t.words_with_prefix("a")))
class TestCountPrefixAgreesWithEnumeration(unittest.TestCase):
"""The item's cell: the counter may not disagree with the enumeration."""
def assert_agrees(self, t, prefixes):
for p in prefixes:
self.assertEqual(
t.count_prefix(p),
len(t.words_with_prefix(p)),
"prefix %r: counter %d vs enumerated %d"
% (p, t.count_prefix(p), len(t.words_with_prefix(p))),
)
def test_after_delete_of_a_word(self):
t = Trie()
for w in ["cat", "car", "card", "dog", ""]:
t.insert(w)
t.delete("car")
self.assert_agrees(t, ["", "c", "ca", "car", "cat", "card", "d", "do", "dog", "z"])
def test_after_delete_of_a_prefix_word(self):
t = Trie()
for w in ["a", "ab", "abc"]:
t.insert(w)
t.delete("a")
self.assertEqual(len(t), 2)
self.assert_agrees(t, ["", "a", "ab", "abc"])
def test_after_insert_then_delete_of_everything(self):
t = Trie()
t.insert("zzz")
t.delete("zzz")
self.assertEqual(len(t), 0)
self.assert_agrees(t, ["", "z", "zz", "zzz"])
def test_root_count_equals_len(self):
t = Trie()
for w in ["a", "ab", "abc", "", "b"]:
t.insert(w)
self.assertEqual(t.count_prefix(""), len(t))
self.assertEqual(t.count_prefix(""), 5)
def test_interleaved_insert_delete_random(self):
rng = random.Random(1234)
t = Trie()
live = set()
prefixes = ["", "a", "b", "c", "aa", "ab", "ba", "cc"]
for _ in range(400):
w = "".join(rng.choice("abc") for _ in range(rng.randint(0, 3)))
if rng.random() < 0.6:
t.insert(w)
live.add(w)
else:
removed = t.delete(w)
self.assertEqual(removed, w in live, "delete(%r) returned %r" % (w, removed))
live.discard(w)
self.assertEqual(len(t), len(live))
self.assert_agrees(t, prefixes)
for p in prefixes:
self.assertEqual(
set(t.words_with_prefix(p)), {x for x in live if x.startswith(p)}
)
class TestRandomVsBruteforce(unittest.TestCase):
def test_random_vs_bruteforce(self):
rng = random.Random(123)
words = []
alphabet = "ab"
for _ in range(200):
w = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 5)))
words.append(w)
t = Trie()
s = set()
for w in words:
t.insert(w)
s.add(w)
for w in set(words):
self.assertEqual(t.search(w), w in s)
# delete half
to_del = list(s)[: len(s) // 2]
for w in to_del:
self.assertTrue(t.delete(w))
s.discard(w)
for w in set(words):
self.assertEqual(t.search(w), w in s, (w, t.search(w), w in s))
# prefix enumeration matches
for p in ["", "a", "b", "aa", "ab"]:
self.assertEqual(set(t.words_with_prefix(p)), {w for w in s if w.startswith(p)})
# and the counter agrees with it after the deletes
for p in ["", "a", "b", "aa", "ab"]:
self.assertEqual(t.count_prefix(p), len(t.words_with_prefix(p)))
self.assertEqual(t.count_prefix(""), len(t))
if __name__ == "__main__":
unittest.main(verbosity=2)