Two faults, both re-measured in a foreign clone atdbc2c4fand 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 atdbc2c4f/ 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.
202 lines
6.9 KiB
Python
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)
|