#include #include #include #include "shell_log.h" using namespace roro; void setUp() {} void tearDown() {} static void feed(ShellLog& log, const std::string& text) { log.feed(text.data(), text.size()); } void test_lines_arrive_in_pieces() { ShellLog log; feed(log, "firmware: roro"); TEST_ASSERT_EQUAL(0, log.lines().size()); feed(log, "9stack\r\nuptime: 1s\n"); TEST_ASSERT_EQUAL(2, log.lines().size()); TEST_ASSERT_EQUAL_STRING("firmware: roro9stack", log.lines()[0].c_str()); TEST_ASSERT_EQUAL_STRING("uptime: 1s", log.lines()[1].c_str()); } void test_the_heap_line_is_never_kept() { ShellLog log; feed(log, "status: heap 105000 min 90000\nirc: connected\n"); TEST_ASSERT_EQUAL(1, log.lines().size()); TEST_ASSERT_EQUAL_STRING("irc: connected", log.lines()[0].c_str()); } void test_the_oldest_lines_go_past_4_kb() { ShellLog log; for (int i = 0; i < 200; i++) feed(log, std::string(49, 'x') + "\n"); // 10 KB in all size_t bytes = 0; for (auto& l : log.lines()) bytes += l.size() + 1; TEST_ASSERT_TRUE(bytes <= ShellLog::kMaxBytes); TEST_ASSERT_TRUE(log.lines().size() >= 80); uint32_t before = log.revision(); log.add("> info"); TEST_ASSERT_EQUAL_STRING("> info", log.lines().back().c_str()); TEST_ASSERT_TRUE(log.revision() != before); log.clear(); TEST_ASSERT_EQUAL(0, log.lines().size()); } static const char* kHelp = "info firmware, uptime, memory\n" "ls [folder] | du | mkdir | rm [-r] [-f] the SD card\n" "lora probe | lora status | lora rx on|off | lora preset the LoRa radio\n" "lora sweep on [from MHz] [to MHz] | lora sweep off | lora sweep dump RSSI across a band\n" "gnss track start|stop | gnss nmea on|off | gnss send \n" "log level <0-5> ESP-IDF log level\n" "wifi ip dhcp | wifi ip ... try | wifi ip keep a Saved Network's IP setting\n" "sd card | sd list | cat | log | burst\n" "debug on | debug token <16 to 64 characters> | debug token new (USB serial only)\n" "get | put | screenshot (Debug Console only) binary\n" "Irc | Notes | Storage open that App\n"; static std::string tab(const std::string& typed, std::vector* candidates = nullptr) { std::vector matches; std::string out = completeWords(typed, kHelp, matches); if (candidates) *candidates = matches; return out; } void test_tab_completes_the_first_word() { std::vector m; TEST_ASSERT_EQUAL_STRING("info ", tab("in", &m).c_str()); // the only one: with its space TEST_ASSERT_EQUAL(0, m.size()); TEST_ASSERT_EQUAL_STRING("l", tab("l", &m).c_str()); // ls, lora, log: they agree on no more TEST_ASSERT_EQUAL(3, m.size()); TEST_ASSERT_EQUAL_STRING("lo", tab("lo", &m).c_str()); // lora, log TEST_ASSERT_EQUAL(2, m.size()); TEST_ASSERT_EQUAL_STRING("mkdir ", tab("m").c_str()); TEST_ASSERT_EQUAL_STRING("zz", tab("zz", &m).c_str()); // nothing: as it was TEST_ASSERT_EQUAL(0, m.size()); TEST_ASSERT_EQUAL_STRING("", tab("").c_str()); TEST_ASSERT_EQUAL_STRING("Notes ", tab("N").c_str()); // an App: a capital TEST_ASSERT_EQUAL_STRING("n", tab("n").c_str()); // and no command starts with n here TEST_ASSERT_EQUAL_STRING("screenshot ", tab("sc").c_str()); } // Every word of a command, not only the first. void test_tab_completes_the_words_after_the_first() { std::vector m; TEST_ASSERT_EQUAL_STRING("lora status ", tab("lora st").c_str()); TEST_ASSERT_EQUAL_STRING("lora s", tab("lora s", &m).c_str()); // status, sweep TEST_ASSERT_EQUAL(2, m.size()); TEST_ASSERT_EQUAL_STRING("lora ", tab("lora ", &m).c_str()); // everything lora can be followed by const char* afterLora[] = {"probe", "status", "rx", "preset", "sweep"}; TEST_ASSERT_EQUAL(5, m.size()); for (size_t i = 0; i < m.size(); i++) TEST_ASSERT_EQUAL_STRING(afterLora[i], m[i].c_str()); TEST_ASSERT_EQUAL_STRING("lora rx o", tab("lora rx o", &m).c_str()); // on|off: either TEST_ASSERT_EQUAL(2, m.size()); TEST_ASSERT_EQUAL_STRING("lora rx off ", tab("lora rx of").c_str()); TEST_ASSERT_EQUAL_STRING("lora sweep dump ", tab("lora sweep d").c_str()); TEST_ASSERT_EQUAL_STRING("gnss track st", tab("gnss track st", &m).c_str()); // start, stop TEST_ASSERT_EQUAL(2, m.size()); TEST_ASSERT_EQUAL_STRING("gnss track stop ", tab("gnss track sto").c_str()); TEST_ASSERT_EQUAL_STRING("log level ", tab("log l").c_str()); TEST_ASSERT_EQUAL_STRING("wifi ip keep ", tab("wifi ip k").c_str()); // past the of its neighbours TEST_ASSERT_EQUAL_STRING("debug token new ", tab("debug token n").c_str()); TEST_ASSERT_EQUAL_STRING("sd ", tab("sd ", &m).c_str()); TEST_ASSERT_EQUAL(2, m.size()); // card, list } // Where a command's words end, Tab has nothing to say: the Shell then tries a path. void test_tab_stops_where_the_arguments_start() { std::vector m; TEST_ASSERT_EQUAL_STRING("ls /no", tab("ls /no", &m).c_str()); TEST_ASSERT_EQUAL(0, m.size()); TEST_ASSERT_EQUAL_STRING("rm ", tab("rm ", &m).c_str()); TEST_ASSERT_EQUAL(0, m.size()); TEST_ASSERT_EQUAL_STRING("lora preset Lo", tab("lora preset Lo").c_str()); // a : not a word of the command TEST_ASSERT_EQUAL_STRING("lora status x", tab("lora status x").c_str()); TEST_ASSERT_EQUAL_STRING("nope st", tab("nope st").c_str()); TEST_ASSERT_EQUAL_STRING("lora st", tab("lora st").c_str()); // two spaces } void test_a_later_word_is_taken_apart_as_a_path() { PathToComplete p; TEST_ASSERT_FALSE(splitForPath("ls", p)); // still the command TEST_ASSERT_FALSE(splitForPath("rm -r", p)); // a switch TEST_ASSERT_FALSE(splitForPath("irc say he", p)); // not a path, and irc doesn't take one TEST_ASSERT_TRUE(splitForPath("rm -r /notes/sh", p)); TEST_ASSERT_EQUAL_STRING("rm -r ", p.head.c_str()); TEST_ASSERT_EQUAL_STRING("/notes/", p.folder.c_str()); TEST_ASSERT_EQUAL_STRING("sh", p.prefix.c_str()); TEST_ASSERT_TRUE(splitForPath("ls /", p)); TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str()); TEST_ASSERT_EQUAL_STRING("", p.prefix.c_str()); TEST_ASSERT_TRUE(splitForPath("cat no", p)); // a file command: the slash is understood TEST_ASSERT_EQUAL_STRING("cat ", p.head.c_str()); TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str()); TEST_ASSERT_EQUAL_STRING("no", p.prefix.c_str()); TEST_ASSERT_TRUE(splitForPath("ls ", p)); TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str()); TEST_ASSERT_TRUE(splitForPath("gemini get /x", p)); // any word that starts with a slash TEST_ASSERT_TRUE(splitForPath("cp /notes/a.txt /gem", p)); // the second path of two TEST_ASSERT_EQUAL_STRING("cp /notes/a.txt ", p.head.c_str()); TEST_ASSERT_EQUAL_STRING("gem", p.prefix.c_str()); } void test_tab_completes_a_path() { std::vector root = {"captures/", "gemini/", "gnss/", "irc/", "notes/", "updates/", "wifi/", "readme.txt"}; std::vector matches; PathToComplete p; splitForPath("ls /no", p); TEST_ASSERT_EQUAL_STRING("ls /notes/", completePath(p, root, matches).c_str()); // a folder: its slash, to go on TEST_ASSERT_EQUAL(0, matches.size()); splitForPath("cat /re", p); TEST_ASSERT_EQUAL_STRING("cat /readme.txt ", completePath(p, root, matches).c_str()); // a file: done, a space splitForPath("ls /g", p); TEST_ASSERT_EQUAL_STRING("ls /g", completePath(p, root, matches).c_str()); // gemini, gnss: they agree on no more TEST_ASSERT_EQUAL(2, matches.size()); TEST_ASSERT_EQUAL_STRING("gemini/", matches[0].c_str()); splitForPath("ls /zz", p); TEST_ASSERT_EQUAL_STRING("ls /zz", completePath(p, root, matches).c_str()); // nothing: as it was TEST_ASSERT_EQUAL(0, matches.size()); splitForPath("rm -r /NO", p); // the card doesn't tell cases apart: the name's own case is taken TEST_ASSERT_EQUAL_STRING("rm -r /notes/", completePath(p, root, matches).c_str()); std::vector notes = {"shopping-list.txt", "Shed.txt", "todo.txt"}; splitForPath("cat /notes/sh", p); TEST_ASSERT_EQUAL_STRING("cat /notes/sh", completePath(p, notes, matches).c_str()); // "sh" and "Sh": as far as they agree TEST_ASSERT_EQUAL(2, matches.size()); splitForPath("cat /notes/", p); TEST_ASSERT_EQUAL_STRING("cat /notes/", completePath(p, notes, matches).c_str()); // everything fits: the list TEST_ASSERT_EQUAL(3, matches.size()); } int main(int, char**) { UNITY_BEGIN(); RUN_TEST(test_lines_arrive_in_pieces); RUN_TEST(test_the_heap_line_is_never_kept); RUN_TEST(test_the_oldest_lines_go_past_4_kb); RUN_TEST(test_tab_completes_the_first_word); RUN_TEST(test_tab_completes_the_words_after_the_first); RUN_TEST(test_tab_stops_where_the_arguments_start); RUN_TEST(test_a_later_word_is_taken_apart_as_a_path); RUN_TEST(test_tab_completes_a_path); return UNITY_END(); }