diff --git a/src/am.lua b/src/am.lua index 9df5cb9..822bd3f 100644 --- a/src/am.lua +++ b/src/am.lua @@ -63,7 +63,7 @@ end ---@return any function am.execute(cmd, args) local interface, args = get_interface(cmd, args) - ami_assert(type(interface) == "table", "No valid command provided!", EXIT_CLI_CMD_UNKNOWN) + ami_assert(type(interface) == "table", "no valid command provided", EXIT_CLI_CMD_UNKNOWN) return cli.process(interface, args) end @@ -116,7 +116,10 @@ function am.__parse_base_args(args, options) if type(options) ~= "table" then options = { stop_on_non_option = true } end - return am.parse_args(interface.new("base"), args, options) + local ami, err = interface.new("base") + assert(ami, "failed to create base interface: " .. tostring(err), EXIT_INVALID_INTERFACE) + + return am.parse_args(ami, args, options) end ---Configures ami cache location @@ -138,14 +141,16 @@ function am.configure_cache(cache) am.options.CACHE_DIR = cache_path --fallback to local dir in case we have no access to global one - if not fs.safe_write_file(path.combine(tostring(am.options.CACHE_DIR), ".ami-test-access"), "") then + local ok, err = fs.write_file(path.combine(tostring(am.options.CACHE_DIR), ".ami-test-access"), "") + if not ok then local log = custom_cache_path and log_error or log_debug - log("Access to '" .. am.options.CACHE_DIR .. "' denied! Using local '.ami-cache' directory.") + log("access to '" .. am.options.CACHE_DIR .. "' denied (error: " .. tostring(err) ..") - using local '.ami-cache' directory") am.options.CACHE_DIR = ".ami-cache" - if not fs.safe_write_file(path.combine(tostring(am.options.CACHE_DIR), ".ami-test-access"), "") then + local ok, err = fs.write_file(path.combine(tostring(am.options.CACHE_DIR), ".ami-test-access"), "") + if not ok then am.options.CACHE_DIR = false - log_debug("Access to '" .. am.options.CACHE_DIR .. "' denied! Cache disabled.") + log_debug("access to '" .. am.options.CACHE_DIR .. "' denied - ".. tostring(err) .." - cache disabled.") end end end @@ -171,7 +176,11 @@ end ---Reloads application interface and returns true if it is application specific. (False if it is from templates) ---@param shallow boolean? function am.__reload_interface(shallow) - am.__has_app_specific_interface, am.__interface = interface.load(am.options.BASE_INTERFACE, shallow) + local ami, err, is_app_specific = interface.load(am.options.BASE_INTERFACE, shallow) + ami_assert(ami, tostring(err), EXIT_INVALID_AMI_INTERFACE) + + am.__interface = ami + am.__has_app_specific_interface = is_app_specific end ---Finds app entrypoint (ami.lua/ami.json/ami.hjson) @@ -203,7 +212,11 @@ end ---@diagnostic disable-next-line: undefined-doc-param ---@param options ExecNativeActionOptions? ---@return any -am.execute_extension = exec.native_action +function am.execute_extension(...) + local result, err, executed = exec.native_action(...) + ami_assert(executed, err or "unknown", EXIT_CLI_ACTION_EXECUTION_ERROR) + return result +end ---#DES am.execute_external() --- diff --git a/src/ami.lua b/src/ami.lua index cc11440..9723f1d 100644 --- a/src/ami.lua +++ b/src/ami.lua @@ -23,10 +23,10 @@ local parsed_options, _, remaining_args = am.__parse_base_args({ ... }) SOURCES = nil if parsed_options["local-sources"] then - local ok, local_sources_raw = fs.safe_read_file(tostring(parsed_options["local-sources"])) - ami_assert(ok, "failed to read local sources file " .. parsed_options["local-sources"], EXIT_INVALID_SOURCES_FILE) - local ok, local_sources = hjson.safe_parse(local_sources_raw) - ami_assert(ok, "failed to parse local sources file " .. parsed_options["local-sources"], EXIT_INVALID_SOURCES_FILE) + local local_sources_raw, err = fs.read_file(tostring(parsed_options["local-sources"])) + ami_assert(local_sources_raw, "failed to read local sources file '" .. parsed_options["local-sources"] .. "': " .. tostring(err), EXIT_INVALID_SOURCES_FILE) + local local_sources, err = hjson.parse(local_sources_raw) + ami_assert(local_sources, "failed to parse local sources file '" .. parsed_options["local-sources"] .. "': " .. tostring(err), EXIT_INVALID_SOURCES_FILE) SOURCES = local_sources end @@ -110,8 +110,8 @@ end if parsed_options["dry-run"] then if parsed_options["dry-run-config"] then - local ok, app_config = hjson.safe_parse(parsed_options["dry-run-config"]) - if ok then -- model is valid json + local app_config, _ = hjson.parse(parsed_options["dry-run-config"] --[[@as string]]) + if app_config then -- model is valid json am.app.__set(app_config) else -- model is not valid json fallback to path am.app.load_configuration(tostring(parsed_options["dry-run-config"])) diff --git a/src/ami/app.lua b/src/ami/app.lua index c09baa5..88dfe65 100644 --- a/src/ami/app.lua +++ b/src/ami/app.lua @@ -84,61 +84,65 @@ if TEST_MODE then end ---@param candidates string[] ----@return boolean, string|table +---@return table|nil config +---@return string|nil error local function find_and_load_configuration(candidates) - local ok, config_content + local config_content, err for _, config_candidate in ipairs(candidates) do - ok, config_content = fs.safe_read_file(config_candidate) - if ok then - local ok, config = hjson.safe_parse(config_content) - return ok, config + config_content, err = fs.read_file(config_candidate) + if config_content then + local config, err = hjson.parse(config_content) + -- we take first existing candidate even if it is not valid + -- to maintain deterministic behavior + return config, err end end - return false, config_content + return nil, err end ---loads configuration and env configuration if available ---@param path string? ----@return string +---@return string? +---@return string? error local function load_configuration_content(path) local predefined_path = path or am.options.APP_CONFIGURATION_PATH if type(predefined_path) == "string" then - local ok, config_content = fs.safe_read_file(predefined_path) - ami_assert(ok, "Failed to load app.h/json - " .. tostring(config_content), EXIT_INVALID_CONFIGURATION) + local config_content, err = fs.read_file(predefined_path) + ami_assert(config_content, "failed to load app.h/json - " .. tostring(err), EXIT_INVALID_CONFIGURATION) return config_content end local env_ok, env_config - local default_ok, default_config = find_and_load_configuration(am.options.APP_CONFIGURATION_CANDIDATES) + local default_config, _ = find_and_load_configuration(am.options.APP_CONFIGURATION_CANDIDATES) if am.options.ENVIRONMENT then local candidates = table.map(am.options.APP_CONFIGURATION_ENVIRONMENT_CANDIDATES, function (v) local result = string.interpolate(v, { environment = am.options.ENVIRONMENT }) return result end) - env_ok, env_config = find_and_load_configuration(candidates) - if not env_ok then log_warn("Failed to load environment configuration - " .. tostring(env_config)) end + env_config, _ = find_and_load_configuration(candidates) + if not env_ok then log_warn("failed to load environment configuration (" .. am.options.ENVIRONMENT .. ") - " .. tostring(env_config)) end end - ami_assert(default_ok or env_ok, "Failed to load app.h/json - " .. tostring(default_config), + ami_assert(default_config or env_config, "failed to load app.h/json - " .. tostring(default_config), EXIT_INVALID_CONFIGURATION) - if not default_ok then log_warn("Failed to load default configuration - " .. tostring(default_config)) end + if not default_config then log_warn("failed to load default configuration - " .. tostring(default_config)) end return hjson.stringify_to_json( - util.merge_tables(default_ok and default_config --[[@as table]] or {}, - env_ok and env_config --[[@as table]] or {}, - true), { indent = false }) + util.merge_tables(default_config --[[@as table]] or {}, env_config --[[@as table]] or {}, true), { indent = false }) end local function load_configuration(path) - local config_content = load_configuration_content(path) - local ok, app = hjson.safe_parse(config_content) - ami_assert(ok, "Failed to parse app.h/json - " .. tostring(app), EXIT_INVALID_CONFIGURATION) + local config_content, err = load_configuration_content(path) + assert(config_content, "failed to load app.h/json - " .. tostring(err), EXIT_INVALID_CONFIGURATION) + local app, err = hjson.parse(config_content) + ami_assert(app, "failed to parse app.h/json - " .. tostring(err), EXIT_INVALID_CONFIGURATION) __set(app) local variables = am.app.get("variables", {}) local options = am.app.get("options", {}) variables = util.merge_tables(variables, { ROOT_DIR = os.EOS and os.cwd() or "." }, true) config_content = am.util.replace_variables(config_content, variables, options) - __set(hjson.parse(config_content)) + local app, _ = hjson.parse(config_content) + __set(app) end @@ -177,7 +181,7 @@ end ---#DES am.app.get_config --- ----Gets valua from path in app.configuration or falls back to default if value in path is nil +---Gets value from path in app.configuration or falls back to default if value in path is nil ---@deprecated ---@param path string|string[] ---@param default any? @@ -199,7 +203,7 @@ function am.app.load_model() local ok, err = pcall(dofile, path) if not ok then is_model_loaded = false - ami_error("Failed to load app model - " .. err, EXIT_APP_INVALID_MODEL) + ami_error("failed to load app model - " .. err, EXIT_APP_INVALID_MODEL) end end @@ -273,14 +277,15 @@ end ---Prepares app environment - extracts layers and builds model. function am.app.prepare() log_info"Preparing the application..." - local file_list, model_info, version_tree, tmp_pkgs = ami_pkg.prepare_pkg(am.app.get"type") + local preparation_result, err = ami_pkg.prepare_pkg(am.app.get"type") + ami_assert(preparation_result, "failed to prepare app - " .. tostring(err), EXIT_APP_INTERNAL_ERROR) - ami_pkg.unpack_layers(file_list) - ami_pkg.generate_model(model_info) - for _, v in ipairs(tmp_pkgs) do - fs.safe_remove(v) - end - fs.write_file(".version-tree.json", hjson.stringify_to_json(version_tree)) + local ok, err = ami_pkg.unpack_layers(preparation_result.files) + ami_assert(ok, "failed to unpack layers: " .. tostring(err), EXIT_PKG_LAYER_EXTRACT_ERROR) + local ok, err = ami_pkg.generate_model(preparation_result.model) + ami_assert(ok, "failed to generate model: " .. tostring(err), EXIT_PKG_MODEL_GENERATION_ERROR) + for _, v in ipairs(preparation_result.tmp_archive_paths) do fs.remove(v) end + fs.write_file(".version-tree.json", hjson.stringify_to_json(preparation_result.version_tree)) is_model_loaded = false -- force mode load on next access am.app.load_configuration() @@ -289,7 +294,10 @@ end ---#DES am.app.render --- ---Renders app templates. -am.app.render = ami_tpl.render_templates +function am.app.render() + local ok, err = ami_tpl.render_templates() + ami_assert(ok, "app.render: " .. tostring(err), EXIT_TPL_RENDERING_ERROR) +end ---#DES am.app.__are_templates_generated --- @@ -303,22 +311,27 @@ end --- ---Returns true if there is update available for any of related packages ---@return boolean +---@return table? updates function am.app.is_update_available() - local ok, version_tree_raw = fs.safe_read_file".version-tree.json" - if ok then - local ok, version_tree = hjson.safe_parse(version_tree_raw) - if ok then + local version_tree_raw, _ = fs.read_file".version-tree.json" + if version_tree_raw then + local version_tree, _ = hjson.parse(version_tree_raw) + if version_tree then log_trace"Using .version-tree.json for update availability check." - return ami_pkg.is_pkg_update_available(version_tree) + local update_available, available_versions_or_error = ami_pkg.is_pkg_update_available(version_tree) + ami_assert(update_available ~= nil, "failed to check update availability - " .. tostring(available_versions_or_error), EXIT_APP_UPDATE_ERROR) + return update_available, available_versions_or_error end end log_warn"Version tree not found. Running update check against specs..." - local ok, specs_raw = fs.safe_read_file"specs.json" - ami_assert(ok, "Failed to load app specs.json", EXIT_APP_UPDATE_ERROR) - local ok, specs = hjson.parse(specs_raw) - ami_assert(ok, "Failed to parse app specs.json", EXIT_APP_UPDATE_ERROR) - return ami_pkg.is_pkg_update_available(am.app.get"type", specs and specs.version) + local specs_raw, err = fs.read_file"specs.json" + ami_assert(specs_raw, "failed to load app specs.json - " .. tostring(err), EXIT_APP_UPDATE_ERROR) + local specs, err = hjson.parse(specs_raw) + ami_assert(specs, "failed to parse app specs.json - " .. tostring(err), EXIT_APP_UPDATE_ERROR) + local update_available, available_versions_or_error = ami_pkg.is_pkg_update_available(am.app.get"type", specs and specs.version) + ami_assert(update_available ~= nil, "failed to check update availability - " .. tostring(available_versions_or_error), EXIT_APP_UPDATE_ERROR) + return update_available, available_versions_or_error end ---@class PackageVersion @@ -333,10 +346,10 @@ end ---Returns app version ---@return PackageVersion?, string? function am.app.get_version_tree() - local ok, version_tree_raw = fs.safe_read_file".version-tree.json" - if ok then - local ok, version_tree = hjson.safe_parse(version_tree_raw) - if ok then + local version_tree_raw, _ = fs.read_file".version-tree.json" + if version_tree_raw then + local version_tree, _ = hjson.parse(version_tree_raw) + if version_tree then return version_tree end return nil, "invalid version tree" @@ -393,7 +406,7 @@ function am.app.remove_data(keep) end, protected_files) end - local ok, err = fs.safe_remove("data", { + local ok, err = fs.remove("data", { recurse = true, content_only = true, keep = function (p, _) @@ -431,7 +444,7 @@ function am.app.remove(keep) end, protected_files) end - local ok, err = fs.safe_remove(".", { + local ok, err = fs.remove(".", { recurse = true, content_only = true, keep = function (p, fp) @@ -452,10 +465,10 @@ end ---Checks whether app is installed based on app.h/json and .version-tree.json ---@return boolean function am.app.is_installed() - local ok, version_tree_json = fs.safe_read_file".version-tree.json" - if not ok then return false end - local ok, version_tree = hjson.safe_parse(version_tree_json) - if not ok then return false end + local version_tree_json, _ = fs.read_file".version-tree.json" + if not version_tree_json then return false end + local version_tree, _ = hjson.parse(version_tree_json) + if not version_tree then return false end local version = am.app.get{ "type", "version" } return am.app.get{ "type", "id" } == version_tree.id and (version == "latest" or version == version_tree.version) @@ -527,6 +540,7 @@ end --- ---Packs the app into a zip archive for easy migration ---@param options PackOptions +---@return boolean function am.app.pack(options) if type(options) ~= "table" then options = {} @@ -604,11 +618,11 @@ function am.app.unpack(options) log_info("unpacking app from archive '" .. source .. "'...") - local ok, metadata = zip.safe_extract_string(source, PACKER_METADATA_FILE) - ami_assert(ok, "failed to extract metadata from packed app - " .. tostring(metadata), EXIT_INVALID_AMI_ARCHIVE) + local metadata_raw, err = zip.extract_string(source, PACKER_METADATA_FILE) + ami_assert(metadata_raw, "failed to extract metadata from packed app - " .. tostring(err), EXIT_INVALID_AMI_ARCHIVE) - local ok, metadata = hjson.safe_parse(metadata) - ami_assert(ok, "failed to parse metadata from packed app - " .. tostring(metadata), EXIT_INVALID_AMI_ARCHIVE) + local metadata, err = hjson.parse(metadata_raw) + ami_assert(metadata, "failed to parse metadata from packed app - " .. tostring(err), EXIT_INVALID_AMI_ARCHIVE) ami_assert(metadata.VERSION == PACKER_VERSION, "packed app version mismatch - " .. tostring(metadata.VERSION), EXIT_INVALID_AMI_ARCHIVE) diff --git a/src/ami/cache.lua b/src/ami/cache.lua index 02604e6..24ee3b5 100644 --- a/src/ami/cache.lua +++ b/src/ami/cache.lua @@ -79,16 +79,16 @@ local function internal_cache_get(kind, id, options) if not file then return false, (err or "unknown error") end if type(options.sha256) == "string" and options.sha256 ~= "" then - local ok, file_hash = fs.safe_hash_file(file, { hex = true, type = "sha256" }) - if not ok or not hash.equals(file_hash, options.sha256, true) then + local file_hash, _= fs.hash_file(file, { hex = true, type = "sha256" }) + if not file_hash or not hash.equals(file_hash, options.sha256, true) then return false, "invalid hash" end file:seek("set") end if type(options.sha512) == "string" and options.sha512 ~= "" then - local ok, file_hash = fs.safe_hash_file(file, { hex = true, type = "sha512" }) - if not ok or not hash.equals(file_hash, options.sha512, true) then + local file_hash, _ = fs.hash_file(file, { hex = true, type = "sha512" }) + if not file_hash or not hash.equals(file_hash, options.sha512, true) then return false, "invalid hash" end file:seek("set") @@ -132,8 +132,7 @@ function am.cache.get_to_file(kind, id, target_path, options) local ok, result = internal_cache_get(kind, id, options) if not ok then return ok, result end - local ok, err = fs.safe_copy_file(result, target_path) - return ok, err + return fs.copy_file(result, target_path) end ---#DES am.cache.put @@ -181,12 +180,6 @@ function am.cache.rm_pkgs() fs.remove(am.cache.__get_package_cache_sub_dir("definition")(), { recurse = true, content_only = true }) end ----#DES am.cache.safe_rm_pkgs ---- ----Deletes content of package cache ----@return boolean -function am.cache.safe_rm_pkgs() return pcall(am.cache.rm_pkgs) end - ---#DES am.cache.rm_plugins --- ---Deletes content of plugin cache @@ -198,22 +191,10 @@ function am.cache.rm_plugins() fs.remove(am.cache.__get_plugin_cache_sub_dir("definition")(), { recurse = true, content_only = true }) end ----#DES am.cache.safe_rm_plugins ---- ----Deletes content of plugin cache ----@return boolean -function am.cache.safe_rm_plugins() return pcall(am.cache.rm_plugins) end - ---#DES am.cache.erase --- ---Deletes everything from cache function am.cache.erase() am.cache.rm_pkgs() am.cache.rm_plugins() -end - ----#DES am.cache.safe_erase ---- ----Deletes everything from cache ----@return boolean -function am.cache.safe_erase() return pcall(am.cache.erase) end +end \ No newline at end of file diff --git a/src/ami/exit-codes.lua b/src/ami/exit-codes.lua index dc15995..0a526d0 100644 --- a/src/ami/exit-codes.lua +++ b/src/ami/exit-codes.lua @@ -34,7 +34,7 @@ local exit_codes = { EXIT_CLI_SCHEME_MISSING = 35, EXIT_CLI_ACTION_MISSING = 36, EXIT_CLI_ARG_VALIDATION_ERROR = 37, - EXIT_CLI_INVALID_VALUE = 38, + EXIT_CLI_ARG_PARSE_ERROR = 38, EXIT_CLI_INVALID_DEFINITION = 39, EXIT_CLI_CMD_UNKNOWN = 40, @@ -43,8 +43,7 @@ local exit_codes = { EXIT_RM_ERROR = 51, EXIT_RM_DATA_ERROR = 52, - EXIT_TPL_READ_ERROR = 70, - EXIT_TPL_WRITE_ERROR = 71, + EXIT_TPL_RENDERING_ERROR = 70, EXIT_PLUGIN_DOWNLOAD_ERROR = 80, EXIT_PLUGIN_INVALID_DEFINITION = 81, @@ -53,7 +52,6 @@ local exit_codes = { EXIT_PKG_DOWNLOAD_ERROR = 90, - EXIT_PKG_INVALID_DEFINITION = 91, EXIT_PKG_INVALID_VERSION = 92, EXIT_PKG_INVALID_TYPE = 93, EXIT_PKG_INTEGRITY_CHECK_ERROR = 94, diff --git a/src/ami/globals.lua b/src/ami/globals.lua index 464fade..b794636 100644 --- a/src/ami/globals.lua +++ b/src/ami/globals.lua @@ -15,7 +15,7 @@ require "ami.exit-codes" -hjson = util.generate_safe_functions(require "hjson") +hjson = require "hjson" ---#DES log_success --- diff --git a/src/ami/internals/cli.lua b/src/ami/internals/cli.lua index 3894bb7..a558eab 100644 --- a/src/ami/internals/cli.lua +++ b/src/ami/internals/cli.lua @@ -19,7 +19,7 @@ local exec = require"ami.internals.exec" local HELP_OPTION = { index = 100, aliases = { "h" }, - description = "Prints this help message" + description = "Prints this help message", } local ami_cli = {} @@ -27,48 +27,50 @@ local ami_cli = {} ---Parses value into required type if possible. ---@param value string ---@param _type string ----@return boolean|number|string|nil -local function _parse_value(value, _type) +---@return boolean|number|string|nil value +---@return string? error_message +---@return boolean success +local function parse_value(value, _type) if type(value) ~= "string" then - return value + return value, "invalid value type - string expected, got: " .. type(value), false end local parse_map = { - boolean = function(v) - if v == "true" or v == "TRUE" or v == "True" then - return true - elseif v == "false" or v == "FALSE" or v == "False" then - return false + boolean = function (v) + if string.lower(v) == "true" or v == "1" then + return true, nil, true + elseif string.lower(v) == "false" or v == "0" then + return false, nil, true else - ami_error("Invalid value type! Boolean expected, got: " .. value .. "!", EXIT_CLI_INVALID_VALUE) + return nil, "invalid value type - boolean expected, got: " .. value, false end end, - number = function(v) - local _n = tonumber(v) - if _n ~= nil then - return _n - else - ami_error("Invalid value type! Number expected, got: " .. value .. "!", EXIT_CLI_INVALID_VALUE) + number = function (v) + local n = tonumber(v) + if n == nil then + return nil, "invalid value type - number expected, got: " .. value, false end + return n, nil, true end, - string = function(v) - return v + string = function (v) + return v, nil, true end, - auto = function(v) - if v == "true" or v == "TRUE" or v == "True" then - return true - elseif v == "false" or v == "FALSE" or v == "False" then - return false - elseif v == "null" or v == "NULL" or v == "nil" then - return nil + auto = function (v) + local l = string.lower(v) + if l == "true" then + return true, nil, true + elseif l == "false" then + return false, nil, true + elseif l == "null" or l == "nil" then + return nil, nil, true else - local _n = tonumber(v) - if _n ~= nil then - return _n + local n = tonumber(v) + if n ~= nil then + return n, nil, true end end - return v - end + return v, nil, true + end, } local parse_fn = parse_map[_type] or parse_map.auto @@ -81,11 +83,10 @@ end local function is_array_of_tables(value) if not util.is_array(value) then return false - else - for _, v in ipairs(value) do - if type(v) ~= "table" then - return false - end + end + for _, v in ipairs(value) do + if type(v) ~= "table" then + return false end end return true @@ -105,7 +106,9 @@ end ---@param args string[]|CliArg[] ---@param scheme AmiCli ---@param options AmiParseArgsOptions ----@return table, AmiCli|nil, CliArg[] +---@return table option_list +---@return AmiCli|nil command +---@return CliArg[] remaining_args function ami_cli.parse_args(args, scheme, options) if not is_array_of_tables(args) then args = cli.parse_args(args) @@ -118,18 +121,12 @@ function ami_cli.parse_args(args, scheme, options) local cli_options = type(scheme.options) == "table" and scheme.options or {} local cli_cmds = type(scheme.commands) == "table" and scheme.commands or {} - --// TODO: remove in next version - if scheme.customHelp ~= nil and not scheme.custom_help then - scheme.custom_help = scheme.customHelp - print("Warning: customHelp is deprecated. Use custom_help instead.") - end - -- inject help option if not scheme.custom_help and not cli_options.help then cli_options.help = HELP_OPTION end - local to_map = function(t) + local to_map = function (t) local result = {} for k, v in pairs(t) do local def = util.merge_tables({ id = k }, v) @@ -155,8 +152,12 @@ function ami_cli.parse_args(args, scheme, options) local arg = args[i] if arg.type == "option" then local cli_option_def = cli_options_map[arg.id] - ami_assert(type(cli_option_def) == "table", "Unknown option - '" .. arg.arg .. "'!", EXIT_CLI_OPTION_UNKNOWN) - cli_options_list[cli_option_def.id] = _parse_value(tostring(arg.value), cli_option_def.type) + ami_assert(type(cli_option_def) == "table", "unknown option - '" .. arg.arg .. "'!", EXIT_CLI_OPTION_UNKNOWN) + local arg_value, err, success = parse_value(tostring(arg.value), cli_option_def.type) + if not success then + ami_assert(false, err or "unknown error while parsing option value", EXIT_CLI_ARG_PARSE_ERROR) + end + cli_options_list[cli_option_def.id] = arg_value elseif options.stop_on_non_option then -- we stop collecting if stop_on_non_option enabled to return everything remaining last_index = i @@ -168,7 +169,7 @@ function ami_cli.parse_args(args, scheme, options) else -- default mode - we try to identify underlying command cli_cmd = cli_cmd_map[arg.arg] - ami_assert(type(cli_cmd) == "table", "Unknown command '" .. (arg.arg or "") .. "'!", EXIT_CLI_CMD_UNKNOWN) + ami_assert(type(cli_cmd) == "table", "unknown command '" .. (arg.arg or "") .. "'!", EXIT_CLI_CMD_UNKNOWN) last_index = i + 1 break end @@ -176,7 +177,7 @@ function ami_cli.parse_args(args, scheme, options) end if not options.is_namespace or options.stop_on_non_option then - -- in case we did not precollect cli args (are precolleted if nonCommand == true and stop_on_non_option == false) + -- in case we did not pre-collect cli args (are pre-colleted if nonCommand == true and stop_on_non_option == false) cli_remaining_args = { table.unpack(args, last_index) } end return cli_options_list, cli_cmd, cli_remaining_args @@ -187,7 +188,8 @@ end ---@param optionList table ---@param command any ---@param cli AmiCli ----@return boolean, nil|string +---@return boolean is_valid +---@return string? error_message local function default_validate_args(optionList, command, cli) local options = type(cli.options) == "table" and cli.options or {} @@ -205,18 +207,6 @@ local function default_validate_args(optionList, command, cli) return true end ----Returns true if all values in table contains property hidden with value true ----@param t table ----@return boolean -local function are_all_hidden(t) - for _, v in pairs(t) do - if not v.hidden then - return false - end - end - return true -end - ---Comparison function for arg/options sorting ---@param t table ---@param a number @@ -230,157 +220,128 @@ local function compare_args(t, a, b) end end ----comment ---@param cli ExecutableAmiCli ---@param include_options_in_usage boolean ---@return string local function generate_usage(cli, include_options_in_usage) - local has_commands = cli.commands and #table.keys(cli.commands) - local has_options = cli.options and #table.keys(cli.options) - local cli_id = cli.__root_cli_id or path.file(APP_ROOT_SCRIPT or "") - local usage = "Usage: " .. cli_id .. " " - local optional_begin = "[" - local optional_end = "]" + local usage_parts = { "Usage: ", cli_id } for _, v in ipairs(cli.__command_stack or {}) do - usage = usage .. v .. " " + table.insert(usage_parts, " " .. v) end + local has_options = cli.options and next(cli.options) ~= nil if has_options and include_options_in_usage then local options = table.keys(cli.options) - local sort_function = function(a, b) + local sort_function = function (a, b) return compare_args(cli.options, a, b) end table.sort(options, sort_function) for _, k in ipairs(options) do local v = cli.options[k] - if not v.hidden then - local usage_beginning = v.required and "" or optional_begin - local usage_ending = v.required and "" or optional_end - local option_alias = v.aliases and v.aliases[1] or k - if #option_alias == 1 then - option_alias = "-" .. option_alias - else - option_alias = "--" .. option_alias - end - usage = usage .. usage_beginning .. option_alias - - if v.type == "boolean" or v.type == nil then - usage = usage .. usage_ending .. " " - else - usage = usage .. "=<" .. k .. ">" .. usage_ending .. " " - end + if v.hidden then + goto continue + end + local begin_bracket = v.required and "" or "[" + local end_bracket = v.required and "" or "]" + local alias = v.aliases and v.aliases[1] or k + alias = (#alias == 1) and ("-" .. alias) or ("--" .. alias) + local option_usage + if v.type == "boolean" or v.type == nil then + option_usage = begin_bracket .. alias .. end_bracket + else + option_usage = begin_bracket .. alias .. "=<" .. k .. ">" .. end_bracket end + table.insert(usage_parts, " " .. option_usage) + ::continue:: end end + local has_commands = cli.commands and next(cli.commands) ~= nil if has_commands then - -- // TODO: remove in next version - if cli.type == "no-command" then - cli.type = "namespace" - print("Warning: cli.type 'no-command' is deprecated. Use 'namespace' instead.") - end - if cli.type == "namespace" then - usage = usage .. "[args...]" .. " " + table.insert(usage_parts, " [args...]") elseif cli.expects_command then - usage = usage .. "" .. " " + table.insert(usage_parts, " ") else - usage = usage .. "[]" .. " " + table.insert(usage_parts, " []") end end - return usage + return table.concat(usage_parts) end local function generate_help_message(cli) - local has_commands = cli.commands and #table.keys(cli.commands) and not are_all_hidden(cli.commands) - - -- // TODO: remove in next version - if cli.customHelp ~= nil and not cli.custom_help then - cli.custom_help = cli.customHelp - print("Warning: customHelp is deprecated. Use custom_help instead.") - end + local rows = {} - if not cli.custom_help then - if type(cli.options) ~= "table" then - cli.options = {} + local function sorted_visible_keys(tbl) + local keys = {} + for k, v in pairs(tbl or {}) do + if not v.hidden then table.insert(keys, k) end end - cli.options.help = HELP_OPTION + table.sort(keys, function (a, b) return compare_args(tbl, a, b) end) + return keys end - local has_options = cli.options and #table.keys(cli.options) and not are_all_hidden(cli.options) - local rows = {} - if has_options then - table.insert(rows, { left = "Options: ", description = "" }) - local options = table.keys(cli.options) - local sort_function = function(a, b) - return compare_args(cli.options, a, b) - end - table.sort(options, sort_function) + if type(cli.options) ~= "table" then cli.options = {} end + if not cli.custom_help then cli.options.help = HELP_OPTION end - for _, k in ipairs(options) do + local options_keys = sorted_visible_keys(cli.options) + if #options_keys > 0 then + table.insert(rows, { left = "Options: ", description = "" }) + for _, k in ipairs(options_keys) do local v = cli.options[k] - local aliases = "" - if v.aliases and v.aliases[1] then + + local parts = {} + if v.aliases then for _, alias in ipairs(v.aliases) do if #alias == 1 then - alias = "-" .. alias + table.insert(parts, "-" .. alias) else - alias = "--" .. alias + table.insert(parts, "--" .. alias) end - aliases = aliases .. alias .. "|" - end - - aliases = aliases .. "--" .. k - if v.type == "boolean" or v.type == nil then - aliases = aliases .. " " - else - aliases = aliases .. "=<" .. k .. ">" .. " " end - else - aliases = "--" .. k end - if not v.hidden then - table.insert(rows, { left = aliases, description = v.description or "" }) + + table.insert(parts, "--" .. k) + -- Add value placeholder for non-boolean + local opt_usage = table.concat(parts, "|") + if v.type ~= "boolean" and v.type ~= nil then + opt_usage = opt_usage .. "=<" .. k .. ">" end + table.insert(rows, { left = opt_usage, description = v.description or "" }) end end - if has_commands then - table.insert(rows, { left = "", description = "" }) + local command_keys = sorted_visible_keys(cli.commands) + if #command_keys > 0 then + if #rows > 0 then table.insert(rows, { left = "", description = "" }) end -- blank line table.insert(rows, { left = "Commands: ", description = "" }) - local commands = table.keys(cli.commands) - local sort_function = function(a, b) - return compare_args(cli.commands, a, b) - end - table.sort(commands, sort_function) - for _, k in ipairs(commands) do + for _, k in ipairs(command_keys) do local v = cli.commands[k] - if not v.hidden then - table.insert(rows, { left = k, description = v.summary or v.description or "" }) - end + table.insert(rows, { left = k, description = v.summary or v.description or "" }) end end local left_length = 0 for _, row in ipairs(rows) do - if #row.left > left_length then - left_length = #row.left - end + if #row.left > left_length then left_length = #row.left end end - local msg = "" + local lines = {} for _, row in ipairs(rows) do if #row.left == 0 then - msg = msg .. NEW_LINE + table.insert(lines, "") else - msg = msg .. row.left .. string.rep(" ", left_length - #row.left) .. "\t\t" .. row.description .. NEW_LINE + table.insert( + lines, + string.format("%-" .. left_length .. "s\t\t%s", row.left, row.description) + ) end end - return msg + return table.concat(lines, NEW_LINE) .. NEW_LINE end ---Prints help for specified @@ -390,69 +351,50 @@ function ami_cli.print_help(ami, options) if type(options) ~= "table" then options = {} end - local title = options.title or ami.title - local description = options.description or ami.description - local _summary = options.summary or ami.summary + local title = options.title or ami.title + local description = options.description or ami.description + local summary = options.summary or ami.summary + local footer = options.footer + local print_usage = options.print_usage + local output_fmt = ami.options and ami.options.OUTPUT_FORMAT + local help_message = ami.help_message - local include_options_in_usage = nil - - -- // TODO: remove in next version - if options.includeOptionsInUsage ~= nil and options.include_options_in_usage ~= nil then - options.include_options_in_usage = options.includeOptionsInUsage - print("Warning: includeOptionsInUsage is deprecated. Use include_options_in_usage instead.") - end - if include_options_in_usage == nil and options.include_options_in_usage ~= nil then - include_options_in_usage = options.include_options_in_usage - end - - -- // TODO: remove in next version - if ami.includeOptionsInUsage ~= nil and ami.include_options_in_usage == nil then - ami.include_options_in_usage = ami.includeOptionsInUsage - print("Warning: includeOptionsInUsage is deprecated. Use include_options_in_usage instead.") - end - - if include_options_in_usage == nil and ami.include_options_in_usage ~= nil then + local include_options_in_usage = options.include_options_in_usage + if include_options_in_usage == nil then include_options_in_usage = ami.include_options_in_usage end - if include_options_in_usage == nil then include_options_in_usage = true end - local print_usage = options.printUsage if print_usage == nil then print_usage = true end - local footer = options.footer + if type(help_message) == "function" then + print(help_message(ami)) + return + elseif type(help_message) == "string" then + print(help_message) + return + end + + if output_fmt == "json" then + print(require"hjson".stringify(ami.commands, { invalid_objects_as_type = true, indent = false })) + return + end + -- collect and print help + if type(title) == "string" and #title > 0 then print(title .. NEW_LINE) end + if type(description) == "string" and #description > 0 then print(description .. NEW_LINE) end + if type(summary) == "string" and #summary > 0 then print("- " .. summary .. NEW_LINE) end - if type(ami.help_message) == "function" then - print(ami.help_message(ami)) - elseif type(ami.help_message) == "string" then - print(ami.help_message) - else - if am.options.OUTPUT_FORMAT == "json" then - print(require "hjson".stringify(ami.commands, { invalid_objects_as_type = true, indent = false })) - else - -- collect and print help - if type(title) == "string" then - print(title .. NEW_LINE) - end - if type(description) == "string" then - print(description .. NEW_LINE) - end - if type(_summary) == "string" then - print("- " .. _summary .. NEW_LINE) - end - if print_usage then - print(generate_usage(ami, include_options_in_usage) .. NEW_LINE) - end - print(generate_help_message(ami)) - if type(footer) == "string" then - print(footer) - end - end + if print_usage then + print(generate_usage(ami, include_options_in_usage) .. NEW_LINE) end + + print(generate_help_message(ami)) + + if type(footer) == "string" then print(footer) end end ---Processes args passed to cli and executes appropriate operation @@ -473,18 +415,24 @@ function ami_cli.process(ami, args) end ami_assert( - type(action) == "table" or type(action) == "function" or type(action) == "string", + type(action) == "table" or type(action) == "function" or type(action) == "string", "Action not specified properly or not found! " .. cli_id, EXIT_CLI_ACTION_MISSING ) if ami.type == "external" then ami_assert( - type(action) == "string", + type(action) == "string", "Action has to be string specifying path to external cli", EXIT_CLI_INVALID_DEFINITION ) - return exec.external_action(action, parsed_args, ami) + local result, err, executed = exec.external_action(action, parsed_args, ami) + ami_assert(result ~= nil, err or "unknown", EXIT_CLI_ACTION_EXECUTION_ERROR) + if ami.should_return then + return result + end + os.exit(result) + return end if ami.type == "raw" then @@ -494,16 +442,13 @@ function ami_cli.process(ami, args) end --- we validate within native_action ---@diagnostic disable-next-line: param-type-mismatch - return exec.native_action(action, raw_args, ami) - end - - -- // TODO: remove in next version - if ami.type == "no-command" then - ami.type = "namespace" - print("Warning: cli.type 'no-command' is deprecated. Use 'namespace' instead.") + local result, err, executed = exec.native_action(action, raw_args, ami) + ami_assert(executed, err or "unknown", EXIT_CLI_ACTION_EXECUTION_ERROR) + return result end - local optionList, command, remainingArgs = ami_cli.parse_args(parsed_args, ami, { is_namespace = ami.type == "namespace", stop_on_non_option = ami.stop_on_non_option }) + local optionList, command, remainingArgs = ami_cli.parse_args(parsed_args, ami, + { is_namespace = ami.type == "namespace", stop_on_non_option = ami.stop_on_non_option }) local executable_command = command local valid, err = validate(optionList, executable_command, ami) @@ -515,18 +460,14 @@ function ami_cli.process(ami, args) table.insert(executable_command.__command_stack, executable_command and executable_command.id) end - -- // TODO: remove in next version - if ami.customHelp ~= nil and not ami.custom_help then - ami.custom_help = ami.customHelp - print("Warning: customHelp is deprecated. Use custom_help instead.") - end - if not ami.custom_help and optionList.help then return ami_cli.print_help(ami) end --- we validate within native_action ---@diagnostic disable-next-line: param-type-mismatch - return exec.native_action(action, { optionList, executable_command, remainingArgs, ami }, ami) + local result, err, executed = exec.native_action(action, { optionList, executable_command, remainingArgs, ami }, ami) + ami_assert(executed, err or "unknown", EXIT_CLI_ACTION_EXECUTION_ERROR) + return result end return ami_cli diff --git a/src/ami/internals/exec.lua b/src/ami/internals/exec.lua index 0fb4533..aac7663 100644 --- a/src/ami/internals/exec.lua +++ b/src/ami/internals/exec.lua @@ -38,6 +38,9 @@ end ---@param cmd string ---@param args CliArg[] ---@param options ExternalActionOptions +---@return number? exit_code +---@return string? error_message +---@return boolean executed function exec.external_action(cmd, args, options) local raw_args = {} if type(options) ~= "table" then options = {} end @@ -63,25 +66,20 @@ function exec.external_action(cmd, args, options) if require_quoting then arg = arg:gsub('"', '\\"') end exec_args = exec_args .. ' ' .. quote .. arg .. quote -- add qouted string end - local ok, result = proc.safe_exec(cmd .. " " .. exec_args) - ami_assert(ok, "Failed to execute external action - " .. tostring(result) .. "!") - if options.should_return then - return result.exit_code + local result, err = proc.exec(cmd .. " " .. exec_args) + if not result then + return nil, "failed to execute external action - " .. tostring(err), false end - os.exit(result.exit_code) + return result.exit_code, nil, true end - local desired_stdio = "inherit" - if options.stdio ~= nil then - desired_stdio = options.stdio - end + local desired_stdio = options.stdio ~= nil and options.stdio or "inherit" - local ok, result = proc.safe_spawn(cmd, raw_args, { wait = true, stdio = desired_stdio, env = options.environment }) - ami_assert(ok, "Failed to execute external action - " .. tostring(result) .. "!") - if options.should_return then - return result.exit_code + local result, err = proc.spawn(cmd, raw_args, { wait = true, stdio = desired_stdio, env = options.environment }) + if not result then + return nil, "failed to execute external action - " .. tostring(err), false end - os.exit(result.exit_code) + return result.exit_code, nil, true end ---@class ExecNativeActionOptions @@ -93,11 +91,12 @@ end ---@param action string|function ---@param args CliArg[]|string[] ---@param options ExecNativeActionOptions | nil ----@return any +---@return any result +---@return string? error_message +---@return boolean executed function exec.native_action(action, args, options) - if type(action) ~= "string" and type(action) ~= "function" then - error("Unsupported action/extension type (" .. type(action) .. ")!") - end + assert(type(action) == "string" or type(action) == "function", "action must be a string or a function") + if type(args) ~= "table" then args = {} end @@ -114,8 +113,7 @@ function exec.native_action(action, args, options) if type(action) == "string" then local ext, err = loadfile(action) if type(ext) ~= "function" then - ami_error("Failed to load extension from " .. action .. " - " .. err) - return + return nil, "failed to load extension from " .. action .. " - " .. err, false end id = action action = ext @@ -129,10 +127,10 @@ function exec.native_action(action, args, options) elseif type(options.partial_error_message) == "string" then err_msg = options.partial_error_message .. " - " .. tostring(result) end - ami_error(err_msg) + return nil, err_msg, false end AMI_CONTEXT_FAIL_EXIT_CODE = past_ctx_exit_code - return result + return result, nil, true end return exec diff --git a/src/ami/internals/interface.lua b/src/ami/internals/interface.lua index e4f5551..e9073e2 100644 --- a/src/ami/internals/interface.lua +++ b/src/ami/internals/interface.lua @@ -16,114 +16,128 @@ local interface = {} local kind_map = { - base = require "ami.internals.interface.base", - app = require "ami.internals.interface.app", - tool = require "ami.internals.interface.tool", + base = require"ami.internals.interface.base", + app = require"ami.internals.interface.app", + tool = require"ami.internals.interface.tool", } ---Creates new ExecutableAmiCli ---@param kind string ---@param options AmiCliGeneratorOptions? ----@return ExecutableAmiCli +---@return ExecutableAmiCli|AmiCliBase? +---@return string? error_message function interface.new(kind, options) local base = kind_map[kind] if base ~= nil then return kind_map[kind].new(options) end -- try load from path if not cached - local new_base, err = loadfile(kind) - ami_assert(new_base, "Base interface " .. (kind or "undefined") .. "not found or can not be loaded (Error: '" .. (err or "") .. "')!", - EXIT_INVALID_AMI_BASE_INTERFACE) - local ok, base = pcall(new_base--[[@as function]] , options) - ami_assert(ok, "Failed to load base interface - " .. (kind or "undefined") .. "!", EXIT_INVALID_AMI_BASE_INTERFACE) + local interface_init, err = loadfile(kind) + if not interface_init then + return nil, "interface '" .. kind .. "' not found or can not be loaded (error: '" .. (err or "") .. "')" + end + local result, err = interface_init(options) + if not result then + return nil, "interface load failure - " .. (kind or "undefined") .. " (error: '" .. (err or "") .. "')" + end + if type(result) ~= "table" then + return nil, "interface '" .. kind .. "' is not a table" + end -- recursively match all nested interfaces - if type(base.base) == "string" then - base = util.merge_tables(interface.new(base.base, options), base, true) + if type(result.base) == "string" then + local base, err = interface.new(result.base, options) + if not base then + return nil, "base: " .. tostring(err) + end + result = util.merge_tables(base, result, true) end - return base + return result end ---Finds and returns ami entrypoint ---@return boolean, ExecutableAmiCli|string, string? function interface.find_entrypoint() - ---@alias LoaderFn fun(content: string): boolean, ExecutableAmiCli|string + ---@alias LoaderFn fun(content: string): ExecutableAmiCli|string|nil, string? ---@type table local candidates = { - ["ami.lua"] = function(content) - local ok, sub_ami_fn, err = pcall(load, content) - if not ok or type(sub_ami_fn) ~= "function" then return false, err or "uknown internal error" end - local ok, sub_ami = pcall(sub_ami_fn) - if not ok then return false, sub_ami end - return true, sub_ami + ["ami.lua"] = function (content) + local sub_ami_fn, err = load(content) + if not sub_ami_fn or type(sub_ami_fn) ~= "function" then return nil, err or "uknown internal error" end + local ok, sub_ami_or_error = pcall(sub_ami_fn) + if not ok then return sub_ami_or_error, nil end + return nil, sub_ami_or_error end, - ["ami.json"] = function(content) - return hjson.safe_parse(content) + ["ami.json"] = function (content) + return hjson.parse(content) + end, + ["ami.hjson"] = function (content) + return hjson.parse(content) end, - ["ami.hjson"] = function(content) - return hjson.safe_parse(content) - end } for candidate, loader in pairs(candidates) do - local ok, sub_ami_content = fs.safe_read_file(candidate) - if ok then + local sub_ami_content, err = fs.read_file(candidate) + if sub_ami_content then log_trace(candidate .. " found loading...") - local ok, ami = loader(sub_ami_content) - return ok, ami, candidate + local ami, err = loader(sub_ami_content) + return ami ~= nil, ami or err, candidate end end - return false, "Entrypoint interface not found (ami.lua/ami.json/ami.hjson missing)!", nil + return false, "entrypoint interface not found (ami.lua/ami.json/ami.hjson missing)", nil end ---Loads ExecutableAmiCli from ami.lua using specified base of interfaceKind ---@param interface_kind string ---@param shallow boolean? ----@return boolean, ExecutableAmiCli +---@return ExecutableAmiCli +---@return string? error_message +---@return boolean is_app_specific function interface.load(interface_kind, shallow) - log_trace("Loading app specific ami...") + log_trace"Loading app specific ami..." local sub_ami if not shallow then - local ok, sub_ami_raw = fs.safe_read_file("ami.json") - if ok then - log_trace("ami.json found loading...") - ok, sub_ami = hjson.safe_parse(sub_ami_raw) - log_trace("ami.json load " .. (ok and "successful" or "failed") .. "...") - if not ok then - log_warn("ami.json load failed - " .. tostring(sub_ami)) + local sub_ami_raw, err = fs.read_file"ami.json" + if sub_ami_raw then + log_trace"ami.json found loading..." + sub_ami, err = hjson.parse(sub_ami_raw) + log_trace("ami.json load " .. (sub_ami and "successful" or "failed") .. "...") + if not sub_ami then + log_warn("ami.json load failed - " .. tostring(err)) else - log_trace "ami.json loaded" + log_trace"ami.json loaded" end end - if not ok then - ok, sub_ami_raw = fs.safe_read_file("ami.hjson") - if ok then - log_trace("ami.hjson found loading...") - ok, sub_ami = hjson.safe_parse(sub_ami_raw) - if not ok then - log_warn("ami.hjson load failed - " .. tostring(sub_ami)) + if not sub_ami_raw then + sub_ami_raw, err = fs.read_file"ami.hjson" + if sub_ami_raw then + log_trace"ami.hjson found loading..." + sub_ami, err = hjson.parse(sub_ami_raw) + if not sub_ami then + log_warn("ami.hjson load failed - " .. tostring(err)) else - log_trace "ami.hjson loaded" + log_trace"ami.hjson loaded" end end end - if not ok then - ok, sub_ami_raw = fs.safe_read_file("ami.lua") - if ok then - log_trace("ami.lua found, loading...") - local _err - ok, sub_ami, _err = pcall(load, sub_ami_raw) - if ok and type(sub_ami) == "function" then - ok, sub_ami = pcall(sub_ami) + if not sub_ami_raw then + sub_ami_raw, err = fs.read_file"ami.lua" + if sub_ami_raw then + log_trace"ami.lua found, loading..." + local err + sub_ami, err = load(sub_ami_raw) + if sub_ami and type(sub_ami) == "function" then + local ok, sub_ami_or_error = pcall(sub_ami) if ok then - log_trace("ami.lua loaded") + log_trace"ami.lua loaded" + sub_ami = sub_ami_or_error else log_warn("ami.lua load failed - " .. tostring(sub_ami)) end else - log_warn("ami.lua load failed - " .. tostring(_err)) + log_warn("ami.lua load failed - " .. tostring(err)) end end end @@ -132,13 +146,16 @@ function interface.load(interface_kind, shallow) local base_interface if type(sub_ami) ~= "table" then - base_interface = interface.new(interface_kind or "app", { is_app_ami_loaded = false }) + base_interface, err = interface.new(interface_kind or "app", { is_app_ami_loaded = false }) if not shallow then - log_warn("App specific ami not found!") + log_warn"app specific ami not found" end - return false, base_interface + return base_interface, nil, false else - base_interface = interface.new(sub_ami.base or interface_kind or "app", { is_app_ami_loaded = true }) + base_interface, err = interface.new(sub_ami.base or interface_kind or "app", { is_app_ami_loaded = true }) + if not base_interface then + return nil, err, false + end end local id = base_interface.id @@ -150,7 +167,7 @@ function interface.load(interface_kind, shallow) local result = util.merge_tables(base_interface, sub_ami, true) result.id = id result.title = title - return true, result + return result, nil, true end return interface diff --git a/src/ami/internals/interface/app.lua b/src/ami/internals/interface/app.lua index 2285671..f805a24 100644 --- a/src/ami/internals/interface/app.lua +++ b/src/ami/internals/interface/app.lua @@ -16,8 +16,9 @@ local ami_base = require"ami.internals.interface.base" ---Generates default app interface ----@param options AmiCliGeneratorOptions ----@return ExecutableAmiCli +---@param options AmiCliGeneratorOptions? +---@return ExecutableAmiCli? result +---@return string? error_message local function new(options) if type(options) ~= "table" then options = {} @@ -30,11 +31,10 @@ local function new(options) local ok, entrypoint = am.__find_entrypoint() if not ok then -- fails with proper error in case of entrypoint not found or invalid - print("Failed to load entrypoint:") - ami_error(entrypoint --[[@as string]], EXIT_INVALID_AMI_INTERFACE) + ami_error("failed to load entrypoint: " .. tostring(entrypoint), EXIT_INVALID_AMI_INTERFACE) end -- entrypoint found and loadable but required action undefined - ami_error("Violation of AMI@app standard! " .. implementation_status, implementation_error) + ami_error("violation of ami@app standard " .. implementation_status, implementation_error) end local base = ami_base.new() --[[@as ExecutableAmiCli]] @@ -147,7 +147,7 @@ local function new(options) }, -- (options, command, args, cli) action = function(options) - ami_assert(am.__has_app_specific_interface or options.force, "You are trying to remove app, but app specific removal routine is not available. Use '--force' to force removal", EXIT_APP_REMOVE_ERROR) + ami_assert(am.__has_app_specific_interface or options.force, "you are trying to remove app, but app specific removal routine is not available. Use '--force' to force removal", EXIT_APP_REMOVE_ERROR) if options.all then am.app.remove() log_success("Application removed.") diff --git a/src/ami/internals/interface/base.lua b/src/ami/internals/interface/base.lua index 1f48db9..1d7998d 100644 --- a/src/ami/internals/interface/base.lua +++ b/src/ami/internals/interface/base.lua @@ -14,7 +14,8 @@ -- along with this program. If not, see . ---Generates AmiBaseInterface ----@return AmiCliBase +---@return AmiCliBase result +---@return string? error_message local function new() return { id = "ami", diff --git a/src/ami/internals/interface/tool.lua b/src/ami/internals/interface/tool.lua index 012bf35..dc9afa7 100644 --- a/src/ami/internals/interface/tool.lua +++ b/src/ami/internals/interface/tool.lua @@ -16,8 +16,9 @@ local ami_base = require"ami.internals.interface.base" ---Generates default tool interface ----@param options AmiCliGeneratorOptions ----@return ExecutableAmiCli +---@param options AmiCliGeneratorOptions? +---@return ExecutableAmiCli? result +---@return string? error_message local function new(options) if type(options) ~= "table" then options = {} @@ -30,11 +31,10 @@ local function new(options) local ok, entrypoint = am.__find_entrypoint() if not ok then -- fails with proper error in case of entrypoint not found or invalid - print("Failed to load entrypoint:") - ami_error(entrypoint --[[@as string]], EXIT_INVALID_AMI_INTERFACE) + ami_error("failed to load entrypoint: " .. tostring(entrypoint), EXIT_INVALID_AMI_INTERFACE) end -- entrypoint found and loadable but required action undefined - ami_error("Violation of AMI@tool standard! " .. implementation_status, implementation_error) + ami_error("violation of ami@tool standard " .. implementation_status, implementation_error) end local base = ami_base.new() --[[@as ExecutableAmiCli]] @@ -47,7 +47,7 @@ local function new(options) action = function() local available, id, ver = am.app.is_update_available() if available then - ami_error("Found new version " .. ver .. " of " .. id .. ", please run setup...", EXIT_SETUP_REQUIRED) + ami_error("found new version " .. ver .. " of " .. id .. ", please run setup...", EXIT_SETUP_REQUIRED) end log_info("Tool is up to date.") end @@ -67,7 +67,7 @@ local function new(options) }, -- (options, command, args, cli) action = function(options) - ami_assert(am.__has_app_specific_interface or options.force, "You are trying to remove tool, but tool specific removal routine is not available. Use '--force' to force removal", EXIT_APP_REMOVE_ERROR) + ami_assert(am.__has_app_specific_interface or options.force, "you are trying to remove tool, but tool specific removal routine is not available. Use '--force' to force removal", EXIT_APP_REMOVE_ERROR) if options.all then am.app.remove() log_success("Tool removed.") diff --git a/src/ami/internals/options/cache.lua b/src/ami/internals/options/cache.lua index b1a3070..12a5af2 100644 --- a/src/ami/internals/options/cache.lua +++ b/src/ami/internals/options/cache.lua @@ -14,9 +14,11 @@ -- along with this program. If not, see . local cache_options = {} +local DEFAULT_EXPIRATION = 86400 + local CACHE_DIR = nil local options = { - CACHE_EXPIRATION_TIME = 86400 + CACHE_EXPIRATION_TIME = DEFAULT_EXPIRATION, } local members = {} @@ -24,9 +26,6 @@ for k, _ in pairs(options) do members[k] = true end -local computed = { -} - function cache_options.index(t, k) if k == "CACHE_DIR" then return true, CACHE_DIR @@ -36,10 +35,6 @@ function cache_options.index(t, k) return true, options[k] end - local getter = computed[k] - if type(getter) == "function" then - return true, getter(t) - end return false, nil end diff --git a/src/ami/internals/options/repository.lua b/src/ami/internals/options/repository.lua index 54bcfaa..bf4ed19 100644 --- a/src/ami/internals/options/repository.lua +++ b/src/ami/internals/options/repository.lua @@ -22,8 +22,8 @@ local mirrors = { local DEFAULT_REPOSITORY_URL for _, candidate in ipairs(mirrors) do - local ok, _, status = net.safe_download_string(candidate .. "TEST", { follow_redirects = true }) - if ok and status / 100 == 2 then + local response, status = net.download_string(candidate .. "TEST", { follow_redirects = true }) + if response and status / 100 == 2 then DEFAULT_REPOSITORY_URL = candidate break end diff --git a/src/ami/internals/pkg.lua b/src/ami/internals/pkg.lua index e52fa30..635145f 100644 --- a/src/ami/internals/pkg.lua +++ b/src/ami/internals/pkg.lua @@ -47,23 +47,44 @@ local ami_util = require "ami.internals.util" ---@field model AmiPackageModelOrigin|nil ---@field extensions AmiPackageModelOrigin[] +---@alias AvailableUpdates table + +---@class PreparedPackage +---@field files table +---@field model AmiPackageModelDef +---@field version_tree AmiPackage +---@field tmp_archive_paths string[] + local pkg = {} ---Normalizes package type ----@param pkg_type AmiPackageType | AmiPackage +---@generic T: AmiPackageType | AmiPackage +---@param pkg_type T +---@return T? result +---@return string? error_message local function normalize_pkg_type(pkg_type) - local bound_packages = am.app.__is_loaded() and am.app.get("dependency override") - if type(pkg_type.id) == "string" and type(bound_packages) == "table" and type(bound_packages[pkg_type.id]) == "string" then - pkg_type.version = bound_packages[pkg_type.id] - log_warn("Using overriden version " .. pkg_type.version .. " of " .. pkg_type.id .. "!") + assert(type(pkg_type) == "table", "invalid pkg type") + pkg_type = util.clone(pkg_type, true) + + local bound_packages = am.app.__is_loaded() and am.app.get("dependency_override", {}) or {} + local bound_package = bound_packages[pkg_type.id] + if type(bound_package) == "string" then + pkg_type.version = bound_package + log_warn("using overridden version " .. pkg_type.version .. " of " .. pkg_type.id) end - if pkg_type.version == nil then - pkg_type.version = "latest" + + if pkg_type.version == nil then pkg_type.version = "latest" end + + if type(pkg_type.version) ~= "string" then + return nil, "invalid pkg version - expected string, got " .. type(pkg_type.version) end - ami_assert(type(pkg_type.version) == "string", "Invalid pkg version", EXIT_PKG_INVALID_VERSION) - if type(pkg_type.repository) ~= "string" then - pkg_type.repository = am.options.DEFAULT_REPOSITORY_URL + if type(pkg_type.repository) ~= "string" then + pkg_type.repository = am.options.DEFAULT_REPOSITORY_URL + if type(pkg_type.repository) ~= "string" then + return nil, "invalid pkg repository - expected string, got " .. type(pkg_type.repository) + end end + return pkg_type, nil end if TEST_MODE then @@ -72,7 +93,7 @@ end ---@param app_type table ---@param channel string? ----@return boolean, AmiPackageDef|string, number? +---@return AmiPackageDef?, string? local function download_pkg_def(app_type, channel) local pkg_id = app_type.id:gsub("%.", "/") @@ -86,147 +107,160 @@ local function download_pkg_def(app_type, channel) local ok, pkg_definition_raw = am.cache.get("package-definition", full_pkg_id) if ok then - local ok, pkg_definition = hjson.safe_parse(pkg_definition_raw) - if ok and - (app_type.version ~= "latest" or (type(pkg_definition.last_ami_check) == "number" and pkg_definition.last_ami_check + am.options.CACHE_EXPIRATION_TIME > os.time())) then - return true, pkg_definition + local pkg_definition, _ = hjson.parse(pkg_definition_raw) + if pkg_definition and + (app_type.version ~= "latest" or (type(pkg_definition.last_ami_check) == "number" and pkg_definition.last_ami_check + am.options.CACHE_EXPIRATION_TIME > os.time())) then + return pkg_definition end end - local ok, pkg_definition_raw, status = net.safe_download_string(definition_url) - if not ok or status / 100 ~= 2 then - return false, "failed to download package definition - " .. tostring(pkg_definition_raw), EXIT_PKG_INVALID_DEFINITION + local pkg_definition_raw, status = net.download_string(definition_url) + if not pkg_definition_raw or status / 100 ~= 2 then + return nil, "failed to download package definition - " .. tostring(status) end - local ok, pkg_definition = hjson.safe_parse(pkg_definition_raw) - if not ok then - return ok, "failed to parse package definition - " .. app_type.id, EXIT_PKG_INVALID_DEFINITION + local pkg_definition, err = hjson.parse(pkg_definition_raw) + if not pkg_definition then + return nil, err end local cached_definition = util.merge_tables(pkg_definition, { last_ami_check = os.time() }) - local ok, cached_definition_raw = hjson.safe_stringify(cached_definition) - ok = ok and am.cache.put(cached_definition_raw, "package-definition", full_pkg_id) + local cached_definition_raw, _ = hjson.stringify(cached_definition) + local ok = cached_definition_raw and am.cache.put(cached_definition_raw, "package-definition", full_pkg_id) if ok then - log_trace("Local copy of " .. app_type.id .. " definition saved into " .. full_pkg_id) + log_trace("local copy of " .. app_type.id .. " definition saved into " .. full_pkg_id) else -- it is not necessary to save definition locally as we hold version in memory already - log_trace("Failed to cache " .. app_type.id .. " definition!") + log_trace("failed to cache " .. app_type.id .. " definition!") end - return true, pkg_definition + return pkg_definition end ---Downloads app package definition from repository. ---@param app_type AmiPackage|AmiPackageType ----@return boolean, AmiPackageDef +---@return AmiPackageDef? result +---@return string? error_message local function get_pkg_def(app_type) -- try to download based on app channel - local ok, package_definition = download_pkg_def(app_type, app_type.channel) + local package_definition, err = download_pkg_def(app_type, app_type.channel) -- if we failed to download channel and we werent downloading default already, try download default - if not ok and type(app_type.channel) == "string" and app_type.channel ~= "" then - log_trace("Failed to obtain package definition from channel " .. app_type.channel .. "! Retrying with default...") - local package_definition_or_error, exit_code - ok, package_definition_or_error, exit_code = download_pkg_def(app_type, nil) - ami_assert(ok, package_definition_or_error--[[@as string]] , exit_code) - package_definition = package_definition_or_error - end - if ok then - log_trace("Successfully parsed " .. app_type.id .. " definition.") + if not package_definition and type(app_type.channel) == "string" and app_type.channel ~= "" then + log_trace("failed to obtain package definition from channel " .. app_type.channel .. ", retrying with default channel...") + package_definition, err = download_pkg_def(app_type, nil) end - return ok, package_definition --[[@as AmiPackageDef]] + return package_definition, err end +---@class AmiGetPackageResult +---@field path string +---@field hash string + ---Downloads app package and returns its path. ---@param package_definition AmiPackageDef ----@return string, string +---@return AmiGetPackageResult? result +---@return string? error_message local function get_pkg(package_definition) - local pkg_id = package_definition.sha256 or package_definition.sha512 + local pkg_hash = package_definition.sha256 or package_definition.sha512 local tmp_pkg_path = os.tmpname() - local ok, err = am.cache.get_to_file("package-archive", pkg_id, tmp_pkg_path, + local ok, err = am.cache.get_to_file("package-archive", pkg_hash, tmp_pkg_path, { sha256 = am.options.NO_INTEGRITY_CHECKS ~= true and package_definition.sha256 or nil, sha512 = am.options.NO_INTEGRITY_CHECKS ~= true and package_definition.sha512 or nil }) if ok then - log_trace("Using cached version of " .. pkg_id) - return pkg_id, tmp_pkg_path + log_trace("using cached version of " .. pkg_hash) + return { + path = tmp_pkg_path, + hash = pkg_hash + } else - log_trace("INTERNAL ERROR: Failed to get package from cache: " .. tostring(err)) + log_trace("internal error: failed to get package from cache: " .. tostring(err)) end - local ok, err = net.safe_download_file(package_definition.source, tmp_pkg_path, { follow_redirects = true, show_default_progress = false }) - if not ok then - ami_error("Failed to get package " .. tostring(err) .. " - " .. tostring(package_definition.id or pkg_id), EXIT_PKG_DOWNLOAD_ERROR) + local ok, err = net.download_file(package_definition.source, tmp_pkg_path, { follow_redirects = true, show_default_progress = false }) + if not ok then return nil, err end + local hash, err = fs.hash_file(tmp_pkg_path, { hex = true, type = package_definition.sha512 and "sha512" or nil }) + if not hash then + fs.remove(tmp_pkg_path) + return nil, err end - local ok, hash = fs.safe_hash_file(tmp_pkg_path, { hex = true, type = package_definition.sha512 and "sha512" or nil }) - ami_assert(ok and hash == pkg_id, "Failed to verify package integrity - " .. pkg_id .. "!", EXIT_PKG_INTEGRITY_CHECK_ERROR) - log_trace("Integrity checks of " .. pkg_id .. " successful.") + log_trace("integrity checks of " .. pkg_hash .. " successful") - local ok, err = am.cache.put_from_file(tmp_pkg_path, "package-archive", pkg_id) + local ok, err = am.cache.put_from_file(tmp_pkg_path, "package-archive", pkg_hash) if not ok then - log_trace("Failed to cache package " .. pkg_id .. " - " .. tostring(err)) + log_trace("failed to cache package " .. pkg_hash .. " - " .. tostring(err)) end - return pkg_id, tmp_pkg_path + return { + path = tmp_pkg_path, + hash = pkg_hash + } end ---Extracts package specs from package archive and returns it ---@param pkg_path string ----@return table +---@return table? specs +---@return string? error_message local function get_pkg_specs(pkg_path) - local ok, specs_raw = zip.safe_extract_string(pkg_path, "specs.json", { flatten_root_dir = true }) - - ami_assert(ok, "Failed to extract " .. pkg_path .. "", EXIT_PKG_LOAD_ERROR) - if specs_raw == nil then - -- no specs, standalone package - return {} + local specs_raw, err = zip.extract_string(pkg_path, "specs.json", { flatten_root_dir = true }) + if not specs_raw then + if err ~= "not found" then return nil, err end + specs_raw = {} end - log_trace("Analyzing " .. pkg_path .. " specs...") + log_trace("analyzing " .. pkg_path .. " specs...") - local ok, specs = hjson.safe_parse(specs_raw) - if not ok then - ami_error("Failed to parse package specification - " .. pkg_path .. " " .. specs, EXIT_PKG_LOAD_ERROR) + local specs, err = hjson.parse(specs_raw) + if not specs then + return nil, err end - log_trace("Successfully parsed " .. pkg_path .. " specification.") + log_trace("successfully parsed '" .. pkg_path .. "' specification") return specs end ---Generates structures necessary for package setup and version tree of all packages required ---@param app_type AmiPackageType ----@return table ----@return AmiPackageModelDef ----@return AmiPackage ----@return string[] +---@return PreparedPackage? result +---@return string? error_message function pkg.prepare_pkg(app_type) if type(app_type.id) ~= "string" then - ami_error("Invalid pkg specification or definition!", EXIT_PKG_INVALID_DEFINITION) + return nil, "invalid pkg id - expected string, got " .. type(app_type.id) end - log_debug("Preparation of " .. app_type.id .. " started ...") - normalize_pkg_type(app_type) - local ok + log_debug("preparation of " .. app_type.id .. " started") + + local app_type, err = normalize_pkg_type(app_type) + if not app_type then return nil, err end + + local local_sources = SOURCES or {} + local package_definition - if type(SOURCES) == "table" and SOURCES[app_type.id] then - local local_source = SOURCES[app_type.id] - log_trace("Loading local package from path " .. local_source) + + local local_package_source = local_sources[app_type.id] + if local_package_source then + log_trace("loading local package from path " .. local_package_source) local tmp_path = os.tmpname() - local ok, err = zip.safe_compress(local_source, tmp_path, { recurse = true, overwrite = true }) + local ok, err = zip.compress(local_package_source, tmp_path, { recurse = true, overwrite = true }) if not ok then - fs.safe_remove(tmp_path) - ami_error("Failed to compress local source directory: " .. (err or ""), EXIT_PKG_LOAD_ERROR) + fs.remove(tmp_path) + return nil, err end - local ok, hash = fs.safe_hash_file(tmp_path, { hex = true, type = "sha256" }) - if not ok then - fs.safe_remove(tmp_path) - ami_error("Failed to load package from local sources", EXIT_PKG_INTEGRITY_CHECK_ERROR) + local hash, err = fs.hash_file(tmp_path, { hex = true, type = "sha256" }) + if not hash then + fs.remove(tmp_path) + return nil, err end am.cache.put_from_file(tmp_path, "package-archive", hash) - fs.safe_remove(tmp_path) + fs.remove(tmp_path) package_definition = { sha256 = hash, id = "debug-dir-pkg" } else - ok, package_definition = get_pkg_def(app_type) - ami_assert(ok, "Failed to get package definition", EXIT_PKG_INVALID_DEFINITION) + package_definition, err = get_pkg_def(app_type) + if not package_definition then return nil, err end end - local pkg_id, package_archive_path = get_pkg(package_definition) - local specs = get_pkg_specs(package_archive_path) + local get_result, err = get_pkg(package_definition) + if not get_result then return nil, err end + local pkg_hash, package_archive_path = get_result.hash, get_result.path + + local specs, err = get_pkg_specs(package_archive_path) + if not specs then return nil, err end ---@type table local result = {} @@ -252,16 +286,18 @@ function pkg.prepare_pkg(app_type) for _, dependency in pairs(specs.dependencies) do log_trace("Collecting dependency " .. (type(dependency) == "table" and dependency.id or "n." .. _) .. "...") - local sub_result, sub_model, sub_version_tree, sub_package_sources = pkg.prepare_pkg(dependency) - tmp_package_sources = util.merge_arrays(tmp_package_sources, sub_package_sources) --[[@as string[] ]] - if type(sub_model.model) == "table" then + local sub_package_preparation_result, err = pkg.prepare_pkg(dependency) + if not sub_package_preparation_result then return nil, err end + + tmp_package_sources = util.merge_arrays(tmp_package_sources, sub_package_preparation_result.tmp_archive_paths) --[[@as string[] ]] + if type(sub_package_preparation_result.model.model) == "table" then -- we overwrite entire model with extension if we didnt get extensions only - model = sub_model + model = sub_package_preparation_result.model else - model = util.merge_tables(model, sub_model, true) + model = util.merge_tables(model, sub_package_preparation_result.model, true) end - result = util.merge_tables(result, sub_result, true) - table.insert(version_tree.dependencies, sub_version_tree) + result = util.merge_tables(result, sub_package_preparation_result.files, true) + table.insert(version_tree.dependencies, sub_package_preparation_result.version_tree) end log_trace("Dependcies of " .. app_type.id .. " successfully collected.") else @@ -270,33 +306,40 @@ function pkg.prepare_pkg(app_type) log_trace("Preparing " .. app_type.id .. " files...") local files = zip.get_files(package_archive_path, { flatten_root_dir = true }) - local _filter = function(_, v) -- ignore directories + local function filter(_, v) -- ignore directories return type(v) == "string" and #v > 0 and not v:match("/$") end local is_model_found = false - for _, file in ipairs(table.filter(files, _filter)) do + for _, file in ipairs(table.filter(files, filter)) do -- assign file source if file == "model.lua" then is_model_found = true ---@type AmiPackageModelOrigin - model.model = { source = package_archive_path, pkg_id = pkg_id } + model.model = { source = package_archive_path, pkg_id = pkg_hash } model.extensions = {} elseif file == "model.ext.lua" then if not is_model_found then -- we ignore extensions in same layer - table.insert(model.extensions, { source = package_archive_path, pkg_id = pkg_id }) + table.insert(model.extensions, { source = package_archive_path, pkg_id = pkg_hash }) end elseif file ~= "model.ext.lua.template" and "model.ext.template.lua" then -- we do not accept templates for model as model is used to render templates :) - result[file] = { source = package_archive_path, id = app_type.id, file = file, pkg_id = pkg_id } + result[file] = { source = package_archive_path, id = app_type.id, file = file, pkg_id = pkg_hash } end end log_trace("Preparation of " .. app_type.id .. " complete.") - return result, model, version_tree, tmp_package_sources + return { + files = result, + model = model, + version_tree = version_tree, + tmp_archive_paths = tmp_package_sources + } end ---Extracts files from package archives ---@param file_list table +---@return boolean success +---@return string? error_message function pkg.unpack_layers(file_list) local unpack_map = {} local unpack_id_map = {} @@ -310,12 +353,12 @@ function pkg.unpack_layers(file_list) end for source, files in pairs(unpack_map) do - log_debug("Extracting (" .. source .. ") " .. unpack_id_map[source]) + log_debug("extracting (" .. source .. ") " .. unpack_id_map[source]) local filter = function(f) return files[f] end - local transform = function(f, destination) + local function transform(f, destination) local name, extension = path.nameext(f) if extension == "template" then destination = path.combine(destination, ".ami-templates") @@ -330,77 +373,91 @@ function pkg.unpack_layers(file_list) end local options = { flatten_root_dir = true, filter = filter, transform_path = transform } - local ok, err = zip.safe_extract(source, ".", options) - ami_assert(ok, err or "", EXIT_PKG_LAYER_EXTRACT_ERROR) + local ok, err = zip.extract(source, ".", options) + if not ok then + return false, err + end log_trace("(" .. source .. ") " .. unpack_id_map[source] .. " extracted.") end + return true end ---Generates app model from model definition ---@param model_definition AmiPackageModelDef +---@return boolean success +---@return string? error_message function pkg.generate_model(model_definition) if type(model_definition.model) ~= "table" or type(model_definition.model.source) ~= "string" then - log_trace("No model found. Skipping model generation ...") - return - end - log_trace("Generating app model...") - local ok, model = zip.safe_extract_string(model_definition.model.source, "model.lua", { flatten_root_dir = true }) - if not ok then - ami_error("Failed to extract app model - " .. model .. "!", EXIT_PKG_MODEL_GENERATION_ERROR) + log_trace("no model found - skipping model generation ...") + return true end + log_trace("generating app model...") + local model, err = zip.extract_string(model_definition.model.source, "model.lua", { flatten_root_dir = true }) + if not model then return false, err end for _, model_extension in ipairs(model_definition.extensions) do - local ok, extension_content_or_error = zip.safe_extract_string(model_extension.source, "model.ext.lua", { flatten_root_dir = true }) - if not ok then - ami_error("Failed to extract app model extension - " .. extension_content_or_error .. "!", EXIT_PKG_MODEL_GENERATION_ERROR) - end - model = model .. "\n\n----------- injected ----------- \n--\t" .. model_extension.pkg_id .. "/model.ext.lua\n-------------------------------- \n\n" .. extension_content_or_error + local extension_content, err = zip.extract_string(model_extension.source, "model.ext.lua", { flatten_root_dir = true }) + if not extension_content then return false, err end + model = model .. "\n\n----------- injected ----------- \n--\t" .. model_extension.pkg_id .. "/model.ext.lua\n-------------------------------- \n\n" .. extension_content end - local ok = fs.safe_write_file("model.lua", model) - ami_assert(ok, "Failed to write model.lua!", EXIT_PKG_MODEL_GENERATION_ERROR) + return fs.write_file("model.lua", model) end + ---Check whether there is new version of specified pkg. ---If new version is found returns true, pkg.id and new version ---@param package AmiPackage | AmiPackageType ---@param current_version string | nil ----@return boolean, string|nil, string|nil +---@return boolean? is_update_available +---@return string|AvailableUpdates? version_or_error_message function pkg.is_pkg_update_available(package, current_version) - if type(current_version) ~= "string" then - current_version = package.version - end - log_trace("Checking update availability of " .. package.id) - normalize_pkg_type(package) + current_version = type(current_version) == "string" and current_version or package.version + log_trace("checking update availability of " .. package.id) + + local normalized, err = normalize_pkg_type(package) + if not normalized then return nil, err end + package = normalized - if package.wanted_version ~= "latest" and package.wanted_version ~= nil then - log_trace("Static version detected, update suppressed.") + local wanted_version = package.wanted_version or package.version + if package.wanted_version ~= "latest" and wanted_version == current_version then + log_trace("static version detected, update not required...") return false end package.version = package.wanted_version - local ok, package_definition = get_pkg_def(package) - ami_assert(ok, "Failed to get package definition", EXIT_PKG_INVALID_DEFINITION) + local pkg_def, err = get_pkg_def(package) + if not pkg_def then + log_trace("failed to get package definition - " .. tostring(err)) + return nil, err + end if type(current_version) ~= "string" then - log_trace("New version available...") - return true, package.id, package_definition.version + log_trace("new version available") + return true, { [package.id] = pkg_def.version } end - if ver.compare(package_definition.version, current_version) > 0 then - log_trace("New version available...") - return true, package.id, package_definition.version + if ver.compare(pkg_def.version, current_version) > 0 then + log_trace("new version available") + return true, { [package.id] = pkg_def.version } end if util.is_array(package.dependencies) then + local updates = {} for _, dep in ipairs(package.dependencies) do - local _available, _id, _ver = pkg.is_pkg_update_available(dep, dep.version) - if _available then - log_trace("New version of child package found...") - return true, _id, _ver + local is_update_available, available_updates = pkg.is_pkg_update_available(dep, dep.version) + if is_update_available then + for dep_id, dep_version in pairs(available_updates) do + local existing_version = updates[dep_id] + if not existing_version or ver.compare(dep_version, existing_version) > 0 then + updates[dep_id] = dep_version + end + end end end + return next(updates) ~= nil, updates end - return false + log_trace("no update required.") + return false, {} end return pkg diff --git a/src/ami/internals/tpl.lua b/src/ami/internals/tpl.lua index 1f233a6..d6ab03e 100644 --- a/src/ami/internals/tpl.lua +++ b/src/ami/internals/tpl.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2024 alis.is +-- Copyright (C) 2025 alis.is -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published @@ -47,13 +47,14 @@ local function _to_renderable_data(source) end ---Renders template files in app directory +---@return boolean, string? function tpl.render_templates() log_info("Generating app templated files...") ---@type boolean, DirEntry[]|string - local ok, templates = fs.safe_read_dir(".ami-templates", { recurse = true, as_dir_entries = true }) - if not ok or #templates == 0 then + local templates, _ = fs.read_dir(".ami-templates", { recurse = true, as_dir_entries = true }) + if not templates or #templates == 0 then log_trace("No template found, skipping...") - return + return true end -- transform model and configuration table to renderable data ( __ARRAY, __CLI_ARGS) @@ -77,20 +78,24 @@ function tpl.render_templates() log_trace("Rendering '" .. template_path .. "' to '" .. rendered_path .. "'...") - local _ok, _template = fs.safe_read_file(template_path) - ami_assert(_ok, "Read failed for " .. template_path .. " - " .. (_template or ""), EXIT_TPL_READ_ERROR) - local _result = lustache:render(_template, vm) - - local _ok, _error = fs.safe_mkdirp(path.dir(rendered_path)) - if _ok then - _ok, _error = fs.safe_write_file(rendered_path, _result) + local template, err = fs.read_file(template_path) + if not template then + return false, "failed to read template file '" .. tostring(template_path) .. "' - " .. tostring(err) end + local _result = lustache:render(template, vm) - ami_assert(_ok, "Write failed for " .. template_path .. " - " .. (_error or ""), EXIT_TPL_WRITE_ERROR) + local ok, err = fs.mkdirp(path.dir(rendered_path)) + if ok then + ok, err = fs.write_file(rendered_path, _result) + end + if not ok then + return false, "failed to write rendered template file '" .. tostring(rendered_path) .. "' - " .. tostring(err) + end log_trace("'" .. rendered_path .. "' rendered successfully.") end end tpl.__templates_generated = true + return true end return tpl diff --git a/src/ami/plugin.lua b/src/ami/plugin.lua index f367881..2de66c6 100644 --- a/src/ami/plugin.lua +++ b/src/ami/plugin.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2024 alis.is +-- Copyright (C) 2025 alis.is -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published @@ -39,6 +39,9 @@ if TEST_MODE then end end +---@param name string +---@param version string +---@return any?, string? local function _get_plugin_def(name, version) local plugin_id = name .. "@" .. version @@ -51,30 +54,30 @@ local function _get_plugin_def(name, version) local ok, plugin_definition_rw = am.cache.get("plugin-definition", plugin_id) if ok then - local ok, plugin_definition = hjson.safe_parse(plugin_definition_rw) - if ok and + local plugin_definition, err = hjson.parse(plugin_definition_rw) + if plugin_definition and (version ~= "latest" or (type(plugin_definition.last_ami_check) == "number" and plugin_definition.last_ami_check + am.options.CACHE_EXPIRATION_TIME > os.time())) then return plugin_definition end end - local ok, plugin_definition_raw, status = net.safe_download_string(definition_url) - ami_assert( - ok and status / 100 == 2, - string.join_strings("", "Failed to download ", plugin_id, " definition: ", plugin_definition_raw), - EXIT_PLUGIN_INVALID_DEFINITION - ) - local ok, plugin_definition = hjson.safe_parse(plugin_definition_raw) - ami_assert( - ok, - string.join_strings("", "Failed to parse ", plugin_id, " definition: ", plugin_definition), - EXIT_PLUGIN_INVALID_DEFINITION - ) + local plugin_definition_raw, status = net.download_string(definition_url) + if not plugin_definition_raw or status / 100 ~= 2 then + if type(status) == "number" and status / 100 == 404 then + return nil, "plugin definition not found: " .. plugin_id + end + return nil, "failed to download plugin definition: " .. tostring(plugin_id) .. " - " .. tostring(status) + end + + local plugin_definition, err = hjson.parse(plugin_definition_raw) + if not plugin_definition or type(plugin_definition) ~= "table" then + return nil, "failed to parse plugin definition: " .. tostring(plugin_id) .. " - " .. tostring(err) + end local cached_definition = util.merge_tables(plugin_definition, { last_ami_check = os.time() }) - local ok, plugin_definition_raw = hjson.safe_stringify(cached_definition) + local plugin_definition_raw, _ = hjson.stringify(cached_definition) - ok = ok and am.cache.put(plugin_definition_raw, "plugin-definition", plugin_id) + local ok = plugin_definition_raw and am.cache.put(plugin_definition_raw, "plugin-definition", plugin_id) if ok then log_trace("Local copy of " .. plugin_id .. " definition saved into cache") else @@ -86,38 +89,31 @@ local function _get_plugin_def(name, version) return plugin_definition end ----@class AmiGetPluginOptions: AmiErrorOptions +---@class AmiGetPluginOptions ---@field version string? ----@field safe boolean? ---#DES am.plugin.get --- ---Loads plugin by name and returns it. ---@param name string ---@param options AmiGetPluginOptions? ----@return any, any +---@return any, string? function am.plugin.get(name, options) if type(options) ~= "table" then options = {} end - local version = "latest" - if type(options.version) == "string" then - version = options.version - end + local version = type(options.version) == "string" and options.version or "latest" - local bound_packages = am.app.__is_loaded() and am.app.get("dependency override") + local bound_packages = am.app.__is_loaded() and am.app.get("dependency_override") if type(name) == "string" and type(bound_packages) == "table" and type(bound_packages["plugin." .. name]) == "string" then version = bound_packages["plugin." .. name] - log_warn("Using overriden plugin version " .. version .. " of " .. name .. "!") + log_warn("Using overridden plugin version " .. version .. " of " .. name .. "!") end local plugin_id = name .. "@" .. version if type(PLUGIN_IN_MEM_CACHE[plugin_id]) == "table" then log_trace("Loading plugin from cache...") - if options.safe then - return true, PLUGIN_IN_MEM_CACHE[plugin_id] - end return PLUGIN_IN_MEM_CACHE[plugin_id] end log_trace("Plugin not loaded, loading...") @@ -128,14 +124,18 @@ function am.plugin.get(name, options) if type(SOURCES) == "table" and SOURCES["plugin." .. name] then local plugin_definition = SOURCES["plugin." .. name] --[[ @as table ]] load_dir = table.get(plugin_definition, "directory") - if not ami_assert(load_dir, "'directory' property has to be specified in case of plugin", EXIT_PKG_LOAD_ERROR, options) then - return false, nil + if not load_dir then + return nil, "plugin 'directory' not specified in SOURCES table for the plugin: " .. tostring(name) end remove_load_dir = false entrypoint = table.get(plugin_definition, "entrypoint", name .. ".lua") log_trace("Loading local plugin from path " .. load_dir) else - local plugin_definition = _get_plugin_def(name, version) + local plugin_definition, err = _get_plugin_def(name, version) + if not plugin_definition then + return nil, "failed to get plugin definition: " .. tostring(err) + end + local archive_path = os.tmpname() local ok, _ = am.cache.get_to_file("plugin-archive", plugin_id, archive_path, { sha256 = plugin_definition.sha256 }) @@ -143,12 +143,11 @@ function am.plugin.get(name, options) log_trace(not download_required and "Plugin package found..." or "Plugin package not found or verification failed, downloading... ") if download_required then - local ok = net.safe_download_file(plugin_definition.source, archive_path, { follow_redirects = true, show_default_progress = false }) - local ok2, file_hash = fs.safe_hash_file(archive_path, { hex = true, type = "sha256" }) - if not ok or not ok2 or not hash.equals(file_hash, plugin_definition.sha256, true) then - fs.safe_remove(archive_path) - ami_error("Failed to verify package integrity - " .. plugin_id .. "!", EXIT_PLUGIN_DOWNLOAD_ERROR, options) - return false, nil + local ok = net.download_file(plugin_definition.source, archive_path, { follow_redirects = true, show_default_progress = false }) + local file_hash, _ = fs.hash_file(archive_path, { hex = true, type = "sha256" }) + if not ok or not file_hash or not hash.equals(file_hash, plugin_definition.sha256, true) then + fs.remove(archive_path) + return nil, "failed to verify package integrity - " .. tostring(plugin_id) end end @@ -156,35 +155,33 @@ function am.plugin.get(name, options) os.remove(tmpfile) load_dir = tmpfile .. "_dir" - local ok, err = fs.safe_mkdirp(load_dir) + local ok, err = fs.mkdirp(load_dir) if not ok then - fs.safe_remove(archive_path) - ami_error(string.join_strings("", "Failed to create directory for plugin: ", plugin_id, " - ", err), EXIT_PLUGIN_LOAD_ERROR, options) - return false, nil + fs.remove(archive_path) + return nil, "failed to create directory for plugin: " .. tostring(plugin_id) .. " - " .. tostring(err) end - local ok, err = zip.safe_extract(archive_path, load_dir, { flatten_root_dir = true }) + local ok, err = zip.extract(archive_path, load_dir, { flatten_root_dir = true }) if not ok then - fs.safe_remove(archive_path) - ami_error(string.join_strings("", "Failed to extract plugin package: ", plugin_id, " - ", err), EXIT_PLUGIN_LOAD_ERROR, options) - return false, nil + fs.remove(archive_path) + return nil, "failed to extract plugin package: " .. tostring(plugin_id) .. " - " .. tostring(err) end local ok, err = am.cache.put_from_file(archive_path, "plugin-archive", plugin_id) - fs.safe_remove(archive_path) + fs.remove(archive_path) if not ok then log_trace("Failed to cache plugin archive - " .. tostring(err) .. "!") end entrypoint = name .. ".lua" - local ok, plugin_specs_raw = fs.safe_read_file(path.combine(load_dir, "specs.json")) - if not ok then - ok, plugin_specs_raw = fs.safe_read_file(path.combine(load_dir, "specs.hjson")) + local plugin_specs_raw, _ = fs.read_file(path.combine(load_dir, "specs.json")) + if not plugin_specs_raw then + plugin_specs_raw, _ = fs.read_file(path.combine(load_dir, "specs.hjson")) end - if ok then - local ok, plugin_specs = hjson.safe_parse(plugin_specs_raw) - if ok and type(plugin_specs.entrypoint) == "string" then + if plugin_specs_raw then + local plugin_specs, err = hjson.parse(plugin_specs_raw) + if plugin_specs and type(plugin_specs.entrypoint) == "string" then entrypoint = plugin_specs.entrypoint end end @@ -200,39 +197,15 @@ function am.plugin.get(name, options) end local ok, result = pcall(dofile, entrypoint) if remove_load_dir then - fs.safe_remove(load_dir, { recurse = true }) + fs.remove(load_dir, { recurse = true }) end if not ok then - ami_error("Failed to require plugin: " .. plugin_id .. " - " .. (type(result) == "string" and result or ""), - EXIT_PLUGIN_LOAD_ERROR, options - ) - return false, nil + return nil, "failed to require plugin: " .. plugin_id .. " - " .. (type(result) == "string" and result or "") end - if os.EOS then - local ok = os.chdir(original_cwd) - if not ok then - ami_error("Failed to chdir after plugin load", EXIT_PLUGIN_LOAD_ERROR, options) - return false, nil - end - end PLUGIN_IN_MEM_CACHE[plugin_id] = result - if options.safe then - return true, result + if os.EOS and not os.chdir(original_cwd) then + return nil, "failed to restore working directory after plugin load" end return result end - ----#DES am.plugin.safe_get ---- ----Loads plugin by name and returns it. ----@param name string ----@param options AmiGetPluginOptions? ----@return boolean, any -function am.plugin.safe_get(name, options) - if type(options) ~= "table" then - options = {} - end - options.safe = true - return am.plugin.get(name, options) -end diff --git a/src/version-info.lua b/src/version-info.lua index 9e1f05a..76e7496 100644 --- a/src/version-info.lua +++ b/src/version-info.lua @@ -2,5 +2,5 @@ local AMI_VERSION = "dev" return { VERSION = AMI_VERSION, - ABOUT = "AMI " .. AMI_VERSION .. " (C) 2025 alis.is" + ABOUT = "ami " .. AMI_VERSION .. " (C) " .. os.date("%Y") .. " alis.is" } diff --git a/tests/app/app_update/2/.version-tree.json b/tests/app/app_update/2/.version-tree.json index 3871a9f..90ca555 100644 --- a/tests/app/app_update/2/.version-tree.json +++ b/tests/app/app_update/2/.version-tree.json @@ -17,6 +17,6 @@ ], "id": "test.app", "repository": "https://raw.githubusercontent.com/cryon-io/air/master/ami/", - "version": "0.1.0", + "version": "0.2.0", "wanted_version": "latest" } \ No newline at end of file diff --git a/tests/assets/interfaces/invalid-ami/ami.lua b/tests/assets/interfaces/invalid-ami/ami.lua index c929a07..b41ddd3 100644 --- a/tests/assets/interfaces/invalid-ami/ami.lua +++ b/tests/assets/interfaces/invalid-ami/ami.lua @@ -1,12 +1,12 @@ -return { - base = "app", - commands = { - about = { - description = "ami 'about' sub command", - summary = 'Prints information about application' - action = function(_, _, _, _) - print("test app") - end - } - } +return { + base = "app", + commands = { + about = { + description = "ami 'about' sub command", + summary = 'Prints information about application' + action = function(_, _, _, _) + print("test app") + end + } + } } \ No newline at end of file diff --git a/tests/cache/2/package/definition/test.app@latest b/tests/cache/2/package/definition/test.app@latest index a150757..917b9ba 100644 --- a/tests/cache/2/package/definition/test.app@latest +++ b/tests/cache/2/package/definition/test.app@latest @@ -1,6 +1,6 @@ { last_ami_check: 9999999999 - sha256: e27b66bfb87d15fa4419a27435e883de65e7ff9c49c26b833381cadae9ef2853 - version: 0.1.0 - source: https://github.com/cryon-io/ami.test.packages/releases/download/0.1.0/test.app-0.1.0.zip + sha256: 6fd2c39b9ba181cc646fb055a449d528d9aa639ca20639eb55c1b703bf1476fa + version: 0.2.0 + source: https://github.com/alis-is/ami.test.packages/releases/download/0.2.0/test.app-0.2.0.zip } \ No newline at end of file diff --git a/tests/test/am.lua b/tests/test/am.lua index 68f95bd..4af9836 100644 --- a/tests/test/am.lua +++ b/tests/test/am.lua @@ -207,12 +207,12 @@ end test["configure_cache"] = function() local original_os_get_env = os.getenv - local original_safe_write_file = fs.safe_write_file + local original_write_file = fs.write_file local original_log_warn = log_warn local original_log_error = log_error local original_log_debug = log_debug - fs.safe_write_file = function(file_path, _) + fs.write_file = function(file_path, _) if file_path == "/var/cache/ami/.ami-test-access" then return true -- Simulating access to global cache directory end @@ -257,9 +257,9 @@ test["configure_cache"] = function() am.configure_cache(nil) test.assert(am.options.CACHE_DIR == "/var/cache/ami") - fs.safe_write_file = function(file_path, _) + fs.write_file = function(file_path, _) if file_path == "/var/cache/ami/.ami-test-access" then - return false -- Simulating no access to global cache directory + return false, "error" -- Simulating no access to global cache directory end return true end @@ -267,11 +267,13 @@ test["configure_cache"] = function() -- Test Case 5: No access to global cache, fallback to local am.configure_cache(nil) test.assert(am.options.CACHE_DIR:match("%.ami%-cache")) - test.assert(#log_messages > 1 and log_messages[2] == "DEBUG: Access to '/var/cache/ami' denied! Using local '.ami-cache' directory.") + test.assert(#log_messages > 1) + test.assert(log_messages[2]:match("access to '/var/cache/ami' denied")) + test.assert(log_messages[2]:match("using local '%.ami%-cache' directory")) -- Restore original functions os.getenv = original_os_get_env - fs.safe_write_file = original_safe_write_file + fs.write_file = original_write_file log_warn = original_log_warn log_error = original_log_error log_debug = original_log_debug diff --git a/tests/test/ami.lua b/tests/test/ami.lua index 410d06f..7db280f 100644 --- a/tests/test/ami.lua +++ b/tests/test/ami.lua @@ -33,11 +33,11 @@ local function init_ami_test(testDir, configPath, options) if options.cleanupTestDir then fs.remove(testDir, {recurse = true, content_only = true}) end - local ok + local ok, err if type(options.environment) == "string" then - ok = fs.safe_copy_file(configPath, path.combine(testDir, "app." .. options.environment .. ".hjson")) + ok, err = fs.copy_file(configPath, path.combine(testDir, "app." .. options.environment .. ".hjson")) else - ok = fs.safe_copy_file(configPath, path.combine(testDir, "app.hjson")) + ok, err = fs.copy_file(configPath, path.combine(testDir, "app.hjson")) end test.assert(ok) am.app.__set_loaded(false) @@ -179,18 +179,19 @@ end test["ami about"] = function() local test_dir = "tests/tmp/ami_test_setup" init_ami_test(test_dir, "tests/app/configs/ami_test_app@latest.hjson") - + local original_print = print local printed = "" print = function(v) printed = printed .. v end - + ami("--path="..test_dir, "-ll=info", "--cache=../ami_cache", "about") os.chdir(default_cwd) + print = original_print + test.assert(not error_called and printed:match"Test app" and printed:match"dummy%.web") - print = original_print end test["ami remove"] = function() diff --git a/tests/test/app.lua b/tests/test/app.lua index 5390f9f..c3090d2 100644 --- a/tests/test/app.lua +++ b/tests/test/app.lua @@ -134,7 +134,7 @@ test["load app details missing default config (dev env)"] = function() version = "latest" } }, true)) - test.assert(string.find(log, "Failed to load default configuration", 0, true)) + test.assert(string.find(log, "failed to load default configuration", 0, true)) end test["load app details missing env config (dev env)"] = function() @@ -173,7 +173,7 @@ test["load app details missing env config (dev env)"] = function() number = 15 } }, true)) - test.assert(string.find(log, "Failed to load environment configuration", 0, true)) + test.assert(string.find(log, "failed to load environment configuration", 0, true)) end test["load app details missing config (dev env)"] = function() @@ -206,8 +206,8 @@ test["load app details missing config (dev env)"] = function() os.chdir(default_cwd) ami_error = original_ami_error_fn log_warn = old_log_warn - test.assert(defaulterror_code == EXIT_INVALID_CONFIGURATION and not string.find(defaultLog, "app.dev.json", 0, true)) - test.assert(deverror_code == EXIT_INVALID_CONFIGURATION and string.find(devLog, "app.dev.json", 0, true)) + test.assert(defaulterror_code == EXIT_INVALID_CONFIGURATION and not string.find(defaultLog, "dev", 0, true)) + test.assert(deverror_code == EXIT_INVALID_CONFIGURATION and string.find(devLog, "dev", 0, true)) end test["load app model"] = function() @@ -225,7 +225,7 @@ test["prepare app"] = function() fs.mkdirp(test_dir) fs.remove(test_dir, { recurse = true, content_only = true }) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -241,7 +241,7 @@ test["is app installed"] = function() fs.mkdirp(test_dir) fs.remove(test_dir, { recurse = true, content_only = true }) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -258,14 +258,14 @@ test["get app version"] = function() fs.mkdirp(test_dir) fs.remove(test_dir, { recurse = true, content_only = true }) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) os.chdir(test_dir) local ok = pcall(am.app.prepare) test.assert(ok) local ok, version = pcall(am.app.get_version) - test.assert(ok and version == "0.1.0") + test.assert(ok and version == "0.2.0") os.chdir(default_cwd) end @@ -278,9 +278,9 @@ test["remove app data"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -288,8 +288,8 @@ test["remove app data"] = function() test.assert(ok) local ok = pcall(am.app.remove_data) test.assert(ok) - local ok, entries = fs.safe_read_dir("data", { recurse = true }) - test.assert(ok and #entries == 0) + local entries, _ = fs.read_dir("data", { recurse = true }) + test.assert(entries and #entries == 0) os.chdir(default_cwd) end @@ -302,9 +302,9 @@ test["remove app data (list of protected files)"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -312,8 +312,8 @@ test["remove app data (list of protected files)"] = function() test.assert(ok) local ok = pcall(am.app.remove_data, { "app.json" }) test.assert(ok) - local ok, entries = fs.safe_read_dir("data", { recurse = true }) - test.assert(ok and #entries == 1) + local entries = fs.read_dir("data", { recurse = true }) + test.assert(entries and #entries == 1) os.chdir(default_cwd) end @@ -326,9 +326,9 @@ test["remove app data (keep function)"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -338,8 +338,8 @@ test["remove app data (keep function)"] = function() return p == "app.json" end) test.assert(ok) - local ok, entries = fs.safe_read_dir("data", { recurse = true }) - test.assert(ok and #entries == 1) + local entries = fs.read_dir("data", { recurse = true }) + test.assert(entries and #entries == 1) os.chdir(default_cwd) end @@ -352,9 +352,9 @@ test["remove app"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -362,14 +362,14 @@ test["remove app"] = function() test.assert(ok) local ok = pcall(am.app.remove) test.assert(ok) - local ok, entries = fs.safe_read_dir(".", { recurse = true }) + local entries = fs.read_dir(".", { recurse = true }) local non_data_entries = {} for _, v in ipairs(entries) do if type(v) == "string" and not v:match("data/.*") then table.insert(non_data_entries, v) end end - test.assert(ok and #entries == 1) + test.assert(entries and #entries == 1) os.chdir(default_cwd) end @@ -381,9 +381,9 @@ test["remove app (list of protected files)"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -391,14 +391,14 @@ test["remove app (list of protected files)"] = function() test.assert(ok) local ok, err = pcall(am.app.remove, { "data/app.json" }) test.assert(ok) - local ok, entries = fs.safe_read_dir(".", { recurse = true }) + local entries = fs.read_dir(".", { recurse = true }) local non_data_entries = {} for _, v in ipairs(entries) do if type(v) == "string" and not v:match("data/.*") then table.insert(non_data_entries, v) end end - test.assert(ok and #entries == 3) + test.assert(entries and #entries == 3) os.chdir(default_cwd) end @@ -410,9 +410,9 @@ test["remove app (keep function)"] = function() local data_dir = path.combine(test_dir, "data") fs.mkdirp(data_dir) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(test_dir, "app.json")) test.assert(ok) - local ok = fs.safe_copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) + local ok = fs.copy_file("tests/app/configs/simple_test_app.json", path.combine(data_dir, "app.json")) test.assert(ok) os.chdir(test_dir) @@ -422,14 +422,14 @@ test["remove app (keep function)"] = function() return path.normalize(p, "unix", { endsep = "leave"}) == "data/app.json" end) test.assert(ok) - local ok, entries = fs.safe_read_dir(".", { recurse = true }) + local entries = fs.read_dir(".", { recurse = true }) local non_data_entries = {} for _, v in ipairs(entries) do if type(v) == "string" and not v:match("data/.*") then table.insert(non_data_entries, v) end end - test.assert(ok and #entries == 3) + test.assert(entries and #entries == 3) os.chdir(default_cwd) end @@ -459,8 +459,9 @@ test["is update available alternative channel"] = function() os.chdir(test_dir) local ok = pcall(am.app.load_configuration) - local is_available, _, version = am.app.is_update_available() - test.assert(is_available and version == "0.0.3-beta") + local is_available, available_versions = am.app.is_update_available() + test.assert(is_available) + test.assert(available_versions["test.app"] == "0.0.3-beta") os.chdir(default_cwd) end diff --git a/tests/test/cli.lua b/tests/test/cli.lua index cf01011..0e50bf7 100644 --- a/tests/test/cli.lua +++ b/tests/test/cli.lua @@ -6,7 +6,7 @@ require "tests.test_init" test["parse args"] = function() local old_args = args args = {} - + -- // TODO: args = old_args end @@ -227,7 +227,6 @@ test["process cli (external)"] = function() recordedExitCode = exit_code end - local cli = { title = "test cli2", description = "test cli description", @@ -294,6 +293,7 @@ test["process cli (external)"] = function() local arg_list_init2 = is_unix_like and { "test2", "-c" } or { "test2", "/c" } local arg_list = util.merge_arrays(arg_list_init2, { "exit 0" }) local ok, result = pcall(am.execute, cli, arg_list) + test.assert(ok and result == 0) local arg_list = util.merge_arrays(arg_list_init, { "exit 0" }) @@ -533,7 +533,7 @@ test["show cli help (include_options_in_usage = false)"] = function() test.assert(ok and not result:match("%[%-%-to%] %[%-%-to2%]") and result:match("Usage:")) end -test["show cli help (printUsage = false)"] = function() +test["show cli help (print_usage = false)"] = function() local cli = { title = "test cli", description = "test cli description", @@ -565,7 +565,7 @@ test["show cli help (printUsage = false)"] = function() local ok, result = collect_printout( function() - am.print_help(cli, { printUsage = false }) + am.print_help(cli, { print_usage = false }) end ) test.assert(ok and not result:match("%[%-%-to%] %[%-%-to2%]") and not result:match("Usage:")) diff --git a/tests/test/interfaces.lua b/tests/test/interfaces.lua index 3cdeed2..98d3dbc 100644 --- a/tests/test/interfaces.lua +++ b/tests/test/interfaces.lua @@ -29,21 +29,21 @@ test["load invalid ami"] = function() am.execute("about") os.chdir(default_cwd) print = default_print - test.assert(result:match("Failed to load entrypoint:")) + test.assert(result:match("failed to load entrypoint:")) end test["load valid ami violating app starndard"] = function() os.chdir("tests/assets/interfaces/valid-ami-violating") local default_print = print local result - print = function(msg) + print = function(msg) result = msg end am.__reload_interface() am.execute("about") os.chdir(default_cwd) print = default_print - test.assert(result:match("Violation of AMI@app standard!")) + test.assert(result:match("violation of ami@app standard")) end if not TEST then diff --git a/tests/test/pkg.lua b/tests/test/pkg.lua index 7a7ca68..1016f1b 100644 --- a/tests/test/pkg.lua +++ b/tests/test/pkg.lua @@ -13,7 +13,8 @@ test["normalize pkg type"] = function() local pkg_type = { id = "test.app" } - ami_pkg.normalize_pkg_type(pkg_type) + pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) test.assert(pkg_type.version == "latest") test.assert(pkg_type.repository == am.options.DEFAULT_REPOSITORY_URL) end @@ -23,7 +24,8 @@ test["normalize pkg type (specific version)"] = function() id = "test.app", version = "0.0.2" } - ami_pkg.normalize_pkg_type(pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) test.assert(pkg_type.version == "0.0.2") end @@ -33,7 +35,8 @@ test["normalize pkg type (specific repository)"] = function() id = "test.app", repository = custom_repo } - ami_pkg.normalize_pkg_type(pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) test.assert(pkg_type.repository == custom_repo) end @@ -44,24 +47,25 @@ test["prepare pkg from remote"] = function() local pkg_type = { id = "test.app" } - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, model_info, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) -- file list check - test.assert(file_list["specs.json"].id == "test.app") - test.assert(file_list["__test/assets/test.template.txt"].id == "test.app") - test.assert(file_list["__test/assets/test2.template.txt"].id == "test.base2") + test.assert(result.files["specs.json"].id == "test.app") + test.assert(result.files["__test/assets/test.template.txt"].id == "test.app") + test.assert(result.files["__test/assets/test2.template.txt"].id == "test.base2") -- model check - local test_base_2_pkg_hash = file_list["__test/assets/test2.template.txt"].source - local test_app_pkg_hash = file_list["__test/assets/test.template.txt"].source - test.assert(model_info.model.source ~= test_app_pkg_hash and model_info.model.source ~= test_base_2_pkg_hash) - test.assert(model_info.extensions[1].source == test_base_2_pkg_hash) - test.assert(model_info.extensions[2].source == test_app_pkg_hash) + local test_base_2_pkg_hash = result.files["__test/assets/test2.template.txt"].source + local test_app_pkg_hash = result.files["__test/assets/test.template.txt"].source + test.assert(result.model.model.source ~= test_app_pkg_hash and result.model.model.source ~= test_base_2_pkg_hash) + test.assert(result.model.extensions[1].source == test_base_2_pkg_hash) + test.assert(result.model.extensions[2].source == test_app_pkg_hash) -- version tree check - test.assert(#version_tree.dependencies == 2) - test.assert(version_tree.dependencies[1].id == "test.base") - test.assert(version_tree.dependencies[2].id == "test.base2") - test.assert(version_tree.id == "test.app") + test.assert(#result.version_tree.dependencies == 2) + test.assert(result.version_tree.dependencies[1].id == "test.base") + test.assert(result.version_tree.dependencies[2].id == "test.base2") + test.assert(result.version_tree.id == "test.app") end test["prepare pkg from local cache"] = function() @@ -71,22 +75,23 @@ test["prepare pkg from local cache"] = function() id = "test.app", repository = "non existing repository" } - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, model_info, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) -- file list check - test.assert(file_list["specs.json"].id == "test.app") - test.assert(file_list["__test/assets/test.template.txt"].id == "test.app") - test.assert(file_list["__test/assets/test2.template.txt"].id == "test.base2") + test.assert(result.files["specs.json"].id == "test.app") + test.assert(result.files["__test/assets/test.template.txt"].id == "test.app") + test.assert(result.files["__test/assets/test2.template.txt"].id == "test.base2") -- model check - test.assert(model_info.model.pkg_id == "33e4e7e3f2e8d0651ff498036cc2098910a950f9b3eed55aa26b9d95d75338d0") - test.assert(model_info.extensions[1].pkg_id == "1e05f3895e0bbfe9c3e4608abb9d5366ff64e93e78e6217a69cc875390e71d7f") - test.assert(model_info.extensions[2].pkg_id == "e27b66bfb87d15fa4419a27435e883de65e7ff9c49c26b833381cadae9ef2853") + test.assert(result.model.model.pkg_id == "33e4e7e3f2e8d0651ff498036cc2098910a950f9b3eed55aa26b9d95d75338d0") + test.assert(result.model.extensions[1].pkg_id == "1e05f3895e0bbfe9c3e4608abb9d5366ff64e93e78e6217a69cc875390e71d7f") + test.assert(result.model.extensions[2].pkg_id == "6fd2c39b9ba181cc646fb055a449d528d9aa639ca20639eb55c1b703bf1476fa") -- version tree check - test.assert(#version_tree.dependencies == 2) - test.assert(version_tree.dependencies[1].id == "test.base") - test.assert(version_tree.dependencies[2].id == "test.base2") - test.assert(version_tree.id == "test.app") + test.assert(#result.version_tree.dependencies == 2) + test.assert(result.version_tree.dependencies[1].id == "test.base") + test.assert(result.version_tree.dependencies[2].id == "test.base2") + test.assert(result.version_tree.id == "test.app") end test["prepare specific pkg from remote"] = function() @@ -97,24 +102,25 @@ test["prepare specific pkg from remote"] = function() id = "test.app", version = "0.0.2" } - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, model_nfo, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) -- file list check - test.assert(file_list["specs.json"].id == "test.app") - test.assert(file_list["__test/assets/test.template.txt"].id == "test.app") - test.assert(file_list["__test/assets/test2.template.txt"].id == "test.base2") + test.assert(result.files["specs.json"].id == "test.app") + test.assert(result.files["__test/assets/test.template.txt"].id == "test.app") + test.assert(result.files["__test/assets/test2.template.txt"].id == "test.base2") -- model check - local test_base_2_pkg_hash = file_list["__test/assets/test2.template.txt"].source - local test_app_pkg_hash = file_list["__test/assets/test.template.txt"].source - test.assert(model_nfo.model.source ~= test_app_pkg_hash and model_nfo.model.source ~= test_base_2_pkg_hash) - test.assert(model_nfo.extensions[1].source == test_base_2_pkg_hash) - test.assert(model_nfo.extensions[2].source == test_app_pkg_hash) + local test_base_2_pkg_hash = result.files["__test/assets/test2.template.txt"].source + local test_app_pkg_hash = result.files["__test/assets/test.template.txt"].source + test.assert(result.model.model.source ~= test_app_pkg_hash and result.model.model.source ~= test_base_2_pkg_hash) + test.assert(result.model.extensions[1].source == test_base_2_pkg_hash) + test.assert(result.model.extensions[2].source == test_app_pkg_hash) -- version tree check - test.assert(#version_tree.dependencies == 2) - test.assert(version_tree.dependencies[1].id == "test.base") - test.assert(version_tree.dependencies[2].id == "test.base2") - test.assert(version_tree.id == "test.app") + test.assert(#result.version_tree.dependencies == 2) + test.assert(result.version_tree.dependencies[1].id == "test.base") + test.assert(result.version_tree.dependencies[2].id == "test.base2") + test.assert(result.version_tree.id == "test.app") end test["prepare specific pkg from local cache"] = function() @@ -125,22 +131,23 @@ test["prepare specific pkg from local cache"] = function() repository = "non existing repository", version = "0.0.2" } - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, model_info, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) -- file list check - test.assert(file_list["specs.json"].id == "test.app") - test.assert(file_list["__test/assets/test.template.txt"].id == "test.app") - test.assert(file_list["__test/assets/test2.template.txt"].id == "test.base2") + test.assert(result.files["specs.json"].id == "test.app") + test.assert(result.files["__test/assets/test.template.txt"].id == "test.app") + test.assert(result.files["__test/assets/test2.template.txt"].id == "test.base2") -- model check - test.assert(model_info.model.pkg_id == "33e4e7e3f2e8d0651ff498036cc2098910a950f9b3eed55aa26b9d95d75338d0") - test.assert(model_info.extensions[1].pkg_id == "1e05f3895e0bbfe9c3e4608abb9d5366ff64e93e78e6217a69cc875390e71d7f") - test.assert(model_info.extensions[2].pkg_id == "d0b5a56925682c70f5e46d99798e16cb791081124af89c780ed40fb97ab589c5") + test.assert(result.model.model.pkg_id == "33e4e7e3f2e8d0651ff498036cc2098910a950f9b3eed55aa26b9d95d75338d0") + test.assert(result.model.extensions[1].pkg_id == "1e05f3895e0bbfe9c3e4608abb9d5366ff64e93e78e6217a69cc875390e71d7f") + test.assert(result.model.extensions[2].pkg_id == "d0b5a56925682c70f5e46d99798e16cb791081124af89c780ed40fb97ab589c5") -- version tree check - test.assert(#version_tree.dependencies == 2) - test.assert(version_tree.dependencies[1].id == "test.base") - test.assert(version_tree.dependencies[2].id == "test.base2") - test.assert(version_tree.id == "test.app") + test.assert(#result.version_tree.dependencies == 2) + test.assert(result.version_tree.dependencies[1].id == "test.base") + test.assert(result.version_tree.dependencies[2].id == "test.base2") + test.assert(result.version_tree.id == "test.app") end test["prepare pkg no integrity checks"] = function() @@ -151,22 +158,23 @@ test["prepare pkg no integrity checks"] = function() id = "test.app", repository = "non existing repository" } - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, model_info, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) -- file list check - test.assert(file_list["specs.json"].id == "test.app") - test.assert(file_list["__test/assets/test.template.txt"].id == "test.app") - test.assert(file_list["__test/assets/test2.template.txt"].id == "test.base2") + test.assert(result.files["specs.json"].id == "test.app") + test.assert(result.files["__test/assets/test.template.txt"].id == "test.app") + test.assert(result.files["__test/assets/test2.template.txt"].id == "test.base2") -- model check - test.assert(model_info.model.pkg_id == "9adfc4bbeee214a8358b40e146a8b44df076502c8f8ebcea8f2e96bae791bb69") - test.assert(model_info.extensions[1].pkg_id == "a2bc34357589128a1e1e8da34d932931b52f09a0c912859de9bf9d87570e97e9") - test.assert(model_info.extensions[2].pkg_id == "d0b5a56925682c70f5e46d99798e16cb791081124af89c780ed40fb97ab589c5") + test.assert(result.model.model.pkg_id == "9adfc4bbeee214a8358b40e146a8b44df076502c8f8ebcea8f2e96bae791bb69") + test.assert(result.model.extensions[1].pkg_id == "a2bc34357589128a1e1e8da34d932931b52f09a0c912859de9bf9d87570e97e9") + test.assert(result.model.extensions[2].pkg_id == "d0b5a56925682c70f5e46d99798e16cb791081124af89c780ed40fb97ab589c5") -- version tree check - test.assert(#version_tree.dependencies == 2) - test.assert(version_tree.dependencies[1].id == "test.base") - test.assert(version_tree.dependencies[2].id == "test.base2") - test.assert(version_tree.id == "test.app") + test.assert(#result.version_tree.dependencies == 2) + test.assert(result.version_tree.dependencies[1].id == "test.base") + test.assert(result.version_tree.dependencies[2].id == "test.base2") + test.assert(result.version_tree.id == "test.app") am.options.NO_INTEGRITY_CHECKS = false end @@ -177,9 +185,10 @@ test["prepare pkg from alternative channel"] = function() id = "test.app", channel = "beta" } - ami_pkg.normalize_pkg_type(pkg_type) - local _, _, _, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) - test.assert(version_tree.version:match(".+-beta")) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) + test.assert(result.version_tree.version:match(".+-beta")) end @@ -190,9 +199,10 @@ test["prepare pkg from non existing alternative channel"] = function() id = "test.app", channel = "alpha" } - ami_pkg.normalize_pkg_type(pkg_type) - local _, _, _, version_tree = pcall(ami_pkg.prepare_pkg, pkg_type) - test.assert(not version_tree.version:match(".+-alpha")) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) + test.assert(not result.version_tree.version:match(".+-alpha")) end test["unpack layers"] = function() @@ -206,19 +216,20 @@ test["unpack layers"] = function() fs.remove(test_dir, {recurse = true, content_only = true}) os.chdir(test_dir) - ami_pkg.normalize_pkg_type(pkg_type) - local result, file_list, _, _ = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) - local result = pcall(ami_pkg.unpack_layers, file_list) + local result = ami_pkg.unpack_layers(result.files) test.assert(result) - local ok, test_hash = fs.safe_hash_file(".ami-templates/__test/assets/test.template.txt", {hex = true}) - test.assert(ok and test_hash == "c2881a3b33316d5ba77075715601114092f50962d1935582db93bb20828fdae5") - local ok, test2_hash = fs.safe_hash_file(".ami-templates/__test/assets/test2.template.txt", {hex = true}) - test.assert(ok and test2_hash == "172fb97f3321e9e3616ada32fb5f9202b3917f5adcf4b67957a098a847e2f12c") - local ok, specs_hash = fs.safe_hash_file("specs.json", {hex = true}) - test.assert(ok and specs_hash == "3aaa99ed2b16ed97e85d9fb7e0666986b230e5dcbe2e04e513b99e7f9dc8810a") + local test_hash = fs.hash_file(".ami-templates/__test/assets/test.template.txt", {hex = true}) + test.assert(test_hash == "c2881a3b33316d5ba77075715601114092f50962d1935582db93bb20828fdae5") + local test2_hash = fs.hash_file(".ami-templates/__test/assets/test2.template.txt", {hex = true}) + test.assert(test2_hash == "172fb97f3321e9e3616ada32fb5f9202b3917f5adcf4b67957a098a847e2f12c") + local specs_hash = fs.hash_file("specs.json", {hex = true}) + test.assert(specs_hash == "f30b06c0ce277fd0ec8c1be82db4287a387fd466e73c485c5d7f8935b0f55ee1") os.chdir(default_cwd) end @@ -234,15 +245,16 @@ test["generate model"] = function() fs.remove(test_dir, {recurse = true, content_only = true}) os.chdir(test_dir) - ami_pkg.normalize_pkg_type(pkg_type) - local result, _, model_info, _ = pcall(ami_pkg.prepare_pkg, pkg_type) + local pkg_type, err = ami_pkg.normalize_pkg_type(pkg_type) + test.assert(pkg_type) + local result, _ = ami_pkg.prepare_pkg(pkg_type) test.assert(result) - local result = pcall(ami_pkg.generate_model, model_info) + local result, _ = ami_pkg.generate_model(result.model) test.assert(result) - local ok, model_hash = fs.safe_hash_file("model.lua", {hex = true}) - test.assert(ok and model_hash == "58517f9f584336674cea455165cd9b1d7d8bccfc49bc7a1aad870e5d402aef9a") + local model_hash = fs.hash_file("model.lua", {hex = true}) + test.assert(model_hash == "11f2eb0c5638019399762d68c07b1f8c45105c854c6322740892f987b2f220b9") os.chdir(default_cwd) end @@ -252,9 +264,9 @@ test["is update available"] = function() id = "test.app", wanted_version = "latest" } - local isAvailable, id, version = ami_pkg.is_pkg_update_available(pkg, "0.0.0") + local isAvailable = ami_pkg.is_pkg_update_available(pkg, "0.0.0") test.assert(isAvailable) - local isAvailable, id, version = ami_pkg.is_pkg_update_available(pkg, "100.0.0") + local isAvailable = ami_pkg.is_pkg_update_available(pkg, "100.0.0") test.assert(not isAvailable) end @@ -265,11 +277,11 @@ test["is update available from alternative channel"] = function() wanted_version = "latest", channel = "beta" } - local isAvailable, id, version = ami_pkg.is_pkg_update_available(pkg, "0.0.0") + local isAvailable = ami_pkg.is_pkg_update_available(pkg, "0.0.0") test.assert(isAvailable) - local isAvailable, id, version = ami_pkg.is_pkg_update_available(pkg, "0.0.3-beta") + local isAvailable = ami_pkg.is_pkg_update_available(pkg, "0.0.3-beta") test.assert(not isAvailable) - local isAvailable, id, version = ami_pkg.is_pkg_update_available(pkg, "100.0.0") + local isAvailable = ami_pkg.is_pkg_update_available(pkg, "100.0.0") test.assert(not isAvailable) end diff --git a/tests/test/plugin.lua b/tests/test/plugin.lua index dc4dc89..096a703 100644 --- a/tests/test/plugin.lua +++ b/tests/test/plugin.lua @@ -8,8 +8,8 @@ test["load cached plugin"] = function() local plugin, _ = am.plugin.get("test") test.assert(plugin.test() == "cached test plugin") am.plugin.__erase_cache() - local ok, plugin = am.plugin.safe_get("test") - test.assert(ok and plugin.test() == "cached test plugin") + local plugin = am.plugin.get("test") + test.assert(plugin and plugin.test() == "cached test plugin") end test["load remote plugin"] = function() @@ -19,19 +19,17 @@ test["load remote plugin"] = function() local plugin = am.plugin.get("test") test.assert(plugin.test() == "remote test plugin") am.plugin.__erase_cache() - local ok, plugin = am.plugin.safe_get("test") - test.assert(ok and plugin.test() == "remote test plugin") + local plugin = am.plugin.get("test") + test.assert(plugin and plugin.test() == "remote test plugin") end test["load from in mem cache"] = function() am.plugin.__remove_cached("test") am.options.CACHE_DIR = "tests/cache/1" local plugin = am.plugin.get("test") - plugin.tag = "taged" + plugin.tag = "tagged" local plugin2 = am.plugin.get("test") - test.assert(plugin2.tag == "taged") - local ok, plugin2 = am.plugin.safe_get("test") - test.assert(ok and plugin2.tag == "taged") + test.assert(plugin2.tag == "tagged") end test["load specific version"] = function() @@ -41,8 +39,8 @@ test["load specific version"] = function() local plugin = am.plugin.get("test", { version = "0.0.1" }) test.assert(plugin.test() == "remote test plugin") am.plugin.__erase_cache() - local ok, plugin = am.plugin.safe_get("test", { version = "0.0.1" }) - test.assert(ok and plugin.test() == "remote test plugin") + local plugin = am.plugin.get("test", { version = "0.0.1" }) + test.assert(plugin and plugin.test() == "remote test plugin") end test["load specific cached version"] = function() @@ -51,8 +49,8 @@ test["load specific cached version"] = function() local plugin = am.plugin.get("test", { version = "0.0.1" }) test.assert(plugin.test() == "cached test plugin") am.plugin.__erase_cache() - local ok, plugin = am.plugin.safe_get("test", { version = "0.0.1" }) - test.assert(ok and plugin.test() == "cached test plugin") + local plugin = am.plugin.get("test", { version = "0.0.1" }) + test.assert(plugin and plugin.test() == "cached test plugin") end test["load from local sources"] = function() @@ -66,8 +64,8 @@ test["load from local sources"] = function() test.assert(plugin.test() == "cached test plugin") am.plugin.__remove_cached("test", "0.0.1") - local ok, plugin = am.plugin.safe_get("test", { version = "0.0.1" }) - test.assert(ok and plugin.test() == "cached test plugin") + local plugin = am.plugin.get("test", { version = "0.0.1" }) + test.assert(plugin and plugin.test() == "cached test plugin") SOURCES = nil end diff --git a/tests/test/tpl.lua b/tests/test/tpl.lua index 303a636..931dd57 100644 --- a/tests/test/tpl.lua +++ b/tests/test/tpl.lua @@ -24,8 +24,8 @@ test["template rendering"] = function() local test_cwd = os.cwd() os.chdir("tests/app/templates/1") am.app.render() - local ok, file_hash = fs.safe_hash_file("test.txt", { hex = true }) - test.assert(ok and file_hash == "079f7524d0446d2fe7a5ce0476f2504a153fcd1e556492a54d05a48b0c204c64") + local file_hash = fs.hash_file("test.txt", { hex = true }) + test.assert(file_hash == "079f7524d0446d2fe7a5ce0476f2504a153fcd1e556492a54d05a48b0c204c64") local ok = os.chdir(test_cwd) test.assert(ok) end diff --git a/tests/test_init.lua b/tests/test_init.lua index 92107bd..34044a9 100644 --- a/tests/test_init.lua +++ b/tests/test_init.lua @@ -1,5 +1,5 @@ ---@diagnostic disable: undefined-global, lowercase-global -hjson = util.generate_safe_functions(require"hjson") +hjson = require"hjson" TEST_MODE = true diff --git a/tests/vendor/u-test.lua b/tests/vendor/u-test.lua index 79f3112..f5e78ea 100644 --- a/tests/vendor/u-test.lua +++ b/tests/vendor/u-test.lua @@ -7,7 +7,7 @@ if eli_fs.exists(U_TEST_FILE) then else print "downloading u-test" local ok = - eli_net.safe_download_file("https://raw.githubusercontent.com/cryi/u-test/master/u-test.lua", U_TEST_FILE) + eli_net.download_file("https://raw.githubusercontent.com/cryi/u-test/master/u-test.lua", U_TEST_FILE) assert(ok, "Failed to download u-test") end