Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
8.6 KiB

Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
  1. #include <unity.h>
  2. #include <Arduino.h>
  3. #include <StreamString.h>
  4. #include <terminal_commands.h>
  5. // TODO: should we just use std::function at this point?
  6. // we don't actually benefit from having basic ptr functions in handler
  7. // test would be simplified too, we would no longer need to have static vars
  8. // Got the idea from the Embedis test suite, set up a proxy for StreamString
  9. // Real terminal processing happens with ringbuffer'ed stream
  10. struct IOStreamString : public Stream {
  11. StreamString in;
  12. StreamString out;
  13. size_t write(uint8_t ch) final override {
  14. return in.write(ch);
  15. }
  16. int read() final override {
  17. return out.read();
  18. }
  19. int available() final override {
  20. return out.available();
  21. }
  22. int peek() final override {
  23. return out.peek();
  24. }
  25. void flush() final override {
  26. out.flush();
  27. }
  28. };
  29. // We need to make sure that our changes to split_args actually worked
  30. void test_hex_codes() {
  31. static bool abc_done = false;
  32. terminal::Terminal::addCommand("abc", [](const terminal::CommandContext& ctx) {
  33. TEST_ASSERT_EQUAL(2, ctx.argc);
  34. TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[0].c_str());
  35. TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[1].c_str());
  36. abc_done = true;
  37. });
  38. IOStreamString str;
  39. str.out += String("abc \"\x61\x62\x63\"\r\n");
  40. terminal::Terminal handler(str);
  41. TEST_ASSERT_EQUAL(
  42. terminal::Terminal::Result::Command,
  43. handler.processLine()
  44. );
  45. TEST_ASSERT(abc_done);
  46. }
  47. // Ensure that we can register multiple commands (at least 3, might want to test much more in the future?)
  48. // Ensure that registered commands can be called and they are called in order
  49. void test_multiple_commands() {
  50. // set up counter to be chained between commands
  51. static int command_calls = 0;
  52. terminal::Terminal::addCommand("test1", [](const terminal::CommandContext& ctx) {
  53. TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
  54. TEST_ASSERT_EQUAL(0, command_calls);
  55. command_calls = 1;
  56. });
  57. terminal::Terminal::addCommand("test2", [](const terminal::CommandContext& ctx) {
  58. TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
  59. TEST_ASSERT_EQUAL(1, command_calls);
  60. command_calls = 2;
  61. });
  62. terminal::Terminal::addCommand("test3", [](const terminal::CommandContext& ctx) {
  63. TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
  64. TEST_ASSERT_EQUAL(2, command_calls);
  65. command_calls = 3;
  66. });
  67. IOStreamString str;
  68. str.out += String("test1\r\ntest2\r\ntest3\r\n");
  69. terminal::Terminal handler(str);
  70. // each processing step only executes a single command
  71. static int process_counter = 0;
  72. handler.process([](terminal::Terminal::Result result) -> bool {
  73. if (process_counter == 3) {
  74. TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::NoInput);
  75. return false;
  76. } else {
  77. TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::Command);
  78. ++process_counter;
  79. return true;
  80. }
  81. TEST_FAIL_MESSAGE("Should not be reached");
  82. return false;
  83. });
  84. TEST_ASSERT_EQUAL(3, command_calls);
  85. TEST_ASSERT_EQUAL(3, process_counter);
  86. }
  87. void test_command() {
  88. static int counter = 0;
  89. terminal::Terminal::addCommand("test.command", [](const terminal::CommandContext& ctx) {
  90. TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
  91. ++counter;
  92. });
  93. IOStreamString str;
  94. terminal::Terminal handler(str);
  95. TEST_ASSERT_EQUAL_MESSAGE(
  96. terminal::Terminal::Result::NoInput, handler.processLine(),
  97. "We have not read anything yet"
  98. );
  99. str.out += String("test.command\r\n");
  100. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  101. TEST_ASSERT_EQUAL_MESSAGE(1, counter, "At this time `test.command` was called just once");
  102. str.out += String("test.command");
  103. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Pending, handler.processLine());
  104. TEST_ASSERT_EQUAL_MESSAGE(1, counter, "We are waiting for either \\r\\n or \\n, handler still has data buffered");
  105. str.out += String("\r\n");
  106. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  107. TEST_ASSERT_EQUAL_MESSAGE(2, counter, "We should call `test.command` the second time");
  108. str.out += String("test.command\n");
  109. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  110. TEST_ASSERT_EQUAL_MESSAGE(3, counter, "We should call `test.command` the third time, with just LF");
  111. }
  112. // Ensure that we can properly handle arguments
  113. void test_command_args() {
  114. static bool waiting = false;
  115. terminal::Terminal::addCommand("test.command.arg1", [](const terminal::CommandContext& ctx) {
  116. TEST_ASSERT_EQUAL(2, ctx.argc);
  117. waiting = false;
  118. });
  119. terminal::Terminal::addCommand("test.command.arg1_empty", [](const terminal::CommandContext& ctx) {
  120. TEST_ASSERT_EQUAL(2, ctx.argc);
  121. TEST_ASSERT(!ctx.argv[1].length());
  122. waiting = false;
  123. });
  124. IOStreamString str;
  125. terminal::Terminal handler(str);
  126. waiting = true;
  127. str.out += String("test.command.arg1 test\r\n");
  128. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  129. TEST_ASSERT(!waiting);
  130. waiting = true;
  131. str.out += String("test.command.arg1_empty \"\"\r\n");
  132. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  133. TEST_ASSERT(!waiting);
  134. }
  135. // Ensure that we return error when nothing was handled, but we kept feeding the processLine() with data
  136. void test_buffer() {
  137. IOStreamString str;
  138. str.out += String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
  139. terminal::Terminal handler(str, str.out.available() - 8);
  140. TEST_ASSERT_EQUAL(terminal::Terminal::Result::BufferOverflow, handler.processLine());
  141. }
  142. // sdssplitargs returns nullptr when quotes are not terminated and empty char for an empty string. we treat it all the same
  143. void test_quotes() {
  144. terminal::Terminal::addCommand("test.quotes", [](const terminal::CommandContext& ctx) {
  145. for (auto& arg : ctx.argv) {
  146. TEST_MESSAGE(arg.c_str());
  147. }
  148. TEST_FAIL_MESSAGE("`test.quotes` should not be called");
  149. });
  150. IOStreamString str;
  151. terminal::Terminal handler(str);
  152. str.out += String("test.quotes \"quote without a pair\r\n");
  153. TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
  154. str.out += String("test.quotes 'quote without a pair\r\n");
  155. TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
  156. TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
  157. }
  158. // we specify that commands lowercase == UPPERCASE, both with hashed values and with equality functions
  159. // (internal note: we use std::unordered_map at this time)
  160. void test_case_insensitive() {
  161. terminal::Terminal::addCommand("test.lowercase1", [](const terminal::CommandContext& ctx) {
  162. __asm__ volatile ("nop");
  163. });
  164. terminal::Terminal::addCommand("TEST.LOWERCASE1", [](const terminal::CommandContext& ctx) {
  165. TEST_FAIL_MESSAGE("`test.lowercase1` was already registered, this should not be registered / called");
  166. });
  167. IOStreamString str;
  168. terminal::Terminal handler(str);
  169. str.out += String("TeSt.lOwErCaSe1\r\n");
  170. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  171. }
  172. // We can use command ctx.output to send something back into the stream
  173. void test_output() {
  174. terminal::Terminal::addCommand("test.output", [](const terminal::CommandContext& ctx) {
  175. if (ctx.argc != 2) return;
  176. ctx.output.print(ctx.argv[1]);
  177. });
  178. IOStreamString str;
  179. terminal::Terminal handler(str);
  180. char match[] = "test1234567890";
  181. str.out += String("test.output ") + String(match) + String("\r\n");
  182. TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
  183. TEST_ASSERT_EQUAL_STRING(match, str.in.c_str());
  184. }
  185. // When adding test functions, don't forget to add RUN_TEST(...) in the main()
  186. int main(int argc, char** argv) {
  187. UNITY_BEGIN();
  188. RUN_TEST(test_command);
  189. RUN_TEST(test_command_args);
  190. RUN_TEST(test_multiple_commands);
  191. RUN_TEST(test_hex_codes);
  192. RUN_TEST(test_buffer);
  193. RUN_TEST(test_quotes);
  194. RUN_TEST(test_case_insensitive);
  195. RUN_TEST(test_output);
  196. UNITY_END();
  197. }