summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--tests/conformance.lua806
2 files changed, 807 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 1a692cd..69cf034 100644
--- a/Makefile
+++ b/Makefile
@@ -4,5 +4,6 @@ NVIM ?= nvim
test:
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/run.lua
+ NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/conformance.lua
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/host_adapter_contract.lua
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/plugin_smoke.lua
diff --git a/tests/conformance.lua b/tests/conformance.lua
new file mode 100644
index 0000000..2598ab7
--- /dev/null
+++ b/tests/conformance.lua
@@ -0,0 +1,806 @@
+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_f.composition_root")
+local domain = require("clever_f.domain")
+local sequence_state = require("clever_f.sequence_state")
+local MemoryHost = require("clever_f.testing.memory_host")
+
+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 "CleverFDirect",
+ }
+ 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 "CleverFChar",
+ 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
+ outcome = invoke_action(context, step.action)
+ 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_operations(expected, actual, context, label)
+ local used = {}
+ 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
+ for index, candidate in ipairs(actual) do
+ if not used[index]
+ and pcall(deep_same, wanted, candidate, label, true)
+ then
+ found = candidate
+ used[index] = true
+ 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_operations(
+ 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
+))