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 |
|
- #include <unity.h>
- #include <Arduino.h>
- #include <StreamString.h>
-
- #include <terminal_commands.h>
-
- // TODO: should we just use std::function at this point?
- // we don't actually benefit from having basic ptr functions in handler
- // test would be simplified too, we would no longer need to have static vars
-
- // Got the idea from the Embedis test suite, set up a proxy for StreamString
- // Real terminal processing happens with ringbuffer'ed stream
- struct IOStreamString : public Stream {
- StreamString in;
- StreamString out;
-
- size_t write(uint8_t ch) final override {
- return in.write(ch);
- }
-
- int read() final override {
- return out.read();
- }
-
- int available() final override {
- return out.available();
- }
-
- int peek() final override {
- return out.peek();
- }
-
- void flush() final override {
- out.flush();
- }
- };
-
- // We need to make sure that our changes to split_args actually worked
-
- void test_hex_codes() {
-
- static bool abc_done = false;
-
- terminal::Terminal::addCommand("abc", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL(2, ctx.argc);
- TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[0].c_str());
- TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[1].c_str());
- abc_done = true;
- });
-
- IOStreamString str;
- str.out += String("abc \"\x61\x62\x63\"\r\n");
-
- terminal::Terminal handler(str);
-
- TEST_ASSERT_EQUAL(
- terminal::Terminal::Result::Command,
- handler.processLine()
- );
- TEST_ASSERT(abc_done);
- }
-
- // Ensure that we can register multiple commands (at least 3, might want to test much more in the future?)
- // Ensure that registered commands can be called and they are called in order
-
- void test_multiple_commands() {
-
- // set up counter to be chained between commands
- static int command_calls = 0;
-
- terminal::Terminal::addCommand("test1", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
- TEST_ASSERT_EQUAL(0, command_calls);
- command_calls = 1;
- });
- terminal::Terminal::addCommand("test2", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
- TEST_ASSERT_EQUAL(1, command_calls);
- command_calls = 2;
- });
- terminal::Terminal::addCommand("test3", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
- TEST_ASSERT_EQUAL(2, command_calls);
- command_calls = 3;
- });
-
- IOStreamString str;
- str.out += String("test1\r\ntest2\r\ntest3\r\n");
-
- terminal::Terminal handler(str);
-
- // each processing step only executes a single command
- static int process_counter = 0;
-
- handler.process([](terminal::Terminal::Result result) -> bool {
- if (process_counter == 3) {
- TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::NoInput);
- return false;
- } else {
- TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::Command);
- ++process_counter;
- return true;
- }
- TEST_FAIL_MESSAGE("Should not be reached");
- return false;
- });
- TEST_ASSERT_EQUAL(3, command_calls);
- TEST_ASSERT_EQUAL(3, process_counter);
-
- }
-
- void test_command() {
-
- static int counter = 0;
-
- terminal::Terminal::addCommand("test.command", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
- ++counter;
- });
-
- IOStreamString str;
- terminal::Terminal handler(str);
-
- TEST_ASSERT_EQUAL_MESSAGE(
- terminal::Terminal::Result::NoInput, handler.processLine(),
- "We have not read anything yet"
- );
-
- str.out += String("test.command\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT_EQUAL_MESSAGE(1, counter, "At this time `test.command` was called just once");
-
- str.out += String("test.command");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Pending, handler.processLine());
- TEST_ASSERT_EQUAL_MESSAGE(1, counter, "We are waiting for either \\r\\n or \\n, handler still has data buffered");
-
- str.out += String("\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT_EQUAL_MESSAGE(2, counter, "We should call `test.command` the second time");
-
- str.out += String("test.command\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT_EQUAL_MESSAGE(3, counter, "We should call `test.command` the third time, with just LF");
-
- }
-
- // Ensure that we can properly handle arguments
-
- void test_command_args() {
-
- static bool waiting = false;
-
- terminal::Terminal::addCommand("test.command.arg1", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL(2, ctx.argc);
- waiting = false;
- });
-
- terminal::Terminal::addCommand("test.command.arg1_empty", [](const terminal::CommandContext& ctx) {
- TEST_ASSERT_EQUAL(2, ctx.argc);
- TEST_ASSERT(!ctx.argv[1].length());
- waiting = false;
- });
-
- IOStreamString str;
- terminal::Terminal handler(str);
-
- waiting = true;
- str.out += String("test.command.arg1 test\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT(!waiting);
-
- waiting = true;
- str.out += String("test.command.arg1_empty \"\"\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT(!waiting);
-
- }
-
- // Ensure that we return error when nothing was handled, but we kept feeding the processLine() with data
-
- void test_buffer() {
-
- IOStreamString str;
- str.out += String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
-
- terminal::Terminal handler(str, str.out.available() - 8);
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::BufferOverflow, handler.processLine());
-
- }
-
- // sdssplitargs returns nullptr when quotes are not terminated and empty char for an empty string. we treat it all the same
-
- void test_quotes() {
-
- terminal::Terminal::addCommand("test.quotes", [](const terminal::CommandContext& ctx) {
- for (auto& arg : ctx.argv) {
- TEST_MESSAGE(arg.c_str());
- }
- TEST_FAIL_MESSAGE("`test.quotes` should not be called");
- });
-
- IOStreamString str;
- terminal::Terminal handler(str);
-
- str.out += String("test.quotes \"quote without a pair\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
-
- str.out += String("test.quotes 'quote without a pair\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
-
- }
-
- // we specify that commands lowercase == UPPERCASE, both with hashed values and with equality functions
- // (internal note: we use std::unordered_map at this time)
-
- void test_case_insensitive() {
-
- terminal::Terminal::addCommand("test.lowercase1", [](const terminal::CommandContext& ctx) {
- __asm__ volatile ("nop");
- });
- terminal::Terminal::addCommand("TEST.LOWERCASE1", [](const terminal::CommandContext& ctx) {
- TEST_FAIL_MESSAGE("`test.lowercase1` was already registered, this should not be registered / called");
- });
-
- IOStreamString str;
- terminal::Terminal handler(str);
-
- str.out += String("TeSt.lOwErCaSe1\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
-
- }
-
- // We can use command ctx.output to send something back into the stream
-
- void test_output() {
-
- terminal::Terminal::addCommand("test.output", [](const terminal::CommandContext& ctx) {
- if (ctx.argc != 2) return;
- ctx.output.print(ctx.argv[1]);
- });
-
- IOStreamString str;
- terminal::Terminal handler(str);
-
- char match[] = "test1234567890";
- str.out += String("test.output ") + String(match) + String("\r\n");
- TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
- TEST_ASSERT_EQUAL_STRING(match, str.in.c_str());
-
- }
-
- // When adding test functions, don't forget to add RUN_TEST(...) in the main()
-
- int main(int argc, char** argv) {
- UNITY_BEGIN();
- RUN_TEST(test_command);
- RUN_TEST(test_command_args);
- RUN_TEST(test_multiple_commands);
- RUN_TEST(test_hex_codes);
- RUN_TEST(test_buffer);
- RUN_TEST(test_quotes);
- RUN_TEST(test_case_insensitive);
- RUN_TEST(test_output);
- UNITY_END();
- }
|