/* * Copyright (c) 2026 Ember * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include using namespace ember; class Registry : public ::testing::Test { public: virtual void SetUp() { registry = commands::create("root"); auto cmd = registry->insert("root") ->description("root command") ->argument("arg1") ->argument("arg2") ->argument("arg3", commands::optional) ->handler([&](auto /*args*/) { success = true; }); cmd->insert("root_nested_1"); cmd->insert("root_nested_2") ->insert("root_nested_2_1"); registry->insert("root_1"); // error: unavailable registry->insert("root_2")->insert("root_2_1"); // error: subcommands only } virtual void TearDown() {} std::shared_ptr registry; bool success = false; }; using Commands = Registry; TEST_F(Registry, ParseInput) { const auto input = "Lorem ipsum dolor sit amet"; const auto tokens = commands::parse_input(input); const std::array expected { "Lorem", "ipsum", "dolor", "sit", "amet" }; ASSERT_EQ(tokens.size(), expected.size()); for(auto [t, e] : std::views::zip(tokens, expected)) { ASSERT_EQ(t, e); } } TEST_F(Registry, ParseQuotedInput) { const auto input = R"(Lorem "ipsum dolor" sit amet)"; const auto tokens = commands::parse_input(input); const std::array expected { "Lorem", "ipsum dolor", "sit", "amet" }; ASSERT_EQ(tokens.size(), expected.size()); for(auto [t, e] : std::views::zip(tokens, expected)) { ASSERT_EQ(t, e); } } TEST_F(Registry, ParseEscapeQuotedInput) { const auto input = R"("Hello, "\"world!"\")"; const auto tokens = commands::parse_input(input); const std::array expected { R"(Hello, "world!")" }; ASSERT_EQ(tokens.size(), expected.size()); for(auto [t, e] : std::views::zip(tokens, expected)) { ASSERT_EQ(t, e); } } TEST_F(Registry, ParseBadEscapedInput) { const std::string_view input = R"("Hello, \world")"; ASSERT_THROW(commands::parse_input(input), commands::parse_error); } TEST_F(Registry, AddCommand) { registry->insert("test") ->description("test_description"); const auto input = commands::parse_input("test"); ASSERT_NE(registry->find(input).command, nullptr); } TEST_F(Registry, EraseCommand) { ASSERT_TRUE(registry->erase("root")); ASSERT_EQ(registry->find("root").command, nullptr); } TEST_F(Registry, EraseNotFoundCommand) { ASSERT_FALSE(registry->erase("fake_command")); } TEST_F(Registry, SearchSubcommands) { const auto result = registry->find("root root_nested_1 root_nested_2"); ASSERT_TRUE(result.command); ASSERT_EQ(result.depth, 2); } TEST_F(Registry, Autocomplete) { const auto result = registry->autocomplete("r"); ASSERT_EQ(result.substring, "root "); ASSERT_EQ(result.records.size(), 3); ASSERT_EQ(result.records.front().name, "root"); ASSERT_EQ(result.records.front().desc, "root command"); } TEST_F(Registry, AutocompleteSubcommands) { const auto result = registry->autocomplete("root r"); ASSERT_EQ(result.substring, "root root_nested_"); ASSERT_EQ(result.records.size(), 2); ASSERT_EQ(result.records.front().name, "root_nested_1"); ASSERT_EQ(result.records.back().name, "root_nested_2"); } TEST_F(Registry, AutocompleteNestedSubcommand) { const auto result = registry->autocomplete("root root_nested_2 r"); ASSERT_EQ(result.substring, "root root_nested_2 root_nested_2_1"); ASSERT_EQ(result.records.size(), 1); ASSERT_EQ(result.records.front().name, "root_nested_2_1"); } TEST_F(Registry, SearchNoMatch) { const auto result = registry->autocomplete("foo"); ASSERT_TRUE(result.substring.empty()); ASSERT_TRUE(result.records.empty()); } TEST_F(Registry, PathFragment) { const auto tokens = commands::parse_input("root root_nested_2 root_nested_2_1"); ASSERT_EQ(commands::path_fragment(tokens, 0), ""); ASSERT_EQ(commands::path_fragment(tokens, 1), "root"); ASSERT_EQ(commands::path_fragment(tokens, 2), "root root_nested_2"); ASSERT_EQ(commands::path_fragment(tokens, 3), "root root_nested_2 root_nested_2_1"); ASSERT_EQ(commands::path_fragment(tokens, 4), ""); } TEST_F(Commands, InsertCommand) { auto cmd = registry->find("root").command; cmd->insert("subcommand")->description("It's a subcommand"); auto subcommands = cmd->commands(); ASSERT_EQ(subcommands.size(), 3); ASSERT_EQ(subcommands.contains("subcommand"), 1); ASSERT_EQ(subcommands["subcommand"]->description(), "It's a subcommand"); } TEST_F(Commands, EraseCommand) { auto cmd = registry->find("root").command; cmd->erase("root_nested_1"); ASSERT_FALSE(cmd->commands().contains("root_nested_1")); } TEST_F(Commands, ClearCommands) { auto cmd = registry->find("root").command; cmd->clear_commands(); ASSERT_TRUE(cmd->commands().empty()); } TEST_F(Commands, UpdateDescription) { auto cmd = registry->find("root").command; ASSERT_EQ(cmd->description(), "root command"); cmd->description("New description!"); ASSERT_EQ(cmd->description(), "New description!"); } TEST_F(Commands, ArgumentCount) { auto cmd = registry->find("root").command; ASSERT_EQ(cmd->argument_count(), 3); } TEST_F(Commands, AddArgument) { auto cmd = registry->find("root").command; cmd->argument("arg4", commands::optional); ASSERT_EQ(cmd->argument_count(), 4); } TEST_F(Commands, AddArgumentOutOfOrder) { auto cmd = registry->find("root").command; ASSERT_THROW(cmd->argument("arg4"), std::invalid_argument); } TEST_F(Commands, EraseInsertArgument) { auto cmd = registry->find("root").command; ASSERT_TRUE(cmd->erase_argument("arg3")); cmd->argument("arg3"); ASSERT_EQ(cmd->argument_count(), 3); } TEST_F(Commands, EraseArgument) { auto cmd = registry->find("root").command; ASSERT_TRUE(cmd->erase_argument("arg1")); } TEST_F(Commands, EraseNotFoundArgument) { auto cmd = registry->find("root").command; ASSERT_FALSE(cmd->erase_argument("fake_arg1")); } TEST_F(Commands, ClearArguments) { auto cmd = registry->find("root").command; cmd->clear_arguments(); ASSERT_TRUE(cmd->arguments().empty()); } TEST_F(Commands, ExecuteNoHandler) { auto cmd = registry->find("root_1").command; ASSERT_EQ(cmd->execute(), commands::Result::unavailable); } TEST_F(Commands, Execute_InvalidTypes) { auto cmd = registry->find("root").command; std::array args { "Are you sure that's the point, Doctor?", "Of course. What else could it be?", "That you should never tell the same lie twice." }; ASSERT_EQ(cmd->execute(args), commands::Result::invalid_types); } TEST_F(Commands, Execute_SubcommandsOnly) { auto cmd = registry->find("root_2").command; ASSERT_EQ(cmd->execute(), commands::Result::subcommands); } TEST_F(Commands, Execute_MissingArgsEmpty) { auto cmd = registry->find("root").command; ASSERT_EQ(cmd->execute(), commands::Result::missing_args); } TEST_F(Commands, Execute_TooManyArgs) { auto cmd = registry->find("root").command; std::array args { "The", "quick", "brown", "fox" }; ASSERT_EQ(cmd->execute(args), commands::Result::too_many_args); } TEST_F(Commands, Execute_MissingArgs) { auto cmd = registry->find("root").command; std::array args { "jumped", }; ASSERT_EQ(cmd->execute(), commands::Result::missing_args); } TEST_F(Commands, Execute_RequiredArgs_Success) { auto cmd = registry->find("root").command; std::array args { std::string("over"), std::string("the"), }; ASSERT_EQ(cmd->execute(args), commands::Result::success); ASSERT_TRUE(success); } TEST_F(Commands, Execute_OptionalArgs_Success) { auto cmd = registry->find("root").command; std::array args { std::string("lazy"), std::string("dog"), std::uint32_t(42) }; ASSERT_EQ(cmd->execute(args), commands::Result::success); ASSERT_TRUE(success); } TEST_F(Commands, Execute_AllArgTypes_Success) { auto cmd = registry->find("root").command; cmd->clear_arguments(); struct UserData { int foo; float bar; } data{}; cmd->argument("arg1") ->argument("arg2") ->argument("arg3") ->argument("arg4") ->argument("arg5") ->argument("arg6") ->argument("arg7") ->argument("arg8") ->argument("arg9") ->argument("arg10") ->argument("arg11") ->argument("arg12") ->argument("arg13"); std::array args { std::string("Hello, world"), 'c', 1.0, 1.0f, std::int16_t(0), std::int32_t(0), std::int64_t(0), std::int8_t(0), std::uint16_t(0), std::uint32_t(0), std::uint64_t(0), std::uint8_t(0), data }; ASSERT_EQ(cmd->execute(args), commands::Result::success); ASSERT_TRUE(success); } TEST_F(Commands, CommandFlags) { auto command = registry->insert("test") ->flags({ 42, 117 }); ASSERT_EQ(command->flags().custom, 42); ASSERT_EQ(command->flags().security, 117); } TEST_F(Commands, CommandFlagsUpdate) { auto command = registry->insert("test") ->flags({ 42, 117 }); command->flags({ 0xB105F00D, 0x8BADF00D }); ASSERT_EQ(command->flags().custom, 0xB105F00D); ASSERT_EQ(command->flags().security, 0x8BADF00D); } TEST_F(Commands, ScopedCommandInsertReset) { auto command = registry->scoped_insert("scoped_test"); auto result = registry->find("scoped_test"); ASSERT_TRUE(result.command); command.reset(); result = registry->find("scoped_test"); ASSERT_FALSE(result.command); } TEST_F(Commands, ScopedSubcommandInsertReset) { auto command = registry->scoped_insert("scoped_test"); command->insert("scoped_subcommand"); auto result = registry->find("scoped_test scoped_subcommand"); ASSERT_TRUE(result.command); command.reset(); result = registry->find("scoped_test scoped_subcommand"); ASSERT_FALSE(result.command); } TEST_F(Commands, ScopedCommandPtrInsertReset) { auto root = commands::create("foo"); root->insert("bar"); auto scoped = registry->scoped_insert(root); auto result = registry->find("foo bar"); ASSERT_TRUE(result.command); scoped.reset(); result = registry->find("foo bar"); ASSERT_FALSE(result.command); } TEST_F(Commands, ScopedCommandRAIIScope) { auto root = commands::create("foo"); root->insert("bar"); { auto scoped = registry->scoped_insert(root); auto result = registry->find("foo bar"); ASSERT_TRUE(result.command); } auto result = registry->find("foo bar"); ASSERT_FALSE(result.command); } TEST_F(Commands, ScopedCommandRelease) { auto root = commands::create("foo"); root->insert("bar"); { auto scoped = registry->scoped_insert(root); auto result = registry->find("foo bar"); ASSERT_TRUE(result.command); scoped.release(); // not a leak, just downgrades to non-scoped } auto result = registry->find("foo bar"); ASSERT_TRUE(result.command); }