test_command_system.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #include "gtest/gtest.h"
  2. #include "lumacs/command_system.hpp"
  3. #include "lumacs/editor_core.hpp"
  4. #include "lumacs/minibuffer_manager.hpp" // For interactive commands
  5. #include <string>
  6. #include <vector>
  7. #include <memory>
  8. #include <algorithm> // For std::find
  9. using namespace lumacs;
  10. // Test fixture for CommandSystem tests
  11. class CommandSystemTest : public ::testing::Test {
  12. protected:
  13. EditorCore core;
  14. CommandSystem* command_system; // Raw pointer to manager owned by core
  15. void SetUp() override {
  16. // EditorCore's constructor performs full initialization.
  17. // We get references to the managers owned by 'core'.
  18. command_system = &core.command_system();
  19. // Ensure initial clean state by clearing all commands registered by init.lua (if any)
  20. command_system->clear_commands();
  21. // Clear buffer and reset cursor
  22. if (core.buffer().line_count() > 0) {
  23. core.buffer().clear();
  24. }
  25. core.set_cursor({0,0});
  26. }
  27. void TearDown() override {
  28. // Nothing specific needed. core will clean up its managers.
  29. }
  30. };
  31. TEST_F(CommandSystemTest, RegisterAndExecuteBasicCommand) {
  32. bool command_executed = false;
  33. command_system->register_command("test-command",
  34. [&command_executed](CommandContext& ctx) -> CommandResult {
  35. (void)ctx; // Mark ctx as unused
  36. command_executed = true;
  37. return {CommandStatus::Success, "Command executed"};
  38. },
  39. "A simple test command.");
  40. CommandResult result = command_system->execute("test-command", {});
  41. ASSERT_EQ(result.status, CommandStatus::Success);
  42. ASSERT_EQ(result.message, "Command executed");
  43. ASSERT_TRUE(command_executed);
  44. }
  45. TEST_F(CommandSystemTest, ExecuteNonExistentCommand) {
  46. CommandResult result = command_system->execute("non-existent-command", {});
  47. ASSERT_EQ(result.status, CommandStatus::Failure);
  48. ASSERT_EQ(result.message, "Unknown command or alias: non-existent-command");
  49. }
  50. TEST_F(CommandSystemTest, RegisterWithAliasAndExecute) {
  51. bool command_executed = false;
  52. command_system->register_command("aliased-command",
  53. [&command_executed](CommandContext& ctx) -> CommandResult {
  54. (void)ctx; // Mark ctx as unused
  55. command_executed = true;
  56. return {CommandStatus::Success, "Aliased command executed"};
  57. },
  58. "A command with an alias.", false, "", {"alias1", "alias2"});
  59. CommandResult result = command_system->execute("alias1", {});
  60. ASSERT_EQ(result.status, CommandStatus::Success);
  61. ASSERT_EQ(result.message, "Aliased command executed");
  62. ASSERT_TRUE(command_executed);
  63. command_executed = false; // Reset for next execution
  64. result = command_system->execute("alias2", {});
  65. ASSERT_EQ(result.status, CommandStatus::Success);
  66. ASSERT_EQ(result.message, "Aliased command executed");
  67. ASSERT_TRUE(command_executed);
  68. // Should also work with canonical name
  69. command_executed = false;
  70. result = command_system->execute("aliased-command", {});
  71. ASSERT_EQ(result.status, CommandStatus::Success);
  72. ASSERT_EQ(result.message, "Aliased command executed");
  73. ASSERT_TRUE(command_executed);
  74. }
  75. TEST_F(CommandSystemTest, RegisterCommandOverwrite) {
  76. bool command_executed_v1 = false;
  77. command_system->register_command("overwrite-cmd",
  78. [&command_executed_v1](CommandContext& ctx) -> CommandResult {
  79. (void)ctx;
  80. command_executed_v1 = true;
  81. return {CommandStatus::Success, "V1"};
  82. });
  83. // Overwrite with V2
  84. bool command_executed_v2 = false;
  85. command_system->register_command("overwrite-cmd",
  86. [&command_executed_v2](CommandContext& ctx) -> CommandResult {
  87. (void)ctx;
  88. command_executed_v2 = true;
  89. return {CommandStatus::Success, "V2"};
  90. });
  91. CommandResult result = command_system->execute("overwrite-cmd", {});
  92. ASSERT_EQ(result.status, CommandStatus::Success);
  93. ASSERT_EQ(result.message, "V2");
  94. ASSERT_FALSE(command_executed_v1); // V1 should not have been called
  95. ASSERT_TRUE(command_executed_v2); // V2 should have been called
  96. }
  97. TEST_F(CommandSystemTest, AliasConflictResolution) {
  98. // Register a command that will be canonical
  99. command_system->register_command("master-cmd",
  100. [](CommandContext& ctx) {
  101. (void)ctx;
  102. return CommandResult{CommandStatus::Success, ""};
  103. },
  104. "Master command doc.", false, "", {"alias-conflict"});
  105. // Try to register another command whose alias conflicts with an existing canonical command
  106. // Note: The current implementation logs a warning to cerr, which we don't capture here,
  107. // but we verify the state of command registration.
  108. command_system->register_command("other-cmd",
  109. [](CommandContext& ctx) {
  110. (void)ctx;
  111. return CommandResult{CommandStatus::Success, ""};
  112. },
  113. "Other command doc.", false, "", {"alias-conflict"}); // "alias-conflict" clashes with "master-cmd" alias
  114. std::vector<std::string> names = command_system->get_command_names();
  115. ASSERT_TRUE(std::find(names.begin(), names.end(), "master-cmd") != names.end());
  116. ASSERT_TRUE(std::find(names.begin(), names.end(), "other-cmd") != names.end());
  117. ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-conflict") != names.end());
  118. }
  119. TEST_F(CommandSystemTest, GetCommandNames) {
  120. command_system->register_command("cmd-a",
  121. [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
  122. "Cmd A");
  123. command_system->register_command("cmd-b",
  124. [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
  125. "Cmd B", false, "", {"alias-b"});
  126. std::vector<std::string> names = command_system->get_command_names();
  127. ASSERT_EQ(names.size(), 3);
  128. ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-a") != names.end());
  129. ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-b") != names.end());
  130. ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-b") != names.end());
  131. }
  132. TEST_F(CommandSystemTest, GetCommandDocString) {
  133. command_system->register_command("doc-cmd",
  134. [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
  135. "This is documentation.");
  136. std::optional<std::string> doc = command_system->get_command_doc_string("doc-cmd");
  137. ASSERT_TRUE(doc.has_value());
  138. ASSERT_EQ(doc.value(), "This is documentation.");
  139. // For alias
  140. command_system->register_command("cmd-alias",
  141. [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; },
  142. "Doc for alias", false, "", {"alias-for-doc"});
  143. doc = command_system->get_command_doc_string("alias-for-doc");
  144. ASSERT_TRUE(doc.has_value());
  145. ASSERT_EQ(doc.value(), "Doc for alias");
  146. // Non-existent
  147. doc = command_system->get_command_doc_string("non-existent");
  148. ASSERT_FALSE(doc.has_value());
  149. }
  150. // Tests for CommandContext argument accessors
  151. TEST_F(CommandSystemTest, CommandContextGetStringArg) {
  152. // Use the fixture's core.minibuffer_manager()
  153. CommandContext ctx(core, core.minibuffer_manager(), {"arg1", "arg2"});
  154. ASSERT_EQ(ctx.get_string_arg(0).value(), "arg1");
  155. ASSERT_EQ(ctx.get_string_arg(1).value(), "arg2");
  156. ASSERT_FALSE(ctx.get_string_arg(2).has_value()); // Out of bounds
  157. }
  158. TEST_F(CommandSystemTest, CommandContextGetIntArg) {
  159. // Use the fixture's core.minibuffer_manager()
  160. CommandContext ctx(core, core.minibuffer_manager(), {"123", "-45", "not-a-number", "2147483648"}); // 2^31 for out of int range
  161. ASSERT_EQ(ctx.get_int_arg(0).value(), 123);
  162. ASSERT_EQ(ctx.get_int_arg(1).value(), -45);
  163. ASSERT_FALSE(ctx.get_int_arg(2).has_value()); // Invalid argument
  164. // Test for out of range (std::stoull can handle it, but stoi might throw)
  165. // std::stoi throws std::out_of_range for 2147483648
  166. ASSERT_FALSE(ctx.get_int_arg(3).has_value());
  167. ASSERT_FALSE(ctx.get_int_arg(4).has_value()); // Out of bounds
  168. // Ensure the message set by stoi failure is not propagating or crashing
  169. }
  170. TEST_F(CommandSystemTest, ExecuteInteractiveBasic) {
  171. // Register a simple interactive command that expects one string argument
  172. command_system->register_command("interactive-string-cmd",
  173. [](CommandContext& ctx) -> CommandResult {
  174. auto arg = ctx.get_string_arg(0);
  175. if (arg) {
  176. return {CommandStatus::Success, "Got string: " + arg.value()};
  177. }
  178. return {CommandStatus::Failure, "No argument provided"};
  179. },
  180. "An interactive command.", true, "s"); // 's' for string argument
  181. // Execute interactive command - should return PendingInput
  182. CommandResult result = command_system->execute_interactive("interactive-string-cmd");
  183. ASSERT_EQ(result.status, CommandStatus::PendingInput);
  184. ASSERT_EQ(result.message, "Waiting for user input.");
  185. // MinibufferManager should now be active and waiting for input
  186. ASSERT_TRUE(core.minibuffer_manager().is_active());
  187. ASSERT_EQ(core.minibuffer_manager().get_prompt(), "String: ");
  188. // Simulate input via MinibufferManager
  189. core.minibuffer_manager().handle_key_event("t");
  190. core.minibuffer_manager().handle_key_event("e");
  191. core.minibuffer_manager().handle_key_event("s");
  192. core.minibuffer_manager().handle_key_event("t");
  193. core.minibuffer_manager().handle_key_event("Return"); // Submit
  194. // After submission, the command should have executed
  195. ASSERT_FALSE(core.minibuffer_manager().is_active());
  196. // The result is communicated via side-effects (EditorCore message) since it's async
  197. ASSERT_EQ(core.last_message(), "Got string: test");
  198. }
  199. TEST_F(CommandSystemTest, ExecuteInteractiveCancel) {
  200. command_system->register_command("interactive-cancel-cmd",
  201. [](CommandContext& ctx) -> CommandResult {
  202. (void)ctx; // Mark ctx as unused
  203. return {CommandStatus::Success, "Should not be executed"};
  204. },
  205. "An interactive command that can be cancelled.", true, "s");
  206. CommandResult result = command_system->execute_interactive("interactive-cancel-cmd");
  207. ASSERT_EQ(result.status, CommandStatus::PendingInput);
  208. // Simulate cancel via MinibufferManager
  209. core.minibuffer_manager().handle_key_event("Escape"); // Cancel
  210. ASSERT_FALSE(core.minibuffer_manager().is_active());
  211. // We expect the command NOT to be executed
  212. ASSERT_NE(core.last_message(), "Should not be executed");
  213. }