local script = debug.getinfo(1, "S").source:sub(2) local root = script:match("^(.*)/tests/conformance%.lua$") or "." package.path = table.concat({ root .. "/lua/?.lua", root .. "/lua/?/init.lua", package.path, }, ";") local composition_root = require("clever_tee.composition_root") local domain = require("clever_tee.domain") local sequence_state = require("clever_tee.sequence_state") local text_topology = require("clever_tee.text_topology") local MemoryHost = require("clever_tee.testing.memory_host") local invariants = dofile(root .. "/tests/invariants.lua") local NULL = vim.NIL local function is_null(value) return value == NULL end local function nullable(value) if is_null(value) then return nil end return value end local function read_file(path) local file, open_error = io.open(path, "rb") if file == nil then error(open_error, 0) end local text = file:read("*a") file:close() return text end local function decode(path) return vim.json.decode(read_file(path), { luanil = { object = true, array = true } }) end local function fail(path, expected, actual) error(path .. ": expected " .. vim.inspect(expected) .. ", got " .. vim.inspect(actual), 0) end local function deep_same(expected, actual, path, partial) path = path or "value" if is_null(expected) then expected = nil end if is_null(actual) then actual = nil end if type(expected) ~= "table" then if expected ~= actual then fail(path, expected, actual) end return end if type(actual) ~= "table" then if partial and next(expected) == nil then return end fail(path, expected, actual) end local expected_count = 0 for key, value in pairs(expected) do expected_count = expected_count + 1 deep_same(value, actual[key], path .. "." .. tostring(key), partial) end if not partial then local actual_count = 0 for _ in pairs(actual) do actual_count = actual_count + 1 end if expected_count ~= actual_count then fail(path .. " key count", expected_count, actual_count) end end end local function position(value) value = nullable(value) return value and domain.Position.new(value.line, value.byte_column) or nil end local function selection(value) return { active = value.active, kind = value.kind, anchor = position(value.anchor), focus = position(value.focus), option = value.option, } end local function target(value) value = nullable(value) if value == nil then return nil end return domain.TargetValue.from_table({ kind = value.kind, value = value.value, first_code = value.first_code, }) end local function sorted_keys(values) local keys = {} for key in pairs(values) do keys[#keys + 1] = key end table.sort(keys) return keys end local function resource_metadata(scenario) local result = {} for _, item in ipairs(scenario.expect.overlay_plans) do if item.identity ~= nil then result[item.identity] = result[item.identity] or {} local metadata = result[item.identity] for _, key in ipairs({ "group", "window", "anchor_line", "priority" }) do if item[key] ~= nil then metadata[key] = item[key] end end if item.positions ~= nil then metadata.positions = item.positions end end end return result end local function load_state(state, fixture_state, metadata) sequence_state._mutate(state, function(data) data.previous_descriptor = {} data.previous_landing = {} data.first_move = {} data.previous_target = {} data.known_contexts = {} data.last_input_context = nil data.moved_forward = fixture_state.moved_forward data.moved_forward_initialized = fixture_state.moved_forward_initialized data.migemo_cache = {} data.repeat_timestamp_ms = fixture_state.repeat_timestamp_ms data.highlight_timer = nullable(fixture_state.highlight_timer) data.target_overlays = {} data.temporary_overlays = {} data.finalizers = {} for _, key in ipairs(sorted_keys(fixture_state.contexts)) do local context = domain.ModeContext.from_full_mode(key) local record = fixture_state.contexts[key] data.known_contexts[context] = true local descriptor = nullable(record.previous_descriptor) data.previous_descriptor[context] = descriptor and domain.Descriptor.from_string(descriptor) or nil data.previous_landing[context] = position(record.previous_landing) data.first_move[context] = nullable(record.first_move) data.previous_target[context] = target(record.previous_target) end local input_context = nullable(fixture_state.last_input_context) data.last_input_context = input_context and domain.ModeContext.from_full_mode(input_context) or nil for _, encoding in ipairs(fixture_state.migemo_cache) do data.migemo_cache[encoding] = { fixture_dictionary = encoding } end for _, identity in ipairs(fixture_state.target_overlays) do local item = metadata[identity] or {} data.target_overlays[#data.target_overlays + 1] = { identity = identity, window = item.window or "window-1", anchor_line = item.anchor_line or 1, } end for _, identity in ipairs(fixture_state.temporary_overlays) do local item = metadata[identity] or {} data.temporary_overlays[#data.temporary_overlays + 1] = { identity = identity, window = item.window or "window-1", group = item.group or "CleverTeeDirect", } end for _, identity in ipairs(fixture_state.finalizers) do data.finalizers[#data.finalizers + 1] = { identity = identity, buffer = "buffer-1", } end end) end local function seed_highlight(host, identity, metadata) if host:highlights()[identity] ~= nil then return end local item = metadata[identity] or {} local specification = { identity = identity, group = item.group or "CleverTeeChar", window = item.window or "window-1", priority = item.priority or "high", anchor_line = item.anchor_line or 1, } if item.positions ~= nil then specification.positions = {} for index, value in ipairs(item.positions) do specification.positions[index] = position(value) end end host:create_highlight(specification) end local function rename_registration(host, generated, identity) if generated == identity then return end local registration = host._event_registrations[generated] host._event_registrations[generated] = nil registration.identity = identity host._event_registrations[identity] = registration for index, value in ipairs(host._event_registration_order) do if value == generated then host._event_registration_order[index] = identity end end end local function seed_resources(context, fixture_state) local host = context.host local feedback = context.feedback for _, identity in ipairs(fixture_state.target_overlays) do seed_highlight(host, identity, context.metadata) end for _, identity in ipairs(fixture_state.temporary_overlays) do seed_highlight(host, identity, context.metadata) end local timer_identity = nullable(fixture_state.highlight_timer) if timer_identity ~= nil and host:timers()[timer_identity] == nil then local generated generated = host:start_timer(1, function(callback_identity) feedback:handle_highlight_timer(callback_identity or generated, "window-1") end) if generated ~= timer_identity then error("fixture timer identity sequence differs: " .. generated .. " and " .. timer_identity, 0) end end for _, identity in ipairs(fixture_state.finalizers) do local registrations = host:event_registrations() if registrations[identity] == nil then local generated = host:register_events( { "CursorMoved", "InsertEnter", "TextChanged" }, function(name, payload) feedback:handle_finalizer_event(name, payload) end, { buffer = "buffer-1" } ) rename_registration(host, generated, identity) end end end local ACTION_METHODS = { StartFindForward = "start_find_forward", StartFindBackward = "start_find_backward", StartTillForward = "start_till_forward", StartTillBackward = "start_till_backward", RepeatSameDirection = "repeat_same_direction", RepeatOppositeDirection = "repeat_opposite_direction", Reset = "reset", DiagnosticFullReset = "diagnostic_full_reset", } local function invoke_action(context, name) if name == "DotRepeat" then return context.host:replay_dot(context.host:read_count().value) end local method = ACTION_METHODS[name] if method == nil then error("unknown fixture action " .. tostring(name), 0) end return context.facade[method](context.facade) end local function delete_cursor_character(host) local cursor = host:read_cursor() local lines = host:read_text():lines() local line = lines[cursor.line] local before = line:sub(1, cursor.byte_column - 1) local after = line:sub(cursor.byte_column + 1) lines[cursor.line] = before .. after host:set_text(lines) host:set_cursor({ line = cursor.line, byte_column = math.max(1, math.min(cursor.byte_column, #lines[cursor.line])), }) end local function annotate_operations(context, first_index, step_index) local operations = context.host:operations() for index = first_index, #operations do operations[index].after_step = step_index context.operations[#context.operations + 1] = operations[index] end end local function run_step(context, step, step_index) local before = #context.host:operations() + 1 local operation = step.operation local outcome if operation == "action" then if nullable(step.count) ~= nil then context.host:set_count(step.count) end local before_state = context.state:snapshot() local mode = context.host:read_mode() local origin = context.host:read_cursor() local before_view = text_topology.from_host(context.host) local persistent_request_count = #context.feedback:persistent_requests() outcome = invoke_action(context, step.action) invariants.after_action({ action = step.action, outcome = outcome, host = context.host, state = context.state, coordinator = context.facade:coordinator(), feedback = context.feedback, before_state = before_state, before_view = before_view, persistent_request_count = persistent_request_count, mode = mode, origin = origin, }) elseif operation == "set_cursor" then context.host:set_cursor(position(step.position)) elseif operation == "set_mode" then context.host:set_mode(step.mode) elseif operation == "set_selection" then context.host:set_selection(selection(step.selection)) elseif operation == "set_state" then load_state(context.state, step.state, context.metadata) seed_resources(context, step.state) before = #context.host:operations() + 1 elseif operation == "editor_key" then if step.key ~= "x" then error("unknown fixture editor key " .. tostring(step.key), 0) end delete_cursor_character(context.host) elseif operation == "event" then local event = context.scenario.given.events[step.event_index + 1] if event.cursor ~= nil then context.host:set_cursor(position(event.cursor)) end if event.buffer_lines ~= nil then context.host:set_text(event.buffer_lines) end local payload = { buffer = "buffer-1", window = "window-1", cursor = event.cursor ~= nil and position(event.cursor) or nil, } if event.name == "WinEnter" or event.name == "WinLeave" or event.name == "CmdwinLeave" then context.feedback:handle_eager_event(event.name, payload) else context.host:deliver_event(event.name, payload) end elseif operation == "timer_callback" then local identity = step.timer_identity local current = context.state.highlight_timer == identity context.timer_callbacks[#context.timer_callbacks + 1] = { after_step = step_index, operation = "fire", identity = identity, } local fired = context.host:fire_timer(identity) if current then context.timer_callbacks[#context.timer_callbacks + 1] = { after_step = step_index, operation = "set_inactive", identity = identity, } elseif not fired then context.feedback:handle_highlight_timer(identity, "window-1") context.timer_callbacks[#context.timer_callbacks + 1] = { after_step = step_index, operation = "ignore", identity = identity, } end else error("unknown fixture operation " .. tostring(operation), 0) end annotate_operations(context, before, step_index) if outcome ~= nil then local item = outcome:to_table() item.after_step = step_index item.buffer_lines = context.host:read_text():lines() context.outcomes[#context.outcomes + 1] = item end context.snapshots[step_index] = context.state:to_table() end local function build_context(scenario) local given = scenario.given local host_options = { buffer_lines = given.buffer.lines, effective_encoding = given.effective_encoding, cursor = given.cursor, mode = given.mode, selection = selection(given.selection), pending_operator = nullable(given.pending_operator), count = nullable(given.count), configuration = given.configuration, input_packets = given.input_packets, time_values_ms = given.time_values_ms, macro_register = nullable(given.host.macro_register), fold_open_policy = given.host.fold_open_policy, closed_fold_levels = given.host.closed_fold_levels, timer_support = given.host.timer_support, cmdline_cursor_support = given.host.cmdline_cursor_support, emit_movement_events = false, } local host = MemoryHost.new(host_options) local root_object = composition_root.new({ host = host }) local state = sequence_state.get() local context = { scenario = scenario, host = host, facade = root_object:facade(), feedback = root_object:feedback(), state = state, metadata = resource_metadata(scenario), operations = {}, outcomes = {}, snapshots = {}, timer_callbacks = {}, identity_aliases = {}, } load_state(state, given.state, context.metadata) seed_resources(context, given.state) host:clear_operations() return context end local function operation_list(context, name) local result = {} for _, item in ipairs(context.operations) do if item.operation == name then result[#result + 1] = item end end return result end local function map_created_identities(context) local actual = operation_list(context, "create_highlight") local expected = {} for _, item in ipairs(context.scenario.expect.overlay_plans) do if item.operation == "create" or item.operation == "rebuild" then expected[#expected + 1] = item end end local actual_index = 1 for _, wanted in ipairs(expected) do while actual[actual_index] ~= nil and actual[actual_index].after_step < wanted.after_step do actual_index = actual_index + 1 end local found = actual[actual_index] if found == nil or found.after_step ~= wanted.after_step then error("missing overlay creation for " .. wanted.identity, 0) end context.identity_aliases[wanted.identity] = found.identity actual_index = actual_index + 1 end local registrations = operation_list(context, "register_events") local registration_index = 1 for _, wanted in ipairs(context.scenario.expect.host_operations) do if wanted.operation == "register_finalizer" then while registrations[registration_index] ~= nil and registrations[registration_index].after_step < wanted.after_step do registration_index = registration_index + 1 end local found = registrations[registration_index] if found == nil or found.after_step ~= wanted.after_step then error("missing finalizer registration for " .. wanted.identity, 0) end context.identity_aliases[wanted.identity] = found.identity registration_index = registration_index + 1 end end end local function alias_identity(context, identity) return context.identity_aliases[identity] or identity end local function aliased_state(context, expected) local result = vim.deepcopy(expected) for _, field in ipairs({ "target_overlays", "temporary_overlays", "finalizers" }) do if result[field] ~= nil then for index, identity in ipairs(result[field]) do result[field][index] = alias_identity(context, identity) end end end if result.highlight_timer ~= nil and not is_null(result.highlight_timer) then result.highlight_timer = alias_identity(context, result.highlight_timer) end return result end local function target_plan_name(plan) if not domain.TargetPlan.is(plan) then return nil end return table.concat({ plan.kind.value, plan.case_mode.value, plan.target.value }, "-") end local function actual_overlay_operations(context) local metadata = {} for identity, item in pairs(context.metadata) do metadata[identity] = vim.deepcopy(item) end local result = {} for _, item in ipairs(context.operations) do if item.operation == "create_highlight" then metadata[item.identity] = item local positions = item.positions if positions == nil and item.position ~= nil then positions = { item.position } end result[#result + 1] = { after_step = item.after_step, operation = "create", group = item.group, identity = item.identity, window = item.window, positions = positions, priority = item.priority, anchor_line = item.anchor_line, descriptor = domain.Descriptor.is(item.descriptor) and item.descriptor.value or item.descriptor, target_plan = target_plan_name(item.target_plan), } elseif item.operation == "remove_highlight" then local prior = metadata[item.identity] or {} result[#result + 1] = { after_step = item.after_step, operation = "remove", group = prior.group, identity = item.identity, window = prior.window, } end end return result end local function assert_overlay_plans(context) local actual = actual_overlay_operations(context) local used = {} for _, expected in ipairs(context.scenario.expect.overlay_plans) do local identity = alias_identity(context, expected.identity) if expected.operation == "preserve" then for _, item in ipairs(actual) do if item.after_step == expected.after_step and item.identity == identity and item.operation == "remove" then error("preserved overlay was removed: " .. expected.identity, 0) end end else local wanted_operation = expected.operation == "rebuild" and "create" or expected.operation local found local found_index for index, item in ipairs(actual) do if not used[index] and item.after_step == expected.after_step and item.operation == wanted_operation and item.identity == identity then found = item found_index = index break end end if found == nil then error("missing overlay operation " .. expected.operation .. " for " .. expected.identity, 0) end used[found_index] = true local wanted = vim.deepcopy(expected) wanted.operation = wanted_operation wanted.identity = identity deep_same(wanted, found, "overlay " .. expected.identity, true) end end end local function meaningful_host_operations(context) local result = {} for _, item in ipairs(context.operations) do local transformed if item.operation == "suppress_cursor_presentation" and item.supported then transformed = { operation = "hide_cursor_presentation" } elseif item.operation == "restore_cursor_presentation" and item.restored then transformed = { operation = "restore_cursor_presentation" } elseif item.operation == "read_input" then transformed = { operation = "read_input" } elseif item.operation == "apply_cursor" then transformed = { operation = "apply_cursor", position = item.position } elseif item.operation == "apply_selection" then transformed = { operation = "apply_selection", position = item.position, kind = item.kind, } elseif item.operation == "open_fold" and item.opened then transformed = { operation = "open_fold", fold_level = item.fold_level } elseif item.operation == "register_events" then transformed = { operation = "register_finalizer", identity = item.identity } elseif item.operation == "remove_event_registration" then transformed = { operation = "remove_finalizer", identity = item.identity } end if transformed ~= nil then transformed.after_step = item.after_step result[#result + 1] = transformed end end return result end local function assert_expected_sequence(expected, actual, context, label) local actual_index = 1 for _, wanted_source in ipairs(expected) do local wanted = vim.deepcopy(wanted_source) if wanted.identity ~= nil then wanted.identity = alias_identity(context, wanted.identity) end local found while actual_index <= #actual do local candidate = actual[actual_index] actual_index = actual_index + 1 if pcall(deep_same, wanted, candidate, label, true) then found = candidate break end end if found == nil then error( "missing expected " .. label .. ": " .. vim.inspect(wanted_source) .. "\nactual " .. label .. "s: " .. vim.inspect(actual), 0 ) end end end local function actual_timer_operations(context) local result = {} for _, item in ipairs(context.operations) do if item.operation == "start_timer" and item.supported then result[#result + 1] = { after_step = item.after_step, operation = "start", identity = item.identity, delay_ms = item.delay_ms, } elseif item.operation == "stop_timer" then result[#result + 1] = { after_step = item.after_step, operation = "stop", identity = item.identity, } end end for _, item in ipairs(context.timer_callbacks) do result[#result + 1] = item end table.sort(result, function(left, right) if left.after_step ~= right.after_step then return left.after_step < right.after_step end local order = { stop = 1, start = 2, fire = 3, set_inactive = 4, ignore = 5 } return order[left.operation] < order[right.operation] end) return result end local function operation_events(context, operation, transform) local result = {} for _, item in ipairs(context.operations) do if item.operation == operation then result[#result + 1] = transform(item) end end return result end local function assert_scenario(context) local scenario = context.scenario map_created_identities(context) if #scenario.expect.action_outcomes ~= #context.outcomes then fail("action outcome count", #scenario.expect.action_outcomes, #context.outcomes) end deep_same(scenario.expect.action_outcomes, context.outcomes, "action outcomes", true) local final_lines = context.host:read_text():lines() deep_same(scenario.expect.final_buffer, final_lines, "final buffer", false) deep_same(scenario.expect.final_position, context.host:read_cursor():to_table(), "final position", false) deep_same(scenario.expect.final_selection, context.host:read_selection():to_table(), "final selection", false) for _, expected in ipairs(scenario.expect.state_snapshots) do local actual = context.snapshots[expected.after_step] deep_same( aliased_state(context, expected.state), actual, "state after step " .. tostring(expected.after_step), true ) end assert_overlay_plans(context) deep_same(scenario.expect.timer_operations, actual_timer_operations(context), "timer operations", false) deep_same(scenario.expect.prompts, context.host:prompts(), "prompts", false) local redraws = operation_events(context, "redraw", function(item) return { after_step = item.after_step, kind = item.kind } end) deep_same(scenario.expect.redraws, redraws, "redraws", false) local diagnostics = operation_events(context, "emit_diagnostic", function(item) return { after_step = item.after_step, level = item.level, text = item.text } end) deep_same(scenario.expect.diagnostics, diagnostics, "diagnostics", false) assert_expected_sequence( scenario.expect.host_operations, meaningful_host_operations(context), context, "host operation" ) end local function run_scenario(scenario) local context = build_context(scenario) for index, step in ipairs(scenario.steps) do run_step(context, step, index - 1) end assert_scenario(context) end local function ledger_fixture_ids(path) local result = {} local seen = {} local first = true for line in read_file(path):gmatch("[^\r\n]+") do if first then first = false else local columns = vim.split(line, "\t", { plain = true }) local fixture_ids = columns[8] if fixture_ids ~= "-" then for _, identity in ipairs(vim.split(fixture_ids, ",", { plain = true })) do if not seen[identity] then seen[identity] = true result[#result + 1] = identity end end end end end table.sort(result) return result end local scenarios = {} for _, filename in ipairs({ "reference-examples.json", "transition-fixtures.json" }) do local document = decode(root .. "/conformance/fixtures/" .. filename) if document.format_version ~= 1 then error(filename .. " has an unsupported format version", 0) end for _, scenario in ipairs(document.scenarios) do if scenarios[scenario.id] ~= nil then error("duplicate conformance scenario " .. scenario.id, 0) end scenarios[scenario.id] = scenario end end local fixture_ids = ledger_fixture_ids(root .. "/conformance/requirements.tsv") for _, identity in ipairs(fixture_ids) do local scenario = scenarios[identity] if scenario == nil then error("ledger fixture is missing: " .. identity, 0) end local ok, failure = xpcall(function() run_scenario(scenario) end, debug.traceback) if not ok then io.stderr:write("FAIL: " .. identity .. "\n" .. tostring(failure) .. "\n") os.exit(1) end end io.stdout:write(string.format( "Phase 16 conformance ledger: %d fixtures passed\n", #fixture_ids ))