Files
roro9stack/test/test_shell_log/test_shell_log.cpp
T
twislaandClaude Opus 5.5 0fdbb5b1ed
CI / build (pull_request) Successful in 1m38s
Site / build (pull_request) Successful in 9s
Shell: Tab completes every word of a command, and * and ? stand for several files (#67)
Tab used to complete a command's first word only. It now follows the help
text word by word: `lora st` gives `lora status`, `gnss track ` lists
`start  stop`. The words are read from the help text as written, so a new
command completes with no table to keep; the Shell's own words are added in
the same notation.

`*` and `?` in the last part of a path, for ls, du, rm, cp and mv, from the
Shell and both consoles. The command runs once for each name matched, lined
up and run by the main loop as each finishes; `cancel` empties the line-up.
64 matches at most, refused whole past that. In the Shell, rm with a pattern
asks once, with the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-07 12:47:10 +02:00

184 lines
9.0 KiB
C++

#include <unity.h>
#include <string>
#include <vector>
#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 <path> | mkdir <path> | rm [-r] [-f] <path> the SD card\n"
"lora probe | lora status | lora rx on|off | lora preset <name> 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 <sentence without $ and checksum>\n"
"log level <0-5> ESP-IDF log level\n"
"wifi ip <ssid> dhcp | wifi ip ... try <seconds> | wifi ip keep a Saved Network's IP setting\n"
"sd card | sd list | cat <path> | log <text> | burst\n"
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only)\n"
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary\n"
"Irc | Notes | Storage open that App\n";
static std::string tab(const std::string& typed, std::vector<std::string>* candidates = nullptr) {
std::vector<std::string> matches;
std::string out = completeWords(typed, kHelp, matches);
if (candidates) *candidates = matches;
return out;
}
void test_tab_completes_the_first_word() {
std::vector<std::string> 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<std::string> 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 <ssid> 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<std::string> 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 <name>: 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<std::string> root = {"captures/", "gemini/", "gnss/", "irc/", "notes/", "updates/", "wifi/", "readme.txt"};
std::vector<std::string> 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<std::string> 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();
}