summaryrefslogtreecommitdiff
path: root/tests/run.lua
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-03 23:07:34 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-03 23:07:34 +0200
commit3b1447358ce9ddddd454e080003c94c494320e6d (patch)
tree9f7d906d63ca9ab7392015148e112b341e515ebf /tests/run.lua
parent4ace71b059d8db228e05363cfee77586f52df136 (diff)
Implement Migemo target matching
Diffstat (limited to 'tests/run.lua')
-rw-r--r--tests/run.lua319
1 files changed, 318 insertions, 1 deletions
diff --git a/tests/run.lua b/tests/run.lua
index 9034b97..6ca2327 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -10,6 +10,7 @@ local domain = require("clever_f.domain")
local capabilities = require("clever_f.capabilities")
local case_policy = require("clever_f.case_policy")
local policy = require("clever_f.policy")
+local migemo_catalog = require("clever_f.migemo_catalog")
local sequence_state = require("clever_f.sequence_state")
local state_transitions = require("clever_f.state_transitions")
local text_topology = require("clever_f.text_topology")
@@ -1819,6 +1820,322 @@ test("Target plan inputs enforce matching contracts", function()
end, "preserve")
end)
+local MIGEMO_HIRAGANA_A = "\227\129\130"
+local MIGEMO_KANJI = "\230\188\162"
+local MIGEMO_SHELTER = "\229\142\166"
+local MIGEMO_GATE = "\233\150\128"
+
+local function read_binary(path)
+ local handle, open_error = io.open(path, "rb")
+ if handle == nil then
+ error("could not open " .. path .. ": " .. tostring(open_error), 2)
+ end
+ local contents = handle:read("*a")
+ handle:close()
+ return contents
+end
+
+local function migemo_components(configuration)
+ local _, transitions = fresh_sequence_state()
+ local host = MemoryHost.new({ configuration = configuration })
+ local service = policy.new(host)
+ local catalog = migemo_catalog.new({
+ policy = service,
+ transitions = transitions,
+ })
+ local factory = target_plan.new({
+ policy = service,
+ migemo_catalog = catalog,
+ })
+ return host, service, catalog, factory, transitions
+end
+
+test("Bundled Migemo assets equal the normative source data", function()
+ local assets = {
+ {
+ encoding = "utf-8",
+ file = "utf8.vim",
+ hash = "fccd16452c105fcb7303d855b96d841f8174d37affa0c43d8b49a54e68289f3e",
+ },
+ {
+ encoding = "cp932",
+ file = "cp932.vim",
+ hash = "3823fda790f43876be78d53e116187f2e0eedc14d203756ea66a3814ca9707f1",
+ },
+ {
+ encoding = "euc-jp",
+ file = "eucjp.vim",
+ hash = "d6d7a2a795d7e3e14cdefd0cf2597bc48bd3d89dbdc41019f460027377855079",
+ },
+ }
+
+ for _, asset in ipairs(assets) do
+ local bundled_path = migemo_catalog.bundled_asset_path(asset.encoding)
+ local bundled = read_binary(bundled_path)
+ same(asset.hash, vim.fn.sha256(bundled), asset.file .. " checksum")
+ end
+end)
+
+test("Migemo dictionaries validate ordered keys and cache each encoding", function()
+ local state, transitions = fresh_sequence_state()
+ local catalog = migemo_catalog.new({ transitions = transitions })
+ local expected_keys = migemo_catalog.expected_keys()
+ same(52, #expected_keys)
+ same(
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
+ table.concat(expected_keys)
+ )
+
+ local encodings = {
+ { requested = "UTF8", canonical = "utf-8" },
+ { requested = "CP-932", canonical = "cp932" },
+ { requested = "EUCJP", canonical = "euc-jp" },
+ }
+ for _, item in ipairs(encodings) do
+ local first = catalog:get(item.requested)
+ local second = catalog:get(item.canonical)
+ same(first, second)
+ same(first, state:get_migemo(item.canonical))
+ same(item.canonical, first.encoding)
+ same(52, first.entry_count)
+ list_same(expected_keys, first:keys())
+ same(1, catalog:load_count(item.canonical))
+
+ for _, key in ipairs(expected_keys) do
+ same("function", type(first[key]))
+ truthy(first:has(key))
+ truthy(first:matches(key, key, domain.CaseMode.SENSITIVE))
+ end
+ fails(function()
+ first.encoding = "changed"
+ end, "immutable")
+ end
+ same(3, map_size(state.migemo_cache))
+end)
+
+test("Migemo catalog rejects wrong entry counts and key order", function()
+ local _, transitions = fresh_sequence_state()
+ local keys = migemo_catalog.expected_keys()
+ local data = {}
+ for _, key in ipairs(keys) do
+ data[key] = key
+ end
+ local compiler = function()
+ return function()
+ return false
+ end
+ end
+
+ local wrong_order = migemo_catalog.expected_keys()
+ wrong_order[1], wrong_order[2] = wrong_order[2], wrong_order[1]
+ local order_catalog = migemo_catalog.new({
+ transitions = transitions,
+ asset_loader = function()
+ return data, wrong_order, "ordered-test"
+ end,
+ pattern_compiler = compiler,
+ })
+ fails(function()
+ order_catalog:get("utf-8")
+ end, "ordered a through z, then A through Z")
+
+ data[keys[#keys]] = nil
+ local count_catalog = migemo_catalog.new({
+ transitions = transitions,
+ asset_loader = function()
+ return data, keys, "count-test"
+ end,
+ pattern_compiler = compiler,
+ })
+ fails(function()
+ count_catalog:get("utf-8")
+ end, "exactly 52 keys")
+end)
+
+test("Native Migemo predicates agree across a fixed encoding corpus", function()
+ local _, transitions = fresh_sequence_state()
+ local catalog = migemo_catalog.new({ transitions = transitions })
+ local corpus = {
+ { "a", "a", domain.CaseMode.SENSITIVE, true },
+ { "a", "A", domain.CaseMode.SENSITIVE, false },
+ { "a", "A", domain.CaseMode.INSENSITIVE, true },
+ { "a", MIGEMO_HIRAGANA_A, domain.CaseMode.SENSITIVE, true },
+ { "k", MIGEMO_KANJI, domain.CaseMode.SENSITIVE, true },
+ {
+ "a",
+ MIGEMO_SHELTER .. "\n" .. MIGEMO_GATE,
+ domain.CaseMode.SENSITIVE,
+ true,
+ },
+ { "a", "x" .. MIGEMO_HIRAGANA_A, domain.CaseMode.SENSITIVE, false },
+ }
+
+ local saved_ignorecase = vim.o.ignorecase
+ local saved_smartcase = vim.o.smartcase
+ vim.o.ignorecase = true
+ vim.o.smartcase = true
+ for _, encoding in ipairs(migemo_catalog.supported_encodings()) do
+ local dictionary = catalog:get(encoding)
+ for index, item in ipairs(corpus) do
+ same(
+ item[4],
+ dictionary:matches(item[1], item[2], item[3]),
+ encoding .. " corpus item " .. tostring(index)
+ )
+ end
+ end
+ vim.o.ignorecase = saved_ignorecase
+ vim.o.smartcase = saved_smartcase
+end)
+
+test("Migemo activation uses live policy, scope, and branch precedence", function()
+ local host, _, _, factory = migemo_components({
+ use_migemo = true,
+ search_current_line_only = false,
+ chars_match_any_signs = "a;",
+ })
+ local ascii_view = text_topology.new({ "xax" }, "utf-8")
+ local context = {
+ text_view = ascii_view,
+ origin = domain.Position.new(1, 1),
+ }
+
+ local migemo = factory:build(target("a"), nil, context)
+ same(domain.TargetPlanKind.MIGEMO, migemo.kind)
+
+ host:set_configuration("use_migemo", false)
+ local symbol = factory:build(target("a"), nil, context)
+ same(domain.TargetPlanKind.SYMBOL, symbol.kind)
+
+ host:set_configuration("use_migemo", true)
+ same(domain.TargetPlanKind.SYMBOL, factory:build(target(";"), nil, context).kind)
+ same(
+ domain.TargetPlanKind.LITERAL,
+ factory:build(target(MIGEMO_HIRAGANA_A, 0x3042), nil, context).kind
+ )
+ same(domain.TargetPlanKind.LITERAL, factory:build(target("ab", 97), nil, context).kind)
+
+ host:set_configuration("chars_match_any_signs", "")
+ host:set_configuration("search_current_line_only", true)
+ local ascii_line = factory:build(target("a"), nil, context)
+ same(domain.TargetPlanKind.LITERAL, ascii_line.kind)
+
+ local multibyte_view = text_topology.new({ "x" .. MIGEMO_HIRAGANA_A }, "utf-8")
+ local multibyte_line = factory:build(target("a"), nil, {
+ text_view = multibyte_view,
+ origin = domain.Position.new(1, 1),
+ })
+ same(domain.TargetPlanKind.MIGEMO, multibyte_line.kind)
+end)
+
+test("Migemo plans enforce case and one-character candidate constraints", function()
+ local host, _, catalog, factory = migemo_components({
+ use_migemo = true,
+ ignore_case = false,
+ smart_case = false,
+ })
+ local view = text_topology.new({
+ "aA" .. MIGEMO_HIRAGANA_A .. "Es",
+ }, "utf-8")
+ local context = {
+ text_view = view,
+ search_scope = domain.SearchScope.BUFFER,
+ }
+ local sensitive = factory:build(target("a"), nil, context)
+ same(domain.CaseMode.SENSITIVE, sensitive.case_mode)
+ truthy(sensitive:matches_at(view, view:position_for_character_index(1, 1)))
+ falsy(sensitive:matches_at(view, view:position_for_character_index(1, 2)))
+ truthy(sensitive:matches_at(view, view:position_for_character_index(1, 3)))
+
+ local other_letter = view:position_for_character_index(1, 4)
+ truthy(catalog:get("utf-8"):matches(
+ "a",
+ view:text_suffix(other_letter),
+ domain.CaseMode.SENSITIVE
+ ))
+ falsy(sensitive:matches_at(view, other_letter))
+ falsy(sensitive:matches("x", view:position_for_character_index(1, 1), view))
+
+ host:set_configuration("ignore_case", true)
+ local insensitive = factory:build(target("a"), nil, context)
+ same(domain.CaseMode.INSENSITIVE, insensitive.case_mode)
+ truthy(insensitive:matches_at(view, view:position_for_character_index(1, 2)))
+end)
+
+test("Migemo assertions inspect later lines while limiting candidate starts", function()
+ local _, _, _, factory = migemo_components({
+ use_migemo = true,
+ search_current_line_only = true,
+ })
+ local view = text_topology.new({
+ "x" .. MIGEMO_SHELTER,
+ "",
+ MIGEMO_GATE,
+ MIGEMO_HIRAGANA_A,
+ }, "utf-8")
+ local candidate = view:position_for_character_index(1, 2)
+ local plan = factory:build(target("a"), nil, {
+ text_view = view,
+ origin = domain.Position.new(1, 1),
+ })
+
+ same(domain.TargetPlanKind.MIGEMO, plan.kind)
+ same(
+ MIGEMO_SHELTER .. "\n\n" .. MIGEMO_GATE
+ .. "\n" .. MIGEMO_HIRAGANA_A,
+ view:text_suffix(candidate)
+ )
+ truthy(plan:matches_at(view, candidate))
+ local matched_start = plan:matches_at(view, candidate) and candidate or nil
+ same(candidate, matched_start)
+ falsy(plan:matches_at(view, domain.Position.new(4, 1)))
+end)
+
+test("Unsupported Migemo encoding disables live policy and raises exactly", function()
+ local host, service, _, factory = migemo_components({
+ use_migemo = true,
+ search_current_line_only = false,
+ })
+ local view = text_topology.new({ "a" }, "latin1")
+ local ok, diagnostic = pcall(function()
+ factory:build(target("a"), nil, {
+ text_view = view,
+ search_scope = domain.SearchScope.BUFFER,
+ })
+ end)
+
+ falsy(ok)
+ same(
+ "clever-f: Encoding 'latin1' is not supported. Migemo is disabled",
+ diagnostic
+ )
+ falsy(service:get_boolean("use_migemo"))
+ same(false, host:read_configuration("use_migemo"))
+ same(0, map_size(sequence_state.get().migemo_cache))
+
+ local literal = factory:build(target("a"), nil, {
+ text_view = view,
+ search_scope = domain.SearchScope.BUFFER,
+ })
+ same(domain.TargetPlanKind.LITERAL, literal.kind)
+end)
+
+test("Public Reset discards loaded Migemo dictionary objects", function()
+ local state, transitions = fresh_sequence_state()
+ local catalog = migemo_catalog.new({ transitions = transitions })
+ local first = catalog:get("utf-8")
+ same(first, state:get_migemo("utf-8"))
+
+ transitions:PublicReset()
+ same(nil, state:get_migemo("utf-8"))
+ same(nil, catalog:cached("utf-8"))
+
+ local second = catalog:get("utf-8")
+ falsy(first == second)
+ same(second, state:get_migemo("utf-8"))
+ same(2, catalog:load_count("utf-8"))
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then
@@ -1828,4 +2145,4 @@ for _, item in ipairs(tests) do
passed = passed + 1
end
-io.stdout:write(string.format("Phase 6: %d tests passed\n", passed))
+io.stdout:write(string.format("Phase 7: %d tests passed\n", passed))