diff options
| -rw-r--r-- | lua/clever_f/capabilities.lua | 1 | ||||
| -rw-r--r-- | lua/clever_f/policy.lua | 494 | ||||
| -rw-r--r-- | lua/clever_f/testing/memory_host.lua | 8 | ||||
| -rw-r--r-- | tests/run.lua | 273 |
4 files changed, 775 insertions, 1 deletions
diff --git a/lua/clever_f/capabilities.lua b/lua/clever_f/capabilities.lua index 77ce130..23da397 100644 --- a/lua/clever_f/capabilities.lua +++ b/lua/clever_f/capabilities.lua @@ -15,6 +15,7 @@ M.read_methods = { M.effect_methods = { movement = { "apply_cursor", "apply_selection", "set_operator_inclusive" }, + configuration = { "write_configuration" }, input = { "read_input" }, folds = { "open_fold" }, prompt = { "show_prompt" }, diff --git a/lua/clever_f/policy.lua b/lua/clever_f/policy.lua new file mode 100644 index 0000000..b56487f --- /dev/null +++ b/lua/clever_f/policy.lua @@ -0,0 +1,494 @@ +local domain = require("clever_f.domain") + +local M = {} +local PolicyService = {} +PolicyService.__index = PolicyService +M.PolicyService = PolicyService + +M.ValueType = { + BOOLEAN = "boolean", + STRING = "string", + STRING_LIST = "string_list", + OPTIONAL_GROUP_NAME = "optional_group_name", + NONNEGATIVE_INTEGER = "nonnegative_integer", + PRESENCE = "presence", +} + +M.Sampling = { + LIVE = "live", + ACTIVATION = "activation", + LINK_EVALUATION = "link_evaluation", +} + +M.DEFAULT_MAP_SUPPRESSION_SENTINEL = "suppress_default_mappings" + +local NO_VALUE = {} +local SCHEMA = { + search_current_line_only = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + ignore_case = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + smart_case = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + use_migemo = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + fix_key_direction = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + show_prompt = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + chars_match_any_signs = { + value_type = M.ValueType.STRING, + default = "", + sampling = M.Sampling.LIVE, + }, + mark_cursor = { + value_type = M.ValueType.BOOLEAN, + default = true, + sampling = M.Sampling.LIVE, + }, + mark_cursor_color = { + value_type = M.ValueType.OPTIONAL_GROUP_NAME, + default = NO_VALUE, + default_target = "Cursor", + feature_setting = "mark_cursor", + highlight_group = "CleverFCursor", + sampling = M.Sampling.LINK_EVALUATION, + }, + hide_cursor_on_cmdline = { + value_type = M.ValueType.BOOLEAN, + default = true, + sampling = M.Sampling.LIVE, + }, + repeat_timeout_ms = { + value_type = M.ValueType.NONNEGATIVE_INTEGER, + default = 0, + sampling = M.Sampling.LIVE, + }, + mark_char = { + value_type = M.ValueType.BOOLEAN, + default = true, + sampling = M.Sampling.LIVE, + }, + mark_char_color = { + value_type = M.ValueType.OPTIONAL_GROUP_NAME, + default = NO_VALUE, + default_target = "CleverFDefaultLabel", + feature_setting = "mark_char", + highlight_group = "CleverFChar", + sampling = M.Sampling.LINK_EVALUATION, + }, + highlight_timeout_ms = { + value_type = M.ValueType.NONNEGATIVE_INTEGER, + default = 0, + sampling = M.Sampling.LIVE, + }, + repeat_last_char_inputs = { + value_type = M.ValueType.STRING_LIST, + default = { "\r" }, + sampling = M.Sampling.LIVE, + }, + mark_direct = { + value_type = M.ValueType.BOOLEAN, + default = false, + sampling = M.Sampling.LIVE, + }, + mark_direct_color = { + value_type = M.ValueType.OPTIONAL_GROUP_NAME, + default = NO_VALUE, + default_target = "CleverFDefaultLabel", + feature_setting = "mark_direct", + highlight_group = "CleverFDirect", + sampling = M.Sampling.LINK_EVALUATION, + }, + clean_labels_eagerly = { + value_type = M.ValueType.BOOLEAN, + default = true, + sampling = M.Sampling.ACTIVATION, + }, + [M.DEFAULT_MAP_SUPPRESSION_SENTINEL] = { + value_type = M.ValueType.PRESENCE, + default = false, + sampling = M.Sampling.ACTIVATION, + }, +} + +local COLOR_SETTINGS = { + "mark_cursor_color", + "mark_char_color", + "mark_direct_color", +} + +local function fail(message, level) + error(message, (level or 1) + 1) +end + +local function is_integer(value) + return type(value) == "number" + and value > -math.huge + and value < math.huge + and value == math.floor(value) +end + +local function copy_list(values) + local result = {} + for index = 1, #values do + result[index] = values[index] + end + return result +end + +local function copy_table(value) + local result = {} + for key, item in pairs(value) do + if type(item) == "table" and domain.type_of(item) == nil then + result[key] = copy_table(item) + else + result[key] = item + end + end + return result +end + +local function schema_entry(name) + local entry = SCHEMA[name] + if entry == nil then + fail("unknown policy setting '" .. tostring(name) .. "'", 2) + end + return entry +end + +local function default_value(entry) + if entry.default == NO_VALUE then + return nil + end + if type(entry.default) == "table" then + return copy_table(entry.default) + end + return entry.default +end + +local function validate_boolean(value, name) + if type(value) ~= "boolean" then + fail("policy setting '" .. name .. "' must be a Boolean", 3) + end + return value +end + +local function validate_string(value, name) + if type(value) ~= "string" then + fail("policy setting '" .. name .. "' must be a string", 3) + end + return value +end + +local function validate_string_list(value, name) + if type(value) ~= "table" then + fail("policy setting '" .. name .. "' must be a list of strings", 3) + end + + local length = #value + local item_count = 0 + for key, item in pairs(value) do + if not is_integer(key) or key < 1 or key > length then + fail("policy setting '" .. name .. "' must be a list of strings", 3) + end + if type(item) ~= "string" then + fail("policy setting '" .. name .. "' must be a list of strings", 3) + end + item_count = item_count + 1 + end + if item_count ~= length then + fail("policy setting '" .. name .. "' must be a list of strings", 3) + end + return copy_list(value) +end + +local function validate_optional_group_name(value, name) + if value ~= nil and (type(value) ~= "string" or value == "") then + fail("policy setting '" .. name .. "' must be an optional group name", 3) + end + return value +end + +local function validate_nonnegative_integer(value, name) + if not is_integer(value) or value < 0 then + fail("policy setting '" .. name .. "' must be a nonnegative integer", 3) + end + return value +end + +local VALIDATORS = { + [M.ValueType.BOOLEAN] = validate_boolean, + [M.ValueType.STRING] = validate_string, + [M.ValueType.STRING_LIST] = validate_string_list, + [M.ValueType.OPTIONAL_GROUP_NAME] = validate_optional_group_name, + [M.ValueType.NONNEGATIVE_INTEGER] = validate_nonnegative_integer, +} + +local function require_provider(provider) + if type(provider) ~= "table" then + fail("policy configuration provider must be a table", 2) + end + local required = { + "configuration_present", + "read_configuration", + "write_configuration", + } + for _, method_name in ipairs(required) do + if type(provider[method_name]) ~= "function" then + fail("policy configuration provider is missing " .. method_name, 2) + end + end + return provider +end + +function M.setting_names() + local names = {} + for name in pairs(SCHEMA) do + names[#names + 1] = name + end + table.sort(names) + return names +end + +function M.schema() + local result = {} + for name, entry in pairs(SCHEMA) do + local public_entry = { + value_type = entry.value_type, + sampling = entry.sampling, + has_default = true, + } + local value = default_value(entry) + if value ~= nil then + public_entry.default = value + end + if entry.default_target ~= nil then + public_entry.default_target = entry.default_target + public_entry.feature_setting = entry.feature_setting + public_entry.highlight_group = entry.highlight_group + end + result[name] = public_entry + end + return result +end + +function M.default(name) + return default_value(schema_entry(name)) +end + +function M.defaults() + local result = {} + for name, entry in pairs(SCHEMA) do + local value = default_value(entry) + if value ~= nil then + result[name] = value + end + end + return result +end + +function PolicyService.new(provider) + return setmetatable({ + _provider = require_provider(provider), + _activation = nil, + }, PolicyService) +end + +function M.new(provider) + return PolicyService.new(provider) +end + +setmetatable(M, { + __call = function(_, provider) + return PolicyService.new(provider) + end, +}) + +function PolicyService:get(name) + local entry = schema_entry(name) + if entry.value_type == M.ValueType.PRESENCE then + return self._provider:configuration_present(name) + end + + local value + if self._provider:configuration_present(name) then + value = self._provider:read_configuration(name) + else + value = default_value(entry) + end + return VALIDATORS[entry.value_type](value, name) +end + +function PolicyService:_get_typed(name, expected_type) + local entry = schema_entry(name) + if entry.value_type ~= expected_type then + fail( + "policy setting '" .. name .. "' does not have type " .. expected_type, + 2 + ) + end + return self:get(name) +end + +function PolicyService:get_boolean(name) + return self:_get_typed(name, M.ValueType.BOOLEAN) +end + +function PolicyService:get_string(name) + return self:_get_typed(name, M.ValueType.STRING) +end + +function PolicyService:get_string_list(name) + return self:_get_typed(name, M.ValueType.STRING_LIST) +end + +function PolicyService:get_optional_group_name(name) + return self:_get_typed(name, M.ValueType.OPTIONAL_GROUP_NAME) +end + +function PolicyService:get_nonnegative_integer(name) + return self:_get_typed(name, M.ValueType.NONNEGATIVE_INTEGER) +end + +function PolicyService:get_presence(name) + return self:_get_typed(name, M.ValueType.PRESENCE) +end + +function PolicyService:default_maps_suppressed() + return self:get_presence(M.DEFAULT_MAP_SUPPRESSION_SENTINEL) +end + +function PolicyService:capture_activation() + if self._activation == nil then + self._activation = { + install_default_mappings = not self:default_maps_suppressed(), + clean_labels_eagerly = self:get_boolean("clean_labels_eagerly"), + } + end + return copy_table(self._activation) +end + +function PolicyService:evaluate_highlight_links() + local result = {} + for _, color_setting in ipairs(COLOR_SETTINGS) do + local entry = SCHEMA[color_setting] + local configured_target = self:get_optional_group_name(color_setting) + result[entry.highlight_group] = { + enabled = self:get_boolean(entry.feature_setting), + feature_setting = entry.feature_setting, + color_setting = color_setting, + configured_target = configured_target, + target = configured_target or entry.default_target, + } + end + return result +end + +local function require_target(target) + if not domain.TargetValue.is(target) then + fail("case policy target must be a TargetValue", 2) + end + return target +end + +function M.resolve_case_mode(target, ignore_case, smart_case) + target = require_target(target) + validate_boolean(ignore_case, "ignore_case") + validate_boolean(smart_case, "smart_case") + + if ignore_case or (smart_case and target.value:match("^[a-z]$") ~= nil) then + return domain.CaseMode.INSENSITIVE + end + return domain.CaseMode.SENSITIVE +end + +function PolicyService:case_mode(target) + return M.resolve_case_mode( + target, + self:get_boolean("ignore_case"), + self:get_boolean("smart_case") + ) +end + +function PolicyService:sample_search() + local current_line_only = self:get_boolean("search_current_line_only") + return { + search_current_line_only = current_line_only, + search_scope = current_line_only + and domain.SearchScope.CURRENT_LINE + or domain.SearchScope.BUFFER, + } +end + +function PolicyService:sample_match(target) + local ignore_case = self:get_boolean("ignore_case") + local smart_case = self:get_boolean("smart_case") + return { + ignore_case = ignore_case, + smart_case = smart_case, + use_migemo = self:get_boolean("use_migemo"), + chars_match_any_signs = self:get_string("chars_match_any_signs"), + case_mode = M.resolve_case_mode(target, ignore_case, smart_case), + } +end + +function PolicyService:sample_direction() + return { + fix_key_direction = self:get_boolean("fix_key_direction"), + } +end + +function PolicyService:sample_acquisition() + return { + show_prompt = self:get_boolean("show_prompt"), + mark_cursor = self:get_boolean("mark_cursor"), + hide_cursor_on_cmdline = self:get_boolean("hide_cursor_on_cmdline"), + mark_direct = self:get_boolean("mark_direct"), + } +end + +function PolicyService:sample_markers() + return { + mark_cursor = self:get_boolean("mark_cursor"), + mark_char = self:get_boolean("mark_char"), + mark_direct = self:get_boolean("mark_direct"), + } +end + +function PolicyService:sample_timeouts() + return { + repeat_timeout_ms = self:get_nonnegative_integer("repeat_timeout_ms"), + highlight_timeout_ms = self:get_nonnegative_integer("highlight_timeout_ms"), + } +end + +function PolicyService:sample_previous_input() + return { + repeat_last_char_inputs = self:get_string_list("repeat_last_char_inputs"), + } +end + +function PolicyService:disable_migemo_for_unsupported_encoding() + self._provider:write_configuration("use_migemo", false) +end + +return M diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua index 726d244..57af1f1 100644 --- a/lua/clever_f/testing/memory_host.lua +++ b/lua/clever_f/testing/memory_host.lua @@ -268,6 +268,14 @@ function MemoryHost:read_configuration(name) return value end +function MemoryHost:write_configuration(name, value) + if type(name) ~= "string" or name == "" then + error("configuration name must be a nonempty string", 2) + end + self._configuration[name] = copy(value) + self:_record("write_configuration", { name = name, value = value }) +end + function MemoryHost:read_encoding() self:_record("read_encoding", { encoding = self._encoding }) return self._encoding diff --git a/tests/run.lua b/tests/run.lua index 6a42d00..f984123 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -8,6 +8,7 @@ package.path = table.concat({ local domain = require("clever_f.domain") local capabilities = require("clever_f.capabilities") +local policy = require("clever_f.policy") local MemoryHost = require("clever_f.testing.memory_host") local tests = {} @@ -561,6 +562,276 @@ test("MemoryHost manages timers, events, mappings, and dot repeat", function() same(2, count.value) end) +test("Policy schema contains every default and color target", function() + local defaults = policy.defaults() + local false_settings = { + "search_current_line_only", + "ignore_case", + "smart_case", + "use_migemo", + "fix_key_direction", + "show_prompt", + "mark_direct", + } + local true_settings = { + "mark_cursor", + "hide_cursor_on_cmdline", + "mark_char", + "clean_labels_eagerly", + } + + for _, name in ipairs(false_settings) do + same(false, defaults[name], name) + end + for _, name in ipairs(true_settings) do + same(true, defaults[name], name) + end + same("", defaults.chars_match_any_signs) + same(0, defaults.repeat_timeout_ms) + same(0, defaults.highlight_timeout_ms) + list_same({ "\r" }, defaults.repeat_last_char_inputs) + same(nil, policy.default("mark_cursor_color")) + same(nil, policy.default("mark_char_color")) + same(nil, policy.default("mark_direct_color")) + + local schema = policy.schema() + same("Cursor", schema.mark_cursor_color.default_target) + same("CleverFDefaultLabel", schema.mark_char_color.default_target) + same("CleverFDefaultLabel", schema.mark_direct_color.default_target) + same(policy.ValueType.OPTIONAL_GROUP_NAME, schema.mark_cursor_color.value_type) + same(policy.ValueType.PRESENCE, + schema[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL].value_type) + + defaults.repeat_last_char_inputs[1] = "changed" + schema.repeat_last_char_inputs.default[1] = "changed" + list_same({ "\r" }, policy.default("repeat_last_char_inputs")) +end) + +test("Policy typed accessors validate semantic values", function() + local host = MemoryHost.new({ + configuration = { + search_current_line_only = true, + chars_match_any_signs = ";:", + repeat_last_char_inputs = { "x", "" }, + mark_cursor_color = "IncSearch", + repeat_timeout_ms = 25, + }, + }) + local service = policy.new(host) + + truthy(service:get_boolean("search_current_line_only")) + same(";:", service:get_string("chars_match_any_signs")) + list_same({ "x", "" }, service:get_string_list("repeat_last_char_inputs")) + same("IncSearch", service:get_optional_group_name("mark_cursor_color")) + same(25, service:get_nonnegative_integer("repeat_timeout_ms")) + same(nil, service:get_optional_group_name("mark_char_color")) + + local inputs = service:get_string_list("repeat_last_char_inputs") + inputs[1] = "changed" + same("x", service:get_string_list("repeat_last_char_inputs")[1]) + + host:set_configuration("ignore_case", 1) + fails(function() + service:get_boolean("ignore_case") + end, "must be a Boolean") + host:set_configuration("chars_match_any_signs", {}) + fails(function() + service:get_string("chars_match_any_signs") + end, "must be a string") + host:set_configuration("repeat_last_char_inputs", { "x", 2 }) + fails(function() + service:get_string_list("repeat_last_char_inputs") + end, "list of strings") + host:set_configuration("mark_cursor_color", "") + fails(function() + service:get_optional_group_name("mark_cursor_color") + end, "optional group name") + host:set_configuration("repeat_timeout_ms", -1) + fails(function() + service:get_nonnegative_integer("repeat_timeout_ms") + end, "nonnegative integer") + host:set_configuration("repeat_timeout_ms", 1.5) + fails(function() + service:get_nonnegative_integer("repeat_timeout_ms") + end, "nonnegative integer") + fails(function() + service:get_string("ignore_case") + end, "does not have type string") + fails(function() + service:get("unknown") + end, "unknown policy setting") +end) + +test("Default-map suppression is presence-based and activation values are retained", function() + local absent_host = MemoryHost.new() + local absent_policy = policy.new(absent_host) + falsy(absent_policy:default_maps_suppressed()) + truthy(absent_policy:capture_activation().install_default_mappings) + + for _, sentinel_value in ipairs({ false, 0 }) do + local configuration = { + clean_labels_eagerly = false, + } + configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = sentinel_value + local host = MemoryHost.new({ configuration = configuration }) + local service = policy.new(host) + + host:clear_operations() + truthy(service:default_maps_suppressed()) + local operations = host:operations() + same(1, #operations) + same("configuration_present", operations[1].operation) + + local activation = service:capture_activation() + falsy(activation.install_default_mappings) + falsy(activation.clean_labels_eagerly) + + host:unset_configuration(policy.DEFAULT_MAP_SUPPRESSION_SENTINEL) + host:set_configuration("clean_labels_eagerly", true) + local retained = service:capture_activation() + falsy(retained.install_default_mappings) + falsy(retained.clean_labels_eagerly) + + local next_activation = policy.new(host):capture_activation() + truthy(next_activation.install_default_mappings) + truthy(next_activation.clean_labels_eagerly) + end +end) + +test("Runtime policy samples read current behavior values", function() + local host = MemoryHost.new() + local service = policy.new(host) + local target = domain.TargetValue.character("a", 97) + + same(domain.SearchScope.BUFFER, service:sample_search().search_scope) + same(domain.CaseMode.SENSITIVE, service:sample_match(target).case_mode) + falsy(service:sample_direction().fix_key_direction) + falsy(service:sample_acquisition().show_prompt) + truthy(service:sample_markers().mark_char) + same(0, service:sample_timeouts().repeat_timeout_ms) + list_same({ "\r" }, service:sample_previous_input().repeat_last_char_inputs) + + host:set_configuration("search_current_line_only", true) + host:set_configuration("ignore_case", true) + host:set_configuration("smart_case", true) + host:set_configuration("use_migemo", true) + host:set_configuration("chars_match_any_signs", ";") + host:set_configuration("fix_key_direction", true) + host:set_configuration("show_prompt", true) + host:set_configuration("mark_cursor", false) + host:set_configuration("hide_cursor_on_cmdline", false) + host:set_configuration("mark_char", false) + host:set_configuration("mark_direct", true) + host:set_configuration("repeat_timeout_ms", 75) + host:set_configuration("highlight_timeout_ms", 125) + host:set_configuration("repeat_last_char_inputs", { "x", "yz" }) + + local search = service:sample_search() + truthy(search.search_current_line_only) + same(domain.SearchScope.CURRENT_LINE, search.search_scope) + + local match = service:sample_match(target) + truthy(match.ignore_case) + truthy(match.smart_case) + truthy(match.use_migemo) + same(";", match.chars_match_any_signs) + same(domain.CaseMode.INSENSITIVE, match.case_mode) + truthy(service:sample_direction().fix_key_direction) + + local acquisition = service:sample_acquisition() + truthy(acquisition.show_prompt) + falsy(acquisition.mark_cursor) + falsy(acquisition.hide_cursor_on_cmdline) + truthy(acquisition.mark_direct) + + local markers = service:sample_markers() + falsy(markers.mark_cursor) + falsy(markers.mark_char) + truthy(markers.mark_direct) + + local timeouts = service:sample_timeouts() + same(75, timeouts.repeat_timeout_ms) + same(125, timeouts.highlight_timeout_ms) + list_same({ "x", "yz" }, service:sample_previous_input().repeat_last_char_inputs) +end) + +test("Highlight policy is reevaluated with live feature and color values", function() + local host = MemoryHost.new() + local service = policy.new(host) + service:capture_activation() + + local initial = service:evaluate_highlight_links() + truthy(initial.CleverFCursor.enabled) + same(nil, initial.CleverFCursor.configured_target) + same("Cursor", initial.CleverFCursor.target) + truthy(initial.CleverFChar.enabled) + same("CleverFDefaultLabel", initial.CleverFChar.target) + falsy(initial.CleverFDirect.enabled) + same("CleverFDefaultLabel", initial.CleverFDirect.target) + + host:set_configuration("mark_cursor", false) + host:set_configuration("mark_cursor_color", "Search") + host:set_configuration("mark_char", false) + host:set_configuration("mark_char_color", "ErrorMsg") + host:set_configuration("mark_direct", true) + host:set_configuration("mark_direct_color", "IncSearch") + + local refreshed = service:evaluate_highlight_links() + falsy(refreshed.CleverFCursor.enabled) + same("Search", refreshed.CleverFCursor.target) + falsy(refreshed.CleverFChar.enabled) + same("ErrorMsg", refreshed.CleverFChar.target) + truthy(refreshed.CleverFDirect.enabled) + same("IncSearch", refreshed.CleverFDirect.target) +end) + +test("Case policy resolves an explicit mode for each target", function() + local host = MemoryHost.new() + local service = policy.new(host) + local lower = domain.TargetValue.character("a", 97) + local upper = domain.TargetValue.character("A", 65) + local multibyte = domain.TargetValue.character("\227\129\130", 0x3042) + + same(domain.CaseMode.SENSITIVE, service:case_mode(lower)) + host:set_configuration("smart_case", true) + same(domain.CaseMode.INSENSITIVE, service:case_mode(lower)) + same(domain.CaseMode.SENSITIVE, service:case_mode(upper)) + same(domain.CaseMode.SENSITIVE, service:case_mode(multibyte)) + host:set_configuration("ignore_case", true) + same(domain.CaseMode.INSENSITIVE, service:case_mode(upper)) + + local match = service:sample_match(upper) + local plan = domain.TargetPlan.new({ + target = upper, + kind = domain.TargetPlanKind.LITERAL, + case_mode = match.case_mode, + matcher = function() + return true + end, + }) + same(domain.CaseMode.INSENSITIVE, plan.case_mode) +end) + +test("Unsupported Migemo policy mutation updates live configuration", function() + local host = MemoryHost.new({ + configuration = { use_migemo = true }, + }) + local service = policy.new(host) + truthy(service:get_boolean("use_migemo")) + + host:clear_operations() + service:disable_migemo_for_unsupported_encoding() + local operations = host:operations() + same(1, #operations) + same("write_configuration", operations[1].operation) + same("use_migemo", operations[1].name) + same(false, operations[1].value) + falsy(service:get_boolean("use_migemo")) + + host:set_configuration("use_migemo", true) + truthy(service:get_boolean("use_migemo")) +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then @@ -570,4 +841,4 @@ for _, item in ipairs(tests) do passed = passed + 1 end -io.stdout:write(string.format("Phase 2: %d tests passed\n", passed)) +io.stdout:write(string.format("Phase 3: %d tests passed\n", passed)) |
