map/symbol.src:12 says "Bare 1-9 choose from a prefix search; keep digit aliases in matching slots", map/README:55 repeats it, and both tests/engine_test.c:1001 and tests/mkemoji_test.py were written to it. None of it happened. dictprefix ends in below(), which returns a node's own words first and then its children in rune order (dict.c:29-39), and under `^` the punctuation sorts ahead of the digits: ^ gave ⁽ ⁾ ⁺ ⁻ ⁰ ¹ ² ³ ⁴ ⁵ ... _ gave ₍ ₎ ₊ ₋ ₀ ₁ ₂ ₃ ₄ ₅ ... < gave ← ♥ 🫰 🫶 ≤ ≠ so the file's own aliases picked the wrong character every time: before after ^ then 1 ⁽ ¹ ^ then 2 ⁾ ² _ then 1 ₍ ₁ < then 3 🫰 ♥ mkemoji has no bare `^` row to emit, because no source row claims `^` as an alias -- the prefix exists in the trie only as the parent of `^1`..`^9` and `^(`..`^n`, and a parent has no words of its own. Giving the nine superscripts `^` as a second alias, the nine subscripts `_`, and ←≤♥≠ `<`, makes build() group them in source order and emit three rows: < ← ≤ ♥ ≠ ^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ _ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ which is what engine_test.c:1001's fixture has said all along, and the first thing below() now returns. Nothing is lost: `^0`, `^(`, `^i` and their kind still answer their own key, `0` is not a selection key so it still extends the query, and addkouho drops the duplicate when a child repeats what the parent already offered. Three rows on 18953. tests/mkemoji_test.py was asking the wrong question. It checked that the first nine `^` keys in the file are `^1`..`^9` -- true before and after, and decided nothing, because the file's order is not the trie's. It now checks the row the engine actually reads, and fails without this change with a bare KeyError on `^`. What this does not cover: `+1` and `-1`. They are not slot aliases, they are words -- plus one, minus one -- and ➕ and ➖ are the right first answers to `+` and `-`. 👍 is slot 3 of `+` and reachable there.
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
||
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
MKEMOJI = ROOT / "map" / "mkemoji"
|
||
TIMEOUT = 10
|
||
|
||
|
||
def generate(*sources):
|
||
args = [sys.executable, "-B", str(MKEMOJI)] + [str(s) for s in sources]
|
||
return subprocess.run(
|
||
args, capture_output=True, text=True, check=False, timeout=TIMEOUT
|
||
)
|
||
|
||
|
||
def table(output):
|
||
return dict(line.split("\t", 1) for line in output.splitlines())
|
||
|
||
|
||
class MkemojiTest(unittest.TestCase):
|
||
def source(self, text):
|
||
tmp = tempfile.TemporaryDirectory()
|
||
path = Path(tmp.name) / "emoji.src"
|
||
path.write_text(text, encoding="utf-8")
|
||
self.addCleanup(tmp.cleanup)
|
||
return path
|
||
|
||
def test_production_digit_slots(self):
|
||
result = generate()
|
||
self.assertEqual(result.returncode, 0, result.stderr)
|
||
data = table(result.stdout)
|
||
# The engine walks a prefix's children in rune order, so only the
|
||
# prefix's own row decides which candidate a digit picks.
|
||
self.assertEqual(data["^"].split(), list("¹²³⁴⁵⁶⁷⁸⁹"))
|
||
self.assertEqual(data["_"].split(), list("₁₂₃₄₅₆₇₈₉"))
|
||
self.assertEqual(data["<"].split()[2], "♥")
|
||
self.assertEqual(data["^1"], "¹")
|
||
self.assertEqual(data["_2"], "₂")
|
||
self.assertEqual(data["<3"].split()[0], "♥")
|
||
self.assertIn("😀", data["smile"].split())
|
||
self.assertIn("😀", data["웃음"].split())
|
||
|
||
def test_fold_normalize_and_source_order(self):
|
||
source = self.source(
|
||
"β\tALPHABET\talpha\n"
|
||
"α\talpha\n"
|
||
"é\tÉ\n"
|
||
"#\thash\n"
|
||
)
|
||
first = generate(source)
|
||
second = generate(source, source)
|
||
self.assertEqual(first.returncode, 0, first.stderr)
|
||
self.assertEqual(first.stdout, second.stdout)
|
||
data = table(first.stdout)
|
||
self.assertEqual(list(data), ["alphabet", "alpha", "é", "hash"])
|
||
self.assertEqual(data["alpha"].split(), ["β", "α"])
|
||
self.assertEqual(data["é"], "é")
|
||
self.assertEqual(data["hash"], "#")
|
||
|
||
def test_rejects_malformed_and_oversized_rows(self):
|
||
bad = [
|
||
"x\n",
|
||
"bad result\ta\n",
|
||
"x\t\n",
|
||
"x\t a\n",
|
||
"\0\ta\n",
|
||
"x\ta\0b\n",
|
||
"x\t;hidden\n",
|
||
"x" * 65 + "\ta\n",
|
||
"x\t" + "a" * 65 + "\n",
|
||
]
|
||
for text in bad:
|
||
with self.subTest(text=repr(text)):
|
||
result = generate(self.source(text))
|
||
self.assertEqual(result.returncode, 1)
|
||
|
||
def test_accepts_runtime_boundary(self):
|
||
source = self.source("x" * 64 + "\t" + "a" * 64 + "\n")
|
||
result = generate(source)
|
||
self.assertEqual(result.returncode, 0, result.stderr)
|
||
self.assertIn("a" * 64 + "\t" + "x" * 64, result.stdout)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|