// Copyright (C) 2026 Viorel Munteanu // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // [unit] tests for parse_args() (cli_args.h) against synthetic argv's -- // pure argument parsing, no mounting/running/side effects. Each call goes // through the real getopt_long()-based parser exactly as main() does. #include #include #include #include "cli_args.h" namespace { // Builds a synthetic argv (argv[0] is always "slocker-lite", matching this // project's own real binary name -- parse_args() only ever uses it for // print_usage()'s own diagnostic text) and calls parse_args(). The // std::vector is kept alive by the caller for as long as the // returned char* vector is used, since parse_args() doesn't copy argv's // own strings out (ParsedArgs::command/test_args are the only things it // keeps, and those *are* copies). struct ParseResult { std::optional exit_code; ParsedArgs args; }; ParseResult run_parse(const std::vector& args) { std::vector owned = {"slocker-lite"}; owned.insert(owned.end(), args.begin(), args.end()); std::vector argv; argv.reserve(owned.size()); for (auto& a : owned) { argv.push_back(a.data()); } ParseResult result; result.exit_code = parse_args(static_cast(argv.size()), argv.data(), result.args); return result; } } // namespace TEST_CASE("parse_args: --mount selects Mode::mount", "[unit]") { auto result = run_parse({"--mount", "image.tar"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::mount); CHECK(result.args.mode_arg == "image.tar"); } TEST_CASE("parse_args: -m is no longer a recognized short option", "[unit]") { auto result = run_parse({"-m", "image.tar"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: -h returns exit code 0 immediately", "[unit]") { auto result = run_parse({"-h"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 0); } TEST_CASE("parse_args: -V returns exit code 0 immediately", "[unit]") { auto result = run_parse({"-V"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 0); } TEST_CASE("parse_args: no mode at all is a parse error", "[unit]") { auto result = run_parse({}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: two actions on the same command line is a parse error", "[unit]") { auto result = run_parse({"--mount", "a.tar", "-l", "dir"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: --group without --user is a parse error", "[unit]") { auto result = run_parse({"-r", "a.tar", "--group", "mygroup", "--", "true"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: -r captures the trailing command after --", "[unit]") { auto result = run_parse({"-r", "a.tar", "--", "sh", "-c", "echo hi"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::run); REQUIRE(result.args.command.size() == 3); CHECK(result.args.command[0] == "sh"); CHECK(result.args.command[1] == "-c"); CHECK(result.args.command[2] == "echo hi"); } TEST_CASE("parse_args: -x -- selects Mode::exec with a parsed pid", "[unit]") { auto result = run_parse({"-x", "1234", "--", "ls", "-la"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::exec); REQUIRE(result.args.exec_pid.has_value()); CHECK(*result.args.exec_pid == 1234); REQUIRE(result.args.command.size() == 2); CHECK(result.args.command[0] == "ls"); } TEST_CASE("parse_args: -x with a non-numeric pid is a parse error", "[unit]") { auto result = run_parse({"-x", "notapid", "--", "ls"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: -x with no trailing command is a parse error", "[unit]") { auto result = run_parse({"-x", "1234"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: leftover positional args are rejected for a mode that doesn't take one", "[unit]") { auto result = run_parse({"-l", "dir", "extra"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: bare -t has an empty test_args and no leftover-args error", "[unit]") { auto result = run_parse({"-t"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::test); CHECK(result.args.test_args.empty()); } TEST_CASE("parse_args: -t -- captures everything after -- verbatim", "[unit]") { auto result = run_parse({"-t", "--", "[unit]", "--reporter=compact"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::test); REQUIRE(result.args.test_args.size() == 2); CHECK(result.args.test_args[0] == "[unit]"); CHECK(result.args.test_args[1] == "--reporter=compact"); } TEST_CASE("parse_args: -t works without a '--' since it doesn't look like an option", "[unit]") { auto result = run_parse({"-t", "[unit]"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::test); REQUIRE(result.args.test_args.size() == 1); CHECK(result.args.test_args[0] == "[unit]"); } TEST_CASE("parse_args: repeated -n accumulates network_specs in order", "[unit]") { auto result = run_parse({"-r", "a.tar", "-n", "net1", "-n", "net2", "--", "true"}); REQUIRE_FALSE(result.exit_code.has_value()); REQUIRE(result.args.network_specs.size() == 2); CHECK(result.args.network_specs[0] == "net1"); CHECK(result.args.network_specs[1] == "net2"); } TEST_CASE("parse_args: -v with -r accumulates volume_specs as (spec, container-path) pairs", "[unit]") { auto result = run_parse({"-r", "a.tar", "-v", "myvol", "/data", "--", "true"}); REQUIRE_FALSE(result.exit_code.has_value()); REQUIRE(result.args.volume_specs.size() == 1); CHECK(result.args.volume_specs[0].first == "myvol"); CHECK(result.args.volume_specs[0].second == "/data"); } TEST_CASE("parse_args: --with-ipv6/--with-veth accept true/false and default to unset", "[unit]") { auto plain = run_parse({"-n", "net1", "--extern"}); REQUIRE_FALSE(plain.exit_code.has_value()); CHECK_FALSE(plain.args.network_with_ipv6_flag.has_value()); CHECK_FALSE(plain.args.network_with_veth_flag.has_value()); auto disabled = run_parse({"-n", "net1", "--extern", "--with-ipv6=false", "--with-veth=false"}); REQUIRE_FALSE(disabled.exit_code.has_value()); REQUIRE(disabled.args.network_with_ipv6_flag.has_value()); CHECK(*disabled.args.network_with_ipv6_flag == false); REQUIRE(disabled.args.network_with_veth_flag.has_value()); CHECK(*disabled.args.network_with_veth_flag == false); // Same accepted forms as the config file itself (parse_bool_flag(), // config_file.h) -- not just a literal "true"/"false". auto on = run_parse({"-n", "net1", "--intern", "--with-ipv6=on", "--with-veth=1"}); REQUIRE_FALSE(on.exit_code.has_value()); REQUIRE(on.args.network_with_ipv6_flag.has_value()); CHECK(*on.args.network_with_ipv6_flag == true); REQUIRE(on.args.network_with_veth_flag.has_value()); CHECK(*on.args.network_with_veth_flag == true); } TEST_CASE("parse_args: --with-ipv6 with an unrecognized value is a parse error", "[unit]") { auto result = run_parse({"-n", "net1", "--extern", "--with-ipv6=maybe"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: --subnet6 and --with-ipv6=false can't be used together", "[unit]") { auto result = run_parse({"-n", "net1", "--extern", "--subnet6", "fdf0::/64", "--with-ipv6=false"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: --kill requires a numeric pid", "[unit]") { auto ok = run_parse({"--kill", "5678"}); REQUIRE_FALSE(ok.exit_code.has_value()); CHECK(ok.args.mode == Mode::kill); REQUIRE(ok.args.kill_pid.has_value()); CHECK(*ok.args.kill_pid == 5678); auto bad = run_parse({"--kill", "notapid"}); REQUIRE(bad.exit_code.has_value()); CHECK(*bad.exit_code == 1); } TEST_CASE("parse_args: -c/--config-file populates config_file_flag, unset by default", "[unit]") { auto without = run_parse({"--list-volumes"}); REQUIRE_FALSE(without.exit_code.has_value()); CHECK_FALSE(without.args.config_file_flag.has_value()); auto with_short = run_parse({"-c", "/tmp/alt.yaml", "--list-volumes"}); REQUIRE_FALSE(with_short.exit_code.has_value()); REQUIRE(with_short.args.config_file_flag.has_value()); CHECK(*with_short.args.config_file_flag == "/tmp/alt.yaml"); auto with_long = run_parse({"--config-file", "/tmp/alt2.yaml", "-w"}); REQUIRE_FALSE(with_long.exit_code.has_value()); REQUIRE(with_long.args.config_file_flag.has_value()); CHECK(*with_long.args.config_file_flag == "/tmp/alt2.yaml"); } TEST_CASE("parse_args: --log-level sets log_level_flag_given", "[unit]") { auto without = run_parse({"--list-volumes"}); REQUIRE_FALSE(without.exit_code.has_value()); CHECK_FALSE(without.args.log_level_flag_given); auto with_level = run_parse({"--log-level", "debug", "--list-volumes"}); REQUIRE_FALSE(with_level.exit_code.has_value()); CHECK(with_level.args.log_level_flag_given); } TEST_CASE("parse_args: -u/--up defaults compose_file_name to compose.yaml when omitted", "[unit]") { auto result = run_parse({"-u", "/var/lib/images"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::compose_up); REQUIRE(result.args.compose_images_directory.has_value()); CHECK(*result.args.compose_images_directory == "/var/lib/images"); CHECK(result.args.compose_file_name == "compose.yaml"); } TEST_CASE("parse_args: -u accepts an explicit second token as compose_file_name", "[unit]") { auto result = run_parse({"-u", "/var/lib/images", "my-stack.yaml"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::compose_up); CHECK(*result.args.compose_images_directory == "/var/lib/images"); CHECK(result.args.compose_file_name == "my-stack.yaml"); } TEST_CASE("parse_args: -u doesn't swallow a following flag as compose_file_name", "[unit]") { auto result = run_parse({"-u", "/var/lib/images", "--log-level", "debug"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::compose_up); CHECK(*result.args.compose_images_directory == "/var/lib/images"); // The default, since "--log-level" looks like a flag, not a filename. CHECK(result.args.compose_file_name == "compose.yaml"); CHECK(result.args.log_level_flag_given); } TEST_CASE("parse_args: -d/--down takes a single optional compose-file-name, no images directory", "[unit]") { auto default_name = run_parse({"--down"}); REQUIRE_FALSE(default_name.exit_code.has_value()); CHECK(default_name.args.mode == Mode::compose_down); CHECK_FALSE(default_name.args.compose_images_directory.has_value()); CHECK(default_name.args.compose_file_name == "compose.yaml"); auto explicit_name = run_parse({"-d", "my-stack.yaml"}); REQUIRE_FALSE(explicit_name.exit_code.has_value()); CHECK(explicit_name.args.mode == Mode::compose_down); CHECK_FALSE(explicit_name.args.compose_images_directory.has_value()); CHECK(explicit_name.args.compose_file_name == "my-stack.yaml"); } TEST_CASE("parse_args: -d doesn't swallow a following flag as compose_file_name", "[unit]") { auto result = run_parse({"-d", "--log-level", "debug"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::compose_down); CHECK(result.args.compose_file_name == "compose.yaml"); CHECK(result.args.log_level_flag_given); } TEST_CASE("parse_args: -u and -d together on the same command line is a parse error", "[unit]") { auto result = run_parse({"-u", "/var/lib/images", "-d", "/var/lib/images"}); REQUIRE(result.exit_code.has_value()); CHECK(*result.exit_code == 1); } TEST_CASE("parse_args: --list-containers selects Mode::list_containers", "[unit]") { auto result = run_parse({"--list-containers"}); REQUIRE_FALSE(result.exit_code.has_value()); CHECK(result.args.mode == Mode::list_containers); }