Files
roro9stack/test/test_shell_log/test_shell_log.cpp
T
twislaandClaude Opus 5.5 d17d10948d
CI / build (pull_request) Successful in 1m48s
Site / build (pull_request) Successful in 10s
Shell: Tab completes a path on the SD card (#67)
Past the command's name, Tab completes the word being typed as a path: a
folder keeps its slash to go on from, a file completed whole gets a space,
several candidates are listed. Any case typed, the name's own is taken.
After a file command the first slash is understood (`cat no` is /no).
The folder is read on the storage task, bounded to 400 entries looked at
and 24 candidates.

489 host tests (2 new). Checked on the device: a folder, a file inside it,
several candidates, no slash, another case, nothing matching.

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

142 lines
6.8 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 <path> the SD card\n"
"lora probe | lora status | lora rx on|off the LoRa radio\n"
"log level <0-5> ESP-IDF log level\n"
"sd card | sd list | cat <path> | log <text> | burst\n"
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary\n"
"Irc | Notes | Storage open that App\n";
void test_command_words_come_from_the_help_text() {
auto words = commandWords(kHelp);
const char* expected[] = {"info", "ls", "du", "mkdir", "rm", "lora", "log", "sd", "cat", "burst", "get", "put", "screenshot", "Irc", "Notes", "Storage"};
TEST_ASSERT_EQUAL(sizeof expected / sizeof expected[0], words.size());
for (size_t i = 0; i < words.size(); i++) TEST_ASSERT_EQUAL_STRING(expected[i], words[i].c_str());
}
void test_tab_completes_the_first_word() {
auto words = commandWords(kHelp);
std::vector<std::string> matches;
TEST_ASSERT_EQUAL_STRING("info ", completeCommand("in", words, matches).c_str()); // the only one: with its space
TEST_ASSERT_EQUAL(0, matches.size());
TEST_ASSERT_EQUAL_STRING("l", completeCommand("l", words, matches).c_str()); // ls, lora, log: they agree on no more
TEST_ASSERT_EQUAL(3, matches.size());
TEST_ASSERT_EQUAL_STRING("mkdir ", completeCommand("m", words, matches).c_str());
TEST_ASSERT_EQUAL_STRING("lo", completeCommand("lo", words, matches).c_str()); // lora, log
TEST_ASSERT_EQUAL(2, matches.size());
TEST_ASSERT_EQUAL_STRING("zz", completeCommand("zz", words, matches).c_str()); // nothing: as it was
TEST_ASSERT_EQUAL_STRING("ls /no", completeCommand("ls /no", words, matches).c_str()); // past the first word
TEST_ASSERT_EQUAL_STRING("", completeCommand("", words, matches).c_str());
TEST_ASSERT_EQUAL_STRING("Notes ", completeCommand("N", words, matches).c_str()); // an App: a capital
TEST_ASSERT_EQUAL_STRING("n", completeCommand("n", words, matches).c_str()); // and no command starts with n here
}
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_command_words_come_from_the_help_text);
RUN_TEST(test_tab_completes_the_first_word);
RUN_TEST(test_a_later_word_is_taken_apart_as_a_path);
RUN_TEST(test_tab_completes_a_path);
return UNITY_END();
}