From 69cb1dbccf8bb840377836bc28f6e143f06161fc Mon Sep 17 00:00:00 2001 From: v Date: Sun, 5 Jan 2025 23:40:16 +0100 Subject: [PATCH 1/2] implementation without testing --- src/am.lua | 21 ++- src/ami/app.lua | 240 ++++++++++++++++++++++------ src/ami/exit-codes.lua | 1 + src/ami/internals/interface/app.lua | 29 ++++ src/ami/internals/types.lua | 4 +- src/ami/internals/util.lua | 120 ++++++++++++++ tests/app/full/1/ami.lua | 0 tests/app/full/1/app.hjson | 0 tests/app/full/1/bin/test.sh | 3 + tests/app/full/1/data/database.txt | 5 + tests/test/app.lua | 48 ++++++ tests/test/internals/util.lua | 38 +++++ 12 files changed, 461 insertions(+), 48 deletions(-) create mode 100644 tests/app/full/1/ami.lua create mode 100644 tests/app/full/1/app.hjson create mode 100644 tests/app/full/1/bin/test.sh create mode 100644 tests/app/full/1/data/database.txt create mode 100644 tests/test/internals/util.lua diff --git a/src/am.lua b/src/am.lua index 5da6c32..18b8566 100644 --- a/src/am.lua +++ b/src/am.lua @@ -41,6 +41,7 @@ am.options = initialize_options(get_default_options()) ---@param cmd string|string[]|AmiCli ---@param args string[] | nil +---@return AmiCli, string[] local function get_interface(cmd, args) local interface = cmd if util.is_array(cmd) then @@ -51,7 +52,7 @@ local function get_interface(cmd, args) local commands = table.get(am, { "__interface", "commands" }, {}) interface = commands[cmd] or interface end - return interface, args + return interface --[[@as AmiCli]], args or {} end ---#DES am.execute @@ -66,6 +67,24 @@ function am.execute(cmd, args) return cli.process(interface, args) end +---#DES am.execute_action +--- +---Executes action of specifed cmd with specified options, command and args +---@param cmd string|string[]|AmiCli +---@param options table? +---@param command string? +---@param args string[]? +function am.execute_action(cmd, options, command, args) + local interface = get_interface(cmd) + ami_assert(type(interface) == "table", "no valid command provided", EXIT_CLI_CMD_UNKNOWN) + local action = interface.action + if type(action) ~= "table" then + ami_error("no valid action provided", EXIT_CLI_CMD_UNKNOWN) + return -- just to make linter happy + end + return action(options, command, args, interface) +end + ---@type string[] am.__args = {} diff --git a/src/ami/app.lua b/src/ami/app.lua index a28a366..0aa8ac2 100644 --- a/src/ami/app.lua +++ b/src/ami/app.lua @@ -14,8 +14,9 @@ -- along with this program. If not, see . ---@diagnostic disable-next-line: different-requires -local ami_pkg = require "ami.internals.pkg" -local ami_tpl = require "ami.internals.tpl" +local ami_pkg = require"ami.internals.pkg" +local ami_tpl = require"ami.internals.tpl" +local ami_internals_util = require"ami.internals.util" am.app = {} @@ -42,7 +43,7 @@ local function normalize_app_pkg_type(pkg) pkg.type = { id = pkg.type, repository = am.options.DEFAULT_REPOSITORY_URL, - version = "latest" + version = "latest", } end local pkg_type = pkg.type @@ -110,7 +111,7 @@ local function load_configuration_content(path) local env_ok, env_config local default_ok, 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 candidates = table.map(am.options.APP_CONFIGURATION_ENVIRONMENT_CANDIDATES, function (v) local result = string.interpolate(v, { environment = am.options.ENVIRONMENT }) return result end) @@ -118,9 +119,11 @@ local function load_configuration_content(path) if not env_ok then log_warn("Failed to load environment configuration - " .. tostring(env_config)) end end - ami_assert(default_ok or env_ok, "Failed to load app.h/json - " .. tostring(default_config), EXIT_INVALID_CONFIGURATION) + ami_assert(default_ok or env_ok, "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 - return hjson.stringify_to_json(util.merge_tables(default_ok and default_config --[[@as table]] or {}, env_ok and env_config --[[@as table]] or {}, + 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 }) end @@ -162,9 +165,9 @@ function am.app.get_configuration(path, default) load_configuration() end if path ~= nil then - return table.get(am.app.get("configuration"), path, default) + return table.get(am.app.get"configuration", path, default) end - local result = am.app.get("configuration") + local result = am.app.get"configuration" if result == nil then return default end @@ -187,7 +190,7 @@ end ---Loads app model from model.lua function am.app.load_model() local path = "model.lua" - log_trace("Loading application model...") + log_trace"Loading application model..." if not fs.exists(path) then return end @@ -252,7 +255,7 @@ function am.app.set_model(value, path, options) if options.merge and type(original) == "table" and type(value) == "table" then value = util.merge_tables(original, value, options.overwrite) end - table.set(__model, path--[[@as string|string[] ]] , value) + table.set(__model, path --[[@as string|string[] ]], value) end end @@ -268,8 +271,8 @@ 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")) + log_info"Preparing the application..." + local file_list, model_info, version_tree, tmp_pkgs = ami_pkg.prepare_pkg(am.app.get"type") ami_pkg.unpack_layers(file_list) ami_pkg.generate_model(model_info) @@ -300,21 +303,21 @@ end ---Returns true if there is update available for any of related packages ---@return boolean function am.app.is_update_available() - local ok, version_tree_raw = fs.safe_read_file(".version-tree.json") + 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 - log_trace("Using .version-tree.json for update availability check.") + log_trace"Using .version-tree.json for update availability check." return ami_pkg.is_pkg_update_available(version_tree) end end - log_warn("Version tree not found. Running update check against specs...") - local ok, specs_raw = fs.safe_read_file("specs.json") + 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) + return ami_pkg.is_pkg_update_available(am.app.get"type", specs and specs.version) end ---#DES am.app.get_version @@ -322,14 +325,14 @@ end ---Returns app version ---@return string|'"unknown"' function am.app.get_version() - local ok, version_tree_raw = fs.safe_read_file(".version-tree.json") + 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 return version_tree.version end end - log_warn("Version tree not found. Can not get the version...") + log_warn"Version tree not found. Can not get the version..." return "unknown" end @@ -338,16 +341,16 @@ end ---Returns app type ---@return string function am.app.get_type() - if type(am.app.get("type")) ~= "table" then - return am.app.get("type") + if type(am.app.get"type") ~= "table" then + return am.app.get"type" end -- we want to get app type nicely formatted - local result = am.app.get({"type", "id"}) - local version = am.app.get({"type", "version"}) + local result = am.app.get{ "type", "id" } + local version = am.app.get{ "type", "version" } if type(version) == "string" then result = result .. "@" .. version end - local repository = am.app.get({"type", "repository"}) + local repository = am.app.get{ "type", "repository" } if type(repository) == "string" and repository ~= am.options.DEFAULT_REPOSITORY_URL then result = result .. "[" .. repository .. "]" end @@ -362,21 +365,25 @@ function am.app.remove_data(keep) local protected_files = {} if type(keep) == "table" then -- inject keep files into protected files - table.reduce(keep, function(acc, v) + table.reduce(keep, function (acc, v) acc[path.normalize(v, "unix", { endsep = "leave" })] = true return acc end, protected_files) end - local ok, err = fs.safe_remove("data", { recurse = true, content_only = true, keep = function(p, _) - local normalized_path = path.normalize(p, "unix", { endsep = "leave" }) - if protected_files[normalized_path] then - return true + local ok, err = fs.safe_remove("data", { + recurse = true, + content_only = true, + keep = function (p, _) + local normalized_path = path.normalize(p, "unix", { endsep = "leave" }) + if protected_files[normalized_path] then + return true + end + if type(keep) == "function" then + return keep(p) + end end - if type(keep) == "function" then - return keep(p) - end - end }) + }) ami_assert(ok, "Failed to remove app data - " .. tostring(err) .. "!", EXIT_RM_DATA_ERROR) end @@ -396,33 +403,176 @@ function am.app.remove(keep) local protected_files = get_protected_files() if type(keep) == "table" then -- inject keep files into protected files - table.reduce(keep, function(acc, v) + table.reduce(keep, function (acc, v) acc[path.normalize(v, "unix", { endsep = "leave" })] = true return acc end, protected_files) end - local ok, err = fs.safe_remove(".", { recurse = true, content_only = true, keep = function(p, fp) - local normalized_path = path.normalize(p, "unix", { endsep = "leave" }) - if protected_files[normalized_path] then - return true - end - if type(keep) == "function" then - return keep(p, fp) + local ok, err = fs.safe_remove(".", { + recurse = true, + content_only = true, + keep = function (p, fp) + local normalized_path = path.normalize(p, "unix", { endsep = "leave" }) + if protected_files[normalized_path] then + return true + end + if type(keep) == "function" then + return keep(p, fp) + end end - end }) + }) ami_assert(ok, "Failed to remove app - " .. tostring(err) .. "!", EXIT_RM_ERROR) end + ---#DES am.app.remove --- ---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") + 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 = am.app.get({"type", "version"}) - return am.app.get({"type", "id"}) == version_tree.id and (version == "latest" or version == version_tree.version) + local version = am.app.get{ "type", "version" } + return am.app.get{ "type", "id" } == version_tree.id and (version == "latest" or version == version_tree.version) +end + +-- packing + +local PACKER_VERSION = 1 +local PACKER_METADATA_FILE = "__ami_packed_metadata.json" + +---@alias PathFilteringMode "whitelist" | "blacklist" +---@alias PathMatchingMode "plain" | "glob" | "lua-pattern" + +---@class PackOptions +---@field mode "full" | "light" | nil +---@field paths string[]? +---@field path_filtering_mode "whitelist" | "blacklist" | nil +---@field path_matching_mode PathMatchingMode | nil +---@field destination string | nil + +local function normalize_separators(p) + -- check if windows + local is_win = string.match(package.config, "\\") + if not is_win then + return p + end + + return p:gsub("\\", "/") -- replace \ with / +end + +local DEFAULT_PATHS = { + ["full"] = { + ["whitelist"] = {}, + ["blacklist"] = { "data" }, + }, + ["light"] = { + ["whitelist"] = {}, + ["blacklist"] = { "data" }, + }, +} + +local REQUIRED_PATHS = { + ami_internals_util.glob_to_lua_pattern("app.?json") +} + +---@param path string +---@param patterns string[] +---@param mode PathMatchingMode +local function path_matches(path, patterns, mode) + for _, pattern in ipairs(patterns) do + if mode == "plain" then + return path == pattern + elseif mode == "glob" then + pattern = ami_internals_util.glob_to_lua_pattern(pattern) + return string.match(path, pattern) + elseif mode == "lua-pattern" then + return string.match(path, pattern) + end + end +end + +---@param options PackOptions +function am.app.pack(options) + if type(options) ~= "table" then + options = {} + end + + local mode = options.mode or "light" + ami_assert(table.includes({ "full", "light" }, mode), "invalid mode - " .. tostring(mode), EXIT_CLI_ARG_VALIDATION_ERROR) + local path_filtering_mode = options.path_filtering_mode or "blacklist" + ami_assert(table.includes({ "whitelist", "blacklist" }, path_filtering_mode), "invalid path filtering mode - " .. tostring(path_filtering_mode), EXIT_CLI_ARG_VALIDATION_ERROR) + local path_matching_mode = options.path_matching_mode or "glob" + ami_assert(table.includes({ "plain", "glob", "lua-pattern" }, path_matching_mode), "invalid path matching mode - " .. tostring(path_matching_mode), EXIT_CLI_ARG_VALIDATION_ERROR) + + local paths = options.paths or DEFAULT_PATHS[mode][path_filtering_mode] + + if path_filtering_mode == "whitelist" and (not table.is_array(paths) or #paths == 0) then + ami_error("empty whitelist paths", EXIT_CLI_ARG_VALIDATION_ERROR) + end + + local destination_path = options.destination or "app.zip" + + zip.compress(os.cwd() or ".", destination_path, { + recurse = true, + filter = function (p) + p = normalize_separators(p) + + if path_matches(p, REQUIRED_PATHS, "lua-pattern") then + return true + end + + if path_filtering_mode == "whitelist" then + return path_matches(p, paths, path_matching_mode) + end + + if path_filtering_mode == "blacklist" then + return not path_matches(p, paths, path_matching_mode) + end + return false + end + }) + -- add __ami_packed_metadata.json + local archive = zip.open_archive(destination_path) + zip.add_to_archive(archive, PACKER_METADATA_FILE, "string", hjson.stringify_to_json{ + VERSION = PACKER_VERSION, + options = table.filter(options, function (k, v) + return table.includes({ + "paths", + "path_filtering_mode", + "path_matching_mode", + }, k) -- only save options that are not related to paths + end) + }) + ---@diagnostic disable-next-line: undefined-field + archive:close() + + log_success("app packed successfully into '" .. destination_path .. "'") +end + +---@param path string +function am.app.unpack(path) + local ok, metadata = zip.safe_extract_string(path, PACKER_METADATA_FILE) + ami_assert(ok, "failed to extract metadata from packed app - " .. tostring(metadata), 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) + + ami_assert(metadata.VERSION == PACKER_VERSION, "packed app version mismatch - " .. tostring(metadata.VERSION), + EXIT_INVALID_AMI_ARCHIVE) + + -- extract + zip.extract(path, os.cwd() or ".", { + filter = function (p) + return not path_matches(p, { PACKER_METADATA_FILE }, "plain") + end + }) + + -- call unpack entrypoint + local options = metadata.options + + am.execute_action("unpack", options) end diff --git a/src/ami/exit-codes.lua b/src/ami/exit-codes.lua index 779987f..dc15995 100644 --- a/src/ami/exit-codes.lua +++ b/src/ami/exit-codes.lua @@ -16,6 +16,7 @@ local exit_codes = { EXIT_INVALID_AMI_INTERFACE = 13, EXIT_INVALID_ELI_VERSION = 14, + EXIT_INVALID_AMI_ARCHIVE = 15, EXIT_APP_INVALID_MODEL = 20, EXIT_APP_DOWNLOAD_ERROR = 21, diff --git a/src/ami/internals/interface/app.lua b/src/ami/internals/interface/app.lua index 160ed0f..4e74e6d 100644 --- a/src/ami/internals/interface/app.lua +++ b/src/ami/internals/interface/app.lua @@ -163,6 +163,35 @@ local function new(options) summary = implementation_status .. " Prints informations about app", -- (options, command, args, cli) action = violation_fallback + }, + pack = { + description = "ami 'pack' sub command", + summary = "Packs the app into a zip archive for easy migration", + options = { + output = { + index = 1, + aliases = {"o"}, + description = "Output path for the archive" + }, + light = { + index = 2, + description = "If used the archive will not include application data" + } + }, + action = function (options) + am.app.pack({ + destination = options.output, + mode = options.light and "light" or "full" + }) + end + }, + unpack = { + description = "ami 'unpack' sub command", + summary = "Unpacks the app from a zip archive", + hidden = true, -- should not be used by end user + action = function (options) + log_success("application unpacked") + end } } return base diff --git a/src/ami/internals/types.lua b/src/ami/internals/types.lua index 6fb3a0c..370f1c3 100644 --- a/src/ami/internals/types.lua +++ b/src/ami/internals/types.lua @@ -11,8 +11,8 @@ ---@class AmiCliBase ---@field id string | nil ---@field title string | nil ----@field expects_command boolean ----@field include_options_in_usage boolean +---@field expects_command boolean? +---@field include_options_in_usage boolean? ---@field action fun(_options: any, _command: any, _args: any, _cli: AmiCli)? ---@class AmiCli : AmiCliBase diff --git a/src/ami/internals/util.lua b/src/ami/internals/util.lua index 97462ca..52daf30 100644 --- a/src/ami/internals/util.lua +++ b/src/ami/internals/util.lua @@ -30,4 +30,124 @@ function util.append_to_url(url, ...) return url end +---@param set string +---@return string?, string? +local function validate_and_escape_set(set) + if type(set) ~= "string" then return set end + + local first_char_in_set = string.sub(set, 2, 2) + if first_char_in_set == "!" then first_char_in_set = "^" end -- replace ! with ^ for lua pattern + + if #set == 2 then -- empty set + return nil, "empty set" + end + + if #set == 3 then -- single character set + if first_char_in_set == "-" or first_char_in_set == "^" then + return nil, "invalid set ('" .. set .."')" + end + return "[" .. first_char_in_set .. "]", nil + end + + local result = "[" .. first_char_in_set .. string.sub(set, 3, #set - 1) .. "]" + return result, nil +end + +---@class GlobCharSetPart +---@field data string +---@field start_pos integer +---@field end_pos integer +---@field __type "char_set" + +---@class GlobRawPart +---@field data string +---@field __type "raw" + +---@alias GlobPart GlobCharSetPart | GlobRawPart + +---@param glob string +---@param start integer +---@return GlobCharSetPart?, string? +local function find_next_set(glob, start) + local set_start, set_end = string.find(glob, "%[[^\\]-%]", start) + if set_start then + -- check if set is escaped at the beginning + local escaped = set_start > 1 and string.sub(glob, set_start - 1, set_start - 1) == "\\" + if escaped then + return find_next_set(glob, set_start + 1) + end + + local valid_set, err = validate_and_escape_set(string.sub(glob, set_start, set_end)) + if valid_set == nil then + return nil, err + end + + return { + data = valid_set, + start_pos = set_start, + end_pos = set_end, + __type = "char_set" + } + end + return nil, nil +end + +local function escape_raw_glob_part(part) + local result = part + -- escape magic characters + result = (result:gsub("[%^%$%(%)%%%.%[%]%+%-]", "%%%1")) + + result = result + :gsub("\\\\", "\001") -- Temporarily replace double backslashes + :gsub("\\%*", "\002") -- Temporarily replace \* + :gsub("\\%?", "\003") -- Temporarily replace \? + :gsub("%*%*", "\004") -- Temporarily replace \? + -- Replace unescaped '*' with '.*' and '?' with '.' + result = result + :gsub("%*", "[^/]*") + :gsub("%?", "[^/]") + :gsub("\004", ".*") + + -- Restore escaped wildcards to their literal forms + result = result + :gsub("\002", "%*") + :gsub("\003", "%?") + :gsub("\001", "\\\\") + + return result +end + +function util.glob_to_lua_pattern(glob) + -- charsets + -- asterisk: matches zero or more characters + -- question mark: matches a single character + + -- find charsets + ---@type GlobPart[] + local glob_parts = {} + + local last_processed_index = 1 + while true do + local set= find_next_set(glob, last_processed_index) + if not set then + break + end + table.insert(glob_parts, { + data = escape_raw_glob_part(string.sub(glob, 1, set.start_pos - 1)), + __type = "raw" + }) + table.insert(glob_parts, set) + last_processed_index = set.end_pos + 1 + end + + table.insert(glob_parts, { + data = escape_raw_glob_part(string.sub(glob, last_processed_index)), + __type = "raw" + }) + + local parts_to_merge = table.map(glob_parts, function(part) return part.data end) + + return "^" .. string.join("", parts_to_merge) .. "$" +end + return util diff --git a/tests/app/full/1/ami.lua b/tests/app/full/1/ami.lua new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/full/1/app.hjson b/tests/app/full/1/app.hjson new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/full/1/bin/test.sh b/tests/app/full/1/bin/test.sh new file mode 100644 index 0000000..5320306 --- /dev/null +++ b/tests/app/full/1/bin/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Hello, world!" \ No newline at end of file diff --git a/tests/app/full/1/data/database.txt b/tests/app/full/1/data/database.txt new file mode 100644 index 0000000..ac4fd2d --- /dev/null +++ b/tests/app/full/1/data/database.txt @@ -0,0 +1,5 @@ +John Doe, 35, johndoe@example.com +Jane Smith, 28, janesmith@example.com +Alice Johnson, 42, alicej@example.com +Bob Brown, 50, bobbrown@example.com +Charlie Davis, 22, charlied@example.com \ No newline at end of file diff --git a/tests/test/app.lua b/tests/test/app.lua index 3ef80b0..8cce773 100644 --- a/tests/test/app.lua +++ b/tests/test/app.lua @@ -12,6 +12,54 @@ if not defaultCwd then return end +test["pack app (light)"] = function () + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/app_details/1") + local _, _ = pcall(am.app.load_configuration) + local app = am.app.__get() + app.type.repository = nil + os.chdir(defaultCwd) + test.assert(util.equals(am.app.__get(), { + configuration = { + TEST_CONFIGURATION = { + bool = true, + bool2 = false, + key = "value", + number = 15 + } + }, + id = "test1", + type = { + id = "test.app", + version = "latest", + } + }, true)) +end + +test["pack app (full)"] = function() + -- // TODO: Implement +end + +test["pack app (whitelist)"] = function() + -- // TODO: Implement +end + +test["pack app (blacklist)"] = function() + -- // TODO: Implement +end + +test["pack app (glob)"] = function() + -- // TODO: Implement +end + +test["pack app (lua-pattern)"] = function() + -- // TODO: Implement +end + +test["unpack app"] = function() + -- // TODO: Implement +end + test["load app details (json)"] = function() am.options.APP_CONFIGURATION_PATH = "app.json" os.chdir("tests/app/app_details/1") diff --git a/tests/test/internals/util.lua b/tests/test/internals/util.lua new file mode 100644 index 0000000..44d3c1f --- /dev/null +++ b/tests/test/internals/util.lua @@ -0,0 +1,38 @@ +local test = TEST or require "tests.vendor.u-test" + +require"tests.test_init" + +local internals_util = require "ami.internals.util" + +test["glob_to_lua_pattern (conversion)"] = function() + local patterns = { + { input = "", output = "^$" }, + { input = "a*b", output = "^a[^/]*b$" }, + { input = "a?b", output = "^a[^/]b$" }, + { input = "a[b]c", output = "^a[b]c$" }, + { input = "a[b-c]d", output = "^a[b-c]d$" }, + { input = "a**b", output = "^a.*b$" }, + { input = "a**b*", output = "^a.*b[^/]*$" }, + { input = "a**b**", output = "^a.*b.*$" }, + { input = "a**b**c", output = "^a.*b.*c$" }, + { input = "a**b**c*", output = "^a.*b.*c[^/]*$" }, + { input = "aaa/bbb[1-9]*.lua", output = "aaa/bbb[1-9]*.lua" } + } + + for _, pattern in ipairs(patterns) do + local lua_pattern = internals_util.glob_to_lua_pattern(pattern.input) + if lua_pattern ~= pattern.output then + print("For input '" .. pattern.input .. "', expected: '" .. pattern.output .. "', got: '" .. lua_pattern .. "'") + test.assert(false) + end + test.assert(true) + end +end + +test["glob_to_lua_pattern (matching)"] = function() + -- // TODO: Implement +end + +if not TEST then + test.summary() +end From a1094e285b1a739e74c434ac61c95280929f9745 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 6 Jan 2025 20:34:11 +0100 Subject: [PATCH 2/2] fixes and tests --- src/am.lua | 10 +- src/ami.lua | 19 +- src/ami/app.lua | 59 ++-- src/ami/internals/interface/base.lua | 8 +- src/version-info.lua | 2 +- tests/app/full/1/data/database2.txt | 11 + tests/test/am.lua | 66 ++++ tests/test/ami.lua | 51 ++++ tests/test/app.lua | 441 ++++++++++++++++++++++----- 9 files changed, 558 insertions(+), 109 deletions(-) create mode 100644 tests/app/full/1/data/database2.txt diff --git a/src/am.lua b/src/am.lua index 18b8566..93b82eb 100644 --- a/src/am.lua +++ b/src/am.lua @@ -78,7 +78,7 @@ function am.execute_action(cmd, options, command, args) local interface = get_interface(cmd) ami_assert(type(interface) == "table", "no valid command provided", EXIT_CLI_CMD_UNKNOWN) local action = interface.action - if type(action) ~= "table" then + if type(action) ~= "function" then ami_error("no valid action provided", EXIT_CLI_CMD_UNKNOWN) return -- just to make linter happy end @@ -211,3 +211,11 @@ am.execute_extension = exec.native_action ---@param inject_args ExternalActionOptions? ---@return integer am.execute_external = exec.external_action + + +---#DES am.unpack_app() +--- +---Unpacks application from zip archive +---@diagnostic disable-next-line: undefined-doc-param +---@param source string +am.unpack_app = am.app.unpack \ No newline at end of file diff --git a/src/ami.lua b/src/ami.lua index 9b5e648..9302ba5 100644 --- a/src/ami.lua +++ b/src/ami.lua @@ -38,7 +38,7 @@ if parsed_options.path then else log_error("Option 'path' provided, but chdir not supported.") log_info("HINT: Run ami without path parameter from path you supplied to 'path' option.") - os.exit(1) + return os.exit(1) end end @@ -77,31 +77,36 @@ end if parsed_options["base"] then if type(parsed_options["base"]) ~= "string" then log_error("Invalid base interface: " .. tostring(parsed_options["base"])) - os.exit(EXIT_INVALID_AMI_BASE_INTERFACE) + return os.exit(EXIT_INVALID_AMI_BASE_INTERFACE) end am.options.BASE_INTERFACE = parsed_options["base"] --[[@as string]] end +local unpack_path = parsed_options["unpack"] +if type(unpack_path) == "string" and unpack_path ~= "" then + am.unpack_app(unpack_path) + return os.exit(0) +end -- expose default options if parsed_options.version then print(am.VERSION) - os.exit(0) + return os.exit(0) end if parsed_options["is-app-installed"] then local is_installed = am.app.is_installed() print(is_installed) - os.exit(is_installed and 0 or EXIT_NOT_INSTALLED) + return os.exit(is_installed and 0 or EXIT_NOT_INSTALLED) end if parsed_options.about then print(am.ABOUT) - os.exit(0) + return os.exit(0) end if parsed_options["erase-cache"] then am.cache.erase() log_success("Cache succesfully erased.") - os.exit(0) + return os.exit(0) end if parsed_options["dry-run"] then @@ -114,7 +119,7 @@ if parsed_options["dry-run"] then end end am.execute_extension(tostring(remaining_args[1].value), ...) - os.exit(0) + return os.exit(0) end am.__reload_interface(am.options.SHALLOW) diff --git a/src/ami/app.lua b/src/ami/app.lua index 0aa8ac2..2edf977 100644 --- a/src/ami/app.lua +++ b/src/ami/app.lua @@ -123,8 +123,9 @@ local function load_configuration_content(path) EXIT_INVALID_CONFIGURATION) if not default_ok 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_ok and default_config --[[@as table]] or {}, + env_ok and env_config --[[@as table]] or {}, + true), { indent = false }) end local function load_configuration(path) @@ -382,7 +383,7 @@ function am.app.remove_data(keep) if type(keep) == "function" then return keep(p) end - end + end, }) ami_assert(ok, "Failed to remove app data - " .. tostring(err) .. "!", EXIT_RM_DATA_ERROR) end @@ -420,7 +421,7 @@ function am.app.remove(keep) if type(keep) == "function" then return keep(p, fp) end - end + end, }) ami_assert(ok, "Failed to remove app - " .. tostring(err) .. "!", EXIT_RM_ERROR) end @@ -467,16 +468,16 @@ end local DEFAULT_PATHS = { ["full"] = { ["whitelist"] = {}, - ["blacklist"] = { "data" }, + ["blacklist"] = {}, }, ["light"] = { ["whitelist"] = {}, - ["blacklist"] = { "data" }, + ["blacklist"] = { "data/**" }, }, } local REQUIRED_PATHS = { - ami_internals_util.glob_to_lua_pattern("app.?json") + ami_internals_util.glob_to_lua_pattern"app.?json", } ---@param path string @@ -484,17 +485,26 @@ local REQUIRED_PATHS = { ---@param mode PathMatchingMode local function path_matches(path, patterns, mode) for _, pattern in ipairs(patterns) do + local matched = false if mode == "plain" then - return path == pattern + matched = path == pattern elseif mode == "glob" then pattern = ami_internals_util.glob_to_lua_pattern(pattern) - return string.match(path, pattern) + matched = string.match(path, pattern) elseif mode == "lua-pattern" then - return string.match(path, pattern) + matched = string.match(path, pattern) + end + + if matched then + return true end end + return false end +---#DES am.app.pack +--- +---Packs the app into a zip archive for easy migration ---@param options PackOptions function am.app.pack(options) if type(options) ~= "table" then @@ -502,11 +512,14 @@ function am.app.pack(options) end local mode = options.mode or "light" - ami_assert(table.includes({ "full", "light" }, mode), "invalid mode - " .. tostring(mode), EXIT_CLI_ARG_VALIDATION_ERROR) + ami_assert(table.includes({ "full", "light" }, mode), "invalid mode - " .. tostring(mode), + EXIT_CLI_ARG_VALIDATION_ERROR) local path_filtering_mode = options.path_filtering_mode or "blacklist" - ami_assert(table.includes({ "whitelist", "blacklist" }, path_filtering_mode), "invalid path filtering mode - " .. tostring(path_filtering_mode), EXIT_CLI_ARG_VALIDATION_ERROR) + ami_assert(table.includes({ "whitelist", "blacklist" }, path_filtering_mode), + "invalid path filtering mode - " .. tostring(path_filtering_mode), EXIT_CLI_ARG_VALIDATION_ERROR) local path_matching_mode = options.path_matching_mode or "glob" - ami_assert(table.includes({ "plain", "glob", "lua-pattern" }, path_matching_mode), "invalid path matching mode - " .. tostring(path_matching_mode), EXIT_CLI_ARG_VALIDATION_ERROR) + ami_assert(table.includes({ "plain", "glob", "lua-pattern" }, path_matching_mode), + "invalid path matching mode - " .. tostring(path_matching_mode), EXIT_CLI_ARG_VALIDATION_ERROR) local paths = options.paths or DEFAULT_PATHS[mode][path_filtering_mode] @@ -515,6 +528,7 @@ function am.app.pack(options) end local destination_path = options.destination or "app.zip" + log_info("packing app into archive '" .. destination_path .. "'...") zip.compress(os.cwd() or ".", destination_path, { recurse = true, @@ -533,7 +547,8 @@ function am.app.pack(options) return not path_matches(p, paths, path_matching_mode) end return false - end + end, + content_only = true, }) -- add __ami_packed_metadata.json local archive = zip.open_archive(destination_path) @@ -545,7 +560,7 @@ function am.app.pack(options) "path_filtering_mode", "path_matching_mode", }, k) -- only save options that are not related to paths - end) + end), }) ---@diagnostic disable-next-line: undefined-field archive:close() @@ -553,9 +568,11 @@ function am.app.pack(options) log_success("app packed successfully into '" .. destination_path .. "'") end ----@param path string -function am.app.unpack(path) - local ok, metadata = zip.safe_extract_string(path, PACKER_METADATA_FILE) +---@param source string +function am.app.unpack(source) + 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 ok, metadata = hjson.safe_parse(metadata) @@ -564,11 +581,11 @@ function am.app.unpack(path) ami_assert(metadata.VERSION == PACKER_VERSION, "packed app version mismatch - " .. tostring(metadata.VERSION), EXIT_INVALID_AMI_ARCHIVE) - -- extract - zip.extract(path, os.cwd() or ".", { + -- extract + zip.extract(source, os.cwd() or ".", { filter = function (p) return not path_matches(p, { PACKER_METADATA_FILE }, "plain") - end + end, }) -- call unpack entrypoint diff --git a/src/ami/internals/interface/base.lua b/src/ami/internals/interface/base.lua index cd67152..09a9ab6 100644 --- a/src/ami/internals/interface/base.lua +++ b/src/ami/internals/interface/base.lua @@ -89,6 +89,12 @@ local function new() description = "Prints this help message" }, -- hidden + unpack = { + index = 97, + type = "string", + description = "Unpacks app from provided path", + hidden = true + }, ["dry-run"] = { index = 95, type = "boolean", @@ -122,7 +128,7 @@ local function new() type = "string", description = "Uses provided as base interface for further execution", hidden = true -- for now we do not want to show this in help. For now intent is to use this in hypothetical ami wrappers - } + }, }, action = function(_, command, args) am.execute(command, args) diff --git a/src/version-info.lua b/src/version-info.lua index bfecfb2..87e2d11 100644 --- a/src/version-info.lua +++ b/src/version-info.lua @@ -1,4 +1,4 @@ -local AM_VERSION = "0.30.1" +local AM_VERSION = "0.31.0" return { VERSION = AM_VERSION, diff --git a/tests/app/full/1/data/database2.txt b/tests/app/full/1/data/database2.txt new file mode 100644 index 0000000..2c9d8df --- /dev/null +++ b/tests/app/full/1/data/database2.txt @@ -0,0 +1,11 @@ +ItemID,ItemName,Quantity,Price +1,Apple,50,0.50 +2,Banana,30,0.20 +3,Orange,20,0.30 +4,Milk,10,1.50 +5,Bread,15,2.00 +6,Butter,5,3.00 +7,Cheese,8,4.00 +8,Chicken,12,5.00 +9,Beef,7,7.00 +10,Carrot,25,0.10 \ No newline at end of file diff --git a/tests/test/am.lua b/tests/test/am.lua index dd03974..9ea73dd 100644 --- a/tests/test/am.lua +++ b/tests/test/am.lua @@ -277,6 +277,72 @@ test["configure_cache"] = function() log_debug = original_log_debug end +test["am.unpack_app"] = function() + local default_cwd = os.cwd() or "." + local unpack_hook_called = false + + local interface = { + commands = { + unpack = { + label = "test", + action = function (options) + unpack_hook_called = true + log_success("Unpacked") + end + } + }, + action = function(_, command, args) + am.execute(command, args) + end + } + am.__set_interface(interface) + + am.options.APP_CONFIGURATION_PATH = "app.json" + local destination = "/tmp/app.zip" + os.remove(destination) + local test_dir = path.combine(default_cwd, "tests/tmp/app_test_unpack_app") + + os.chdir("tests/app/full/1") + fs.mkdirp(test_dir) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ mode = "light", destination = destination }) + test.assert(error_code == 0) + + os.chdir(test_dir) + am.unpack_app(destination) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin", + "ami.lua", + "data" + } + + local packed_paths_count = 0 + + local unpacked_paths = fs.read_dir(".", { recurse = true }) + for _, path in ipairs(unpacked_paths) do + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + packed_paths_count = packed_paths_count + 1 + end + + os.remove(destination) + test.assert(packed_paths_count == 5 and #paths_to_check == 0 and unpack_hook_called) + fs.remove(test_dir, { recurse = true, content_only = true }) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + if not TEST then test.summary() end diff --git a/tests/test/ami.lua b/tests/test/ami.lua index f2b5c0f..6918cf5 100644 --- a/tests/test/ami.lua +++ b/tests/test/ami.lua @@ -209,6 +209,57 @@ test["ami remove --all"] = function() os.chdir(default_cwd) end +test["ami --unpack=..."] = function() + local destination = "/tmp/app.zip" + os.remove(destination) + local test_dir = path.combine(default_cwd, "tests/tmp/app_test_unpack_app") + + os.chdir("tests/app/full/1") + fs.mkdirp(test_dir) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ mode = "light", destination = destination }) + test.assert(error_code == 0) + + os.chdir(default_cwd) + local original_exit = os.exit + os.exit = function() end + ami("--unpack="..destination, "--path="..test_dir) + os.exit = original_exit + os.remove(destination) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin", + "ami.lua", + "data" + } + + local packed_paths_count = 0 + local unpacked_paths = fs.read_dir(test_dir, { recurse = true }) + for _, path in ipairs(unpacked_paths) do + if path:match("^%.ami%-cache") then goto continue end -- skip .ami-cache + + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + packed_paths_count = packed_paths_count + 1 + ::continue:: + end + + test.assert(packed_paths_count == 5 and #paths_to_check == 0) + fs.remove(test_dir, { recurse = true, content_only = true }) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + ami_error = original_ami_error_fn if not TEST then test.summary() diff --git a/tests/test/app.lua b/tests/test/app.lua index 8cce773..ee3c4b5 100644 --- a/tests/test/app.lua +++ b/tests/test/app.lua @@ -4,69 +4,21 @@ require "tests.test_init" local stringify = require "hjson".stringify -local defaultCwd = os.cwd() -if not defaultCwd then +local default_cwd = os.cwd() +if not default_cwd then test["get cwd"] = function() test.assert(false) end return end -test["pack app (light)"] = function () - am.options.APP_CONFIGURATION_PATH = "app.json" - os.chdir("tests/app/app_details/1") - local _, _ = pcall(am.app.load_configuration) - local app = am.app.__get() - app.type.repository = nil - os.chdir(defaultCwd) - test.assert(util.equals(am.app.__get(), { - configuration = { - TEST_CONFIGURATION = { - bool = true, - bool2 = false, - key = "value", - number = 15 - } - }, - id = "test1", - type = { - id = "test.app", - version = "latest", - } - }, true)) -end - -test["pack app (full)"] = function() - -- // TODO: Implement -end - -test["pack app (whitelist)"] = function() - -- // TODO: Implement -end - -test["pack app (blacklist)"] = function() - -- // TODO: Implement -end - -test["pack app (glob)"] = function() - -- // TODO: Implement -end - -test["pack app (lua-pattern)"] = function() - -- // TODO: Implement -end - -test["unpack app"] = function() - -- // TODO: Implement -end - test["load app details (json)"] = function() am.options.APP_CONFIGURATION_PATH = "app.json" os.chdir("tests/app/app_details/1") local _, _ = pcall(am.app.load_configuration) local app = am.app.__get() app.type.repository = nil - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(util.equals(am.app.__get(), { configuration = { TEST_CONFIGURATION = { @@ -91,7 +43,7 @@ test["load app details (hjson)"] = function() local app = am.app.__get() app.type.repository = nil - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(util.equals(app, { configuration = { TEST_CONFIGURATION = { @@ -113,7 +65,7 @@ test["load app details (variables - json)"] = function() am.options.APP_CONFIGURATION_PATH = "app.json" os.chdir("tests/app/app_details/4") local ok = pcall(am.app.load_configuration) - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(am.app.get_configuration({ "TEST_CONFIGURATION", "key" }) == "test-key2") end @@ -121,7 +73,7 @@ test["load app details (variables - hjson)"] = function() am.options.APP_CONFIGURATION_PATH = "app.hjson" os.chdir("tests/app/app_details/4") local ok = pcall(am.app.load_configuration) - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(am.app.get_configuration({ "TEST_CONFIGURATION", "key" }) == "test-key") end @@ -134,7 +86,7 @@ test["load app details (dev env)"] = function() local app = am.app.__get() app.type.repository = nil - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(util.equals(app, { configuration = { TEST_CONFIGURATION = { @@ -166,7 +118,7 @@ test["load app details missing default config (dev env)"] = function() local app = am.app.__get() app.type.repository = nil - os.chdir(defaultCwd) + os.chdir(default_cwd) log_warn = old_log_warn test.assert(util.equals(app, { @@ -199,7 +151,7 @@ test["load app details missing env config (dev env)"] = function() local app = am.app.__get() app.type.repository = nil - os.chdir(defaultCwd) + os.chdir(default_cwd) log_warn = old_log_warn test.assert(util.equals(app, { @@ -226,10 +178,10 @@ end test["load app details missing config (dev env)"] = function() os.chdir("tests/app/app_details/7") - local errorCode = 0 + local error_code = 0 local original_ami_error_fn = ami_error ami_error = function(_, exitCode) - errorCode = errorCode ~= 0 and errorCode or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR end local old_log_warn = log_warn local log = "" @@ -240,22 +192,22 @@ test["load app details missing config (dev env)"] = function() am.options.APP_CONFIGURATION_PATH = nil am.options.ENVIRONMENT = "dev" local _ = pcall(am.app.load_configuration) - local devErrorCode = errorCode + local deverror_code = error_code local devLog = log -- test no env - errorCode = 0 + error_code = 0 log = "" am.options.APP_CONFIGURATION_PATH = nil am.options.ENVIRONMENT = nil local ok = pcall(am.app.load_configuration) - local defaultErrorCode = errorCode + local defaulterror_code = error_code local defaultLog = log - os.chdir(defaultCwd) + os.chdir(default_cwd) ami_error = original_ami_error_fn log_warn = old_log_warn - test.assert(defaultErrorCode == EXIT_INVALID_CONFIGURATION and not string.find(defaultLog, "app.dev.json", 0, true)) - test.assert(devErrorCode == EXIT_INVALID_CONFIGURATION and string.find(devLog, "app.dev.json", 0, true)) + 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)) end test["load app model"] = function() @@ -263,7 +215,7 @@ test["load app model"] = function() os.chdir("tests/app/app_details/2") pcall(am.app.load_configuration) local result = hash.sha256_sum(stringify(am.app.get_model(), { sortKeys = true, indent = " " }), true) - os.chdir(defaultCwd) + os.chdir(default_cwd) test.assert(result == "4042b5f3b3dd1463d55166db96f3b17ecfe08b187fecfc7fb53860a478ed0844") end @@ -280,7 +232,7 @@ test["prepare app"] = function() local ok, error = pcall(am.app.prepare) test.assert(ok, error) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["is app installed"] = function() @@ -297,7 +249,7 @@ test["is app installed"] = function() local ok = pcall(am.app.prepare) test.assert(ok) test.assert(am.app.is_installed() == true) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["get app version"] = function() @@ -315,7 +267,7 @@ test["get app version"] = function() local ok, version = pcall(am.app.get_version) test.assert(ok and version == "0.1.0") - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app data"] = function() @@ -339,7 +291,7 @@ test["remove app data"] = function() local ok, entries = fs.safe_read_dir("data", { recurse = true }) test.assert(ok and #entries == 0) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app data (list of protected files)"] = function() @@ -363,7 +315,7 @@ test["remove app data (list of protected files)"] = function() local ok, entries = fs.safe_read_dir("data", { recurse = true }) test.assert(ok and #entries == 1) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app data (keep function)"] = function() @@ -389,7 +341,7 @@ test["remove app data (keep function)"] = function() local ok, entries = fs.safe_read_dir("data", { recurse = true }) test.assert(ok and #entries == 1) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app"] = function() @@ -418,7 +370,7 @@ test["remove app"] = function() end end test.assert(ok and #entries == 1) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app (list of protected files)"] = function() @@ -447,7 +399,7 @@ test["remove app (list of protected files)"] = function() end end test.assert(ok and #entries == 3) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["remove app (keep function)"] = function() @@ -478,7 +430,7 @@ test["remove app (keep function)"] = function() end end test.assert(ok and #entries == 3) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["is update available"] = function() @@ -488,7 +440,7 @@ test["is update available"] = function() os.chdir(test_dir) local ok = pcall(am.app.load_configuration) test.assert(am.app.is_update_available()) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["is update available (updated already)"] = function() @@ -498,7 +450,7 @@ test["is update available (updated already)"] = function() os.chdir(test_dir) local ok = pcall(am.app.load_configuration) test.assert(not am.app.is_update_available()) - os.chdir(defaultCwd) + os.chdir(default_cwd) end test["is update available alternative channel"] = function() @@ -509,7 +461,340 @@ test["is update available alternative channel"] = function() 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") - os.chdir(defaultCwd) + os.chdir(default_cwd) +end + +test["pack app (light)"] = function () + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ mode = "light", destination = destination }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin/", + "ami.lua", + "data/", + "__ami_packed_metadata.json" + } + + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + test.assert(packed_paths_count == 6 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["pack app (full)"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ mode = "full", destination = destination }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin/", + "ami.lua", + "data/database.txt", + "data/database2.txt", + "data/", + "__ami_packed_metadata.json", + } + + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + test.assert(packed_paths_count == 8 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["pack app (whitelist)"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ + mode = "full", + destination = destination, + path_filtering_mode = "whitelist", + paths = { + "app.hjson", + "bin/**" + } + }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "__ami_packed_metadata.json" + } + + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + test.assert(packed_paths_count == 3 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["pack app (blacklist)"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ + mode = "full", + destination = destination, + path_filtering_mode = "blacklist", + paths = { + "bin/**", + "data/**" + } + }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/", + "ami.lua", + "data/", + "__ami_packed_metadata.json" + } + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + util.print_table(paths_to_check) + test.assert(packed_paths_count == 5 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["pack app (glob)"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ + mode = "full", + destination = destination, + path_filtering_mode ="whitelist", + path_matching_mode = "glob", + paths = { "**" } + }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin/", + "ami.lua", + "data/database.txt", + "data/database2.txt", + "data/", + "__ami_packed_metadata.json", + } + + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + test.assert(packed_paths_count == 8 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["pack app (lua-pattern)"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + os.chdir("tests/app/full/1") + + local destination = "/tmp/app.zip" + os.remove(destination) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ + mode = "full", + destination = destination, + path_filtering_mode ="whitelist", + path_matching_mode = "lua-pattern", + paths = { ".*" } + }) + test.assert(error_code == 0) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin/", + "ami.lua", + "data/database.txt", + "data/database2.txt", + "data/", + "__ami_packed_metadata.json", + } + + local packed_paths_count = 0 + zip.extract(destination, "/tmp", { + filter = function (path) + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + + packed_paths_count = packed_paths_count + 1 + return false + end + }) + os.remove(destination) + test.assert(packed_paths_count == 8 and #paths_to_check == 0) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) +end + +test["unpack app"] = function() + am.options.APP_CONFIGURATION_PATH = "app.json" + local destination = "/tmp/app.zip" + os.remove(destination) + local test_dir = path.combine(default_cwd, "tests/tmp/app_test_unpack_app") + + os.chdir("tests/app/full/1") + fs.mkdirp(test_dir) + + local error_code = 0 + local original_ami_error_fn = ami_error + ami_error = function(_, exitCode) + error_code = error_code ~= 0 and error_code or exitCode or AMI_CONTEXT_FAIL_EXIT_CODE or EXIT_UNKNOWN_ERROR + end + + am.app.pack({ mode = "light", destination = destination }) + test.assert(error_code == 0) + + os.chdir(test_dir) + am.app.unpack(destination) + + local paths_to_check = { + "app.hjson", + "bin/test.sh", + "bin", + "ami.lua", + "data" + } + + local packed_paths_count = 0 + + local unpacked_paths = fs.read_dir(".", { recurse = true }) + for _, path in ipairs(unpacked_paths) do + paths_to_check = table.filter(paths_to_check, function (_, v) + return path ~= v + end) + packed_paths_count = packed_paths_count + 1 + end + + os.remove(destination) + test.assert(packed_paths_count == 5 and #paths_to_check == 0) + fs.remove(test_dir, { recurse = true, content_only = true }) + + ami_error = original_ami_error_fn + os.chdir(default_cwd) end if not TEST then