|
|
@@ -0,0 +1,256 @@
|
|
|
+#include "gtest/gtest.h"
|
|
|
+#include "lumacs/command_system.hpp"
|
|
|
+#include "lumacs/editor_core.hpp"
|
|
|
+#include "lumacs/minibuffer_manager.hpp" // For interactive commands
|
|
|
+#include <string>
|
|
|
+#include <vector>
|
|
|
+#include <memory>
|
|
|
+#include <algorithm> // For std::find
|
|
|
+
|
|
|
+using namespace lumacs;
|
|
|
+
|
|
|
+// Test fixture for CommandSystem tests
|
|
|
+class CommandSystemTest : public ::testing::Test {
|
|
|
+protected:
|
|
|
+ EditorCore core;
|
|
|
+ CommandSystem* command_system; // Raw pointer to manager owned by core
|
|
|
+
|
|
|
+ void SetUp() override {
|
|
|
+ // EditorCore's constructor performs full initialization.
|
|
|
+ // We get references to the managers owned by 'core'.
|
|
|
+ command_system = &core.command_system();
|
|
|
+
|
|
|
+ // Ensure initial clean state by clearing all commands registered by init.lua (if any)
|
|
|
+ command_system->clear_commands();
|
|
|
+
|
|
|
+ // Clear buffer and reset cursor
|
|
|
+ if (core.buffer().line_count() > 0) {
|
|
|
+ core.buffer().clear();
|
|
|
+ }
|
|
|
+ core.set_cursor({0,0});
|
|
|
+ }
|
|
|
+
|
|
|
+ void TearDown() override {
|
|
|
+ // Nothing specific needed. core will clean up its managers.
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, RegisterAndExecuteBasicCommand) {
|
|
|
+ bool command_executed = false;
|
|
|
+ command_system->register_command("test-command",
|
|
|
+ [&command_executed](CommandContext& ctx) -> CommandResult {
|
|
|
+ (void)ctx; // Mark ctx as unused
|
|
|
+ command_executed = true;
|
|
|
+ return {CommandStatus::Success, "Command executed"};
|
|
|
+ },
|
|
|
+ "A simple test command.");
|
|
|
+
|
|
|
+ CommandResult result = command_system->execute("test-command", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Success);
|
|
|
+ ASSERT_EQ(result.message, "Command executed");
|
|
|
+ ASSERT_TRUE(command_executed);
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, ExecuteNonExistentCommand) {
|
|
|
+ CommandResult result = command_system->execute("non-existent-command", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Failure);
|
|
|
+ ASSERT_EQ(result.message, "Unknown command or alias: non-existent-command");
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, RegisterWithAliasAndExecute) {
|
|
|
+ bool command_executed = false;
|
|
|
+ command_system->register_command("aliased-command",
|
|
|
+ [&command_executed](CommandContext& ctx) -> CommandResult {
|
|
|
+ (void)ctx; // Mark ctx as unused
|
|
|
+ command_executed = true;
|
|
|
+ return {CommandStatus::Success, "Aliased command executed"};
|
|
|
+ },
|
|
|
+ "A command with an alias.", false, "", {"alias1", "alias2"});
|
|
|
+
|
|
|
+ CommandResult result = command_system->execute("alias1", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Success);
|
|
|
+ ASSERT_EQ(result.message, "Aliased command executed");
|
|
|
+ ASSERT_TRUE(command_executed);
|
|
|
+
|
|
|
+ command_executed = false; // Reset for next execution
|
|
|
+ result = command_system->execute("alias2", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Success);
|
|
|
+ ASSERT_EQ(result.message, "Aliased command executed");
|
|
|
+ ASSERT_TRUE(command_executed);
|
|
|
+
|
|
|
+ // Should also work with canonical name
|
|
|
+ command_executed = false;
|
|
|
+ result = command_system->execute("aliased-command", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Success);
|
|
|
+ ASSERT_EQ(result.message, "Aliased command executed");
|
|
|
+ ASSERT_TRUE(command_executed);
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, RegisterCommandOverwrite) {
|
|
|
+ bool command_executed_v1 = false;
|
|
|
+ command_system->register_command("overwrite-cmd",
|
|
|
+ [&command_executed_v1](CommandContext& ctx) -> CommandResult {
|
|
|
+ (void)ctx;
|
|
|
+ command_executed_v1 = true;
|
|
|
+ return {CommandStatus::Success, "V1"};
|
|
|
+ });
|
|
|
+
|
|
|
+ // Overwrite with V2
|
|
|
+ bool command_executed_v2 = false;
|
|
|
+ command_system->register_command("overwrite-cmd",
|
|
|
+ [&command_executed_v2](CommandContext& ctx) -> CommandResult {
|
|
|
+ (void)ctx;
|
|
|
+ command_executed_v2 = true;
|
|
|
+ return {CommandStatus::Success, "V2"};
|
|
|
+ });
|
|
|
+
|
|
|
+ CommandResult result = command_system->execute("overwrite-cmd", {});
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::Success);
|
|
|
+ ASSERT_EQ(result.message, "V2");
|
|
|
+ ASSERT_FALSE(command_executed_v1); // V1 should not have been called
|
|
|
+ ASSERT_TRUE(command_executed_v2); // V2 should have been called
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, AliasConflictResolution) {
|
|
|
+ // Register a command that will be canonical
|
|
|
+ command_system->register_command("master-cmd",
|
|
|
+ [](CommandContext& ctx) {
|
|
|
+ (void)ctx;
|
|
|
+ return CommandResult{CommandStatus::Success, ""};
|
|
|
+ },
|
|
|
+ "Master command doc.", false, "", {"alias-conflict"});
|
|
|
+
|
|
|
+ // Try to register another command whose alias conflicts with an existing canonical command
|
|
|
+ // Note: The current implementation logs a warning to cerr, which we don't capture here,
|
|
|
+ // but we verify the state of command registration.
|
|
|
+ command_system->register_command("other-cmd",
|
|
|
+ [](CommandContext& ctx) {
|
|
|
+ (void)ctx;
|
|
|
+ return CommandResult{CommandStatus::Success, ""};
|
|
|
+ },
|
|
|
+ "Other command doc.", false, "", {"alias-conflict"}); // "alias-conflict" clashes with "master-cmd" alias
|
|
|
+
|
|
|
+ std::vector<std::string> names = command_system->get_command_names();
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "master-cmd") != names.end());
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "other-cmd") != names.end());
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-conflict") != names.end());
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, GetCommandNames) {
|
|
|
+ command_system->register_command("cmd-a",
|
|
|
+ [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
|
|
|
+ "Cmd A");
|
|
|
+ command_system->register_command("cmd-b",
|
|
|
+ [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
|
|
|
+ "Cmd B", false, "", {"alias-b"});
|
|
|
+
|
|
|
+ std::vector<std::string> names = command_system->get_command_names();
|
|
|
+
|
|
|
+ ASSERT_EQ(names.size(), 3);
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-a") != names.end());
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-b") != names.end());
|
|
|
+ ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-b") != names.end());
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, GetCommandDocString) {
|
|
|
+ command_system->register_command("doc-cmd",
|
|
|
+ [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
|
|
|
+ "This is documentation.");
|
|
|
+
|
|
|
+ std::optional<std::string> doc = command_system->get_command_doc_string("doc-cmd");
|
|
|
+ ASSERT_TRUE(doc.has_value());
|
|
|
+ ASSERT_EQ(doc.value(), "This is documentation.");
|
|
|
+
|
|
|
+ // For alias
|
|
|
+ command_system->register_command("cmd-alias",
|
|
|
+ [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
|
|
|
+ "Doc for alias", false, "", {"alias-for-doc"});
|
|
|
+
|
|
|
+ doc = command_system->get_command_doc_string("alias-for-doc");
|
|
|
+ ASSERT_TRUE(doc.has_value());
|
|
|
+ ASSERT_EQ(doc.value(), "Doc for alias");
|
|
|
+
|
|
|
+ // Non-existent
|
|
|
+ doc = command_system->get_command_doc_string("non-existent");
|
|
|
+ ASSERT_FALSE(doc.has_value());
|
|
|
+}
|
|
|
+
|
|
|
+// Tests for CommandContext argument accessors
|
|
|
+TEST_F(CommandSystemTest, CommandContextGetStringArg) {
|
|
|
+ // Use the fixture's core.minibuffer_manager()
|
|
|
+ CommandContext ctx(core, core.minibuffer_manager(), {"arg1", "arg2"});
|
|
|
+
|
|
|
+ ASSERT_EQ(ctx.get_string_arg(0).value(), "arg1");
|
|
|
+ ASSERT_EQ(ctx.get_string_arg(1).value(), "arg2");
|
|
|
+ ASSERT_FALSE(ctx.get_string_arg(2).has_value()); // Out of bounds
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, CommandContextGetIntArg) {
|
|
|
+ // Use the fixture's core.minibuffer_manager()
|
|
|
+ CommandContext ctx(core, core.minibuffer_manager(), {"123", "-45", "not-a-number", "2147483648"}); // 2^31 for out of int range
|
|
|
+
|
|
|
+ ASSERT_EQ(ctx.get_int_arg(0).value(), 123);
|
|
|
+ ASSERT_EQ(ctx.get_int_arg(1).value(), -45);
|
|
|
+ ASSERT_FALSE(ctx.get_int_arg(2).has_value()); // Invalid argument
|
|
|
+ // Test for out of range (std::stoull can handle it, but stoi might throw)
|
|
|
+ // std::stoi throws std::out_of_range for 2147483648
|
|
|
+ ASSERT_FALSE(ctx.get_int_arg(3).has_value());
|
|
|
+ ASSERT_FALSE(ctx.get_int_arg(4).has_value()); // Out of bounds
|
|
|
+
|
|
|
+ // Ensure the message set by stoi failure is not propagating or crashing
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, ExecuteInteractiveBasic) {
|
|
|
+ // Register a simple interactive command that expects one string argument
|
|
|
+ command_system->register_command("interactive-string-cmd",
|
|
|
+ [](CommandContext& ctx) -> CommandResult {
|
|
|
+ auto arg = ctx.get_string_arg(0);
|
|
|
+ if (arg) {
|
|
|
+ return {CommandStatus::Success, "Got string: " + arg.value()};
|
|
|
+ }
|
|
|
+ return {CommandStatus::Failure, "No argument provided"};
|
|
|
+ },
|
|
|
+ "An interactive command.", true, "s"); // 's' for string argument
|
|
|
+
|
|
|
+ // Execute interactive command - should return PendingInput
|
|
|
+ CommandResult result = command_system->execute_interactive("interactive-string-cmd");
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::PendingInput);
|
|
|
+ ASSERT_EQ(result.message, "Waiting for user input.");
|
|
|
+
|
|
|
+ // MinibufferManager should now be active and waiting for input
|
|
|
+ ASSERT_TRUE(core.minibuffer_manager().is_active());
|
|
|
+ ASSERT_EQ(core.minibuffer_manager().get_prompt(), "String: ");
|
|
|
+
|
|
|
+ // Simulate input via MinibufferManager
|
|
|
+ core.minibuffer_manager().handle_key_event("t");
|
|
|
+ core.minibuffer_manager().handle_key_event("e");
|
|
|
+ core.minibuffer_manager().handle_key_event("s");
|
|
|
+ core.minibuffer_manager().handle_key_event("t");
|
|
|
+ core.minibuffer_manager().handle_key_event("Return"); // Submit
|
|
|
+
|
|
|
+ // After submission, the command should have executed
|
|
|
+ ASSERT_FALSE(core.minibuffer_manager().is_active());
|
|
|
+
|
|
|
+ // The result is communicated via side-effects (EditorCore message) since it's async
|
|
|
+ ASSERT_EQ(core.last_message(), "Got string: test");
|
|
|
+}
|
|
|
+
|
|
|
+TEST_F(CommandSystemTest, ExecuteInteractiveCancel) {
|
|
|
+ command_system->register_command("interactive-cancel-cmd",
|
|
|
+ [](CommandContext& ctx) -> CommandResult {
|
|
|
+ (void)ctx; // Mark ctx as unused
|
|
|
+ return {CommandStatus::Success, "Should not be executed"};
|
|
|
+ },
|
|
|
+ "An interactive command that can be cancelled.", true, "s");
|
|
|
+
|
|
|
+ CommandResult result = command_system->execute_interactive("interactive-cancel-cmd");
|
|
|
+ ASSERT_EQ(result.status, CommandStatus::PendingInput);
|
|
|
+
|
|
|
+ // Simulate cancel via MinibufferManager
|
|
|
+ core.minibuffer_manager().handle_key_event("Escape"); // Cancel
|
|
|
+
|
|
|
+ ASSERT_FALSE(core.minibuffer_manager().is_active());
|
|
|
+
|
|
|
+ // We expect the command NOT to be executed
|
|
|
+ ASSERT_NE(core.last_message(), "Should not be executed");
|
|
|
+}
|