Public Access
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
183 lines
9.4 KiB
C++
183 lines
9.4 KiB
C++
#include <unity.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "file_names.h"
|
|
|
|
using namespace roro::files;
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
void test_path_parts() {
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/saved", parentOf("/gemini/saved/index.gmi").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/", parentOf("/gemini").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/", parentOf("/").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("index.gmi", baseName("/gemini/saved/index.gmi").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", baseName("/").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/a.gmi", joinPath("/gemini", "a.gmi").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/a.gmi", joinPath("/", "a.gmi").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("gmi", extensionOf("Index.GMI").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("pcap", extensionOf("20261005-183340.pcap").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", extensionOf("README").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", extensionOf(".hidden").c_str());
|
|
}
|
|
|
|
void test_inside() {
|
|
TEST_ASSERT_TRUE(isInside("/gemini/cache/page0.gmi", "/gemini/cache"));
|
|
TEST_ASSERT_TRUE(isInside("/gemini/cache", "/gemini/cache"));
|
|
TEST_ASSERT_FALSE(isInside("/gemini/cached", "/gemini/cache")); // a longer name isn't inside
|
|
TEST_ASSERT_TRUE(isInside("/irc/x.log", "/"));
|
|
}
|
|
|
|
// Q131: names typed for rename and new folder.
|
|
void test_names_checked() {
|
|
TEST_ASSERT_EQUAL_STRING("", checkName("notes 2026.txt").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", checkName("#roro").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't be empty", checkName("").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't contain /", checkName("a/b").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't contain :", checkName("12:30.txt").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't contain ?", checkName("what?").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("\".\" and \"..\" aren't names", checkName("..").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't end with a dot or a space", checkName("notes.").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can't end with a dot or a space", checkName("notes ").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A name can be 64 characters at most", checkName(std::string(65, 'a')).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", checkName(std::string(64, 'a')).c_str());
|
|
}
|
|
|
|
// Q130: what can't be renamed, moved or deleted, and why.
|
|
void test_read_only() {
|
|
std::vector<std::string> inUse = {"/irc/irc.libera.chat/#roro/2026-10-06.log", "/gnss/tracks/20261006-071500.gpx"};
|
|
TEST_ASSERT_EQUAL_STRING("", whyReadOnly("/gemini/saved/a.gmi", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", whyReadOnly("/irc/irc.libera.chat/#roro/2026-10-05.log", inUse).c_str()); // yesterday's
|
|
TEST_ASSERT_EQUAL_STRING("", whyReadOnly("/updates/roro9stack-v0.8.1.ota", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", whyReadOnly("/gemini/saved", inUse).c_str()); // a folder below the top level
|
|
|
|
TEST_ASSERT_EQUAL_STRING("It's being written right now", whyReadOnly("/gnss/tracks/20261006-071500.gpx", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("It holds a file that's being written right now", whyReadOnly("/gnss/tracks", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("It holds a file that's being written right now", whyReadOnly("/irc/irc.libera.chat", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/cache is the Gemini App's working space", whyReadOnly("/gemini/cache/page0.gmi", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/cache is the Gemini App's working space", whyReadOnly("/gemini/cache", inUse).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("The firmware keeps its files in /irc", whyReadOnly("/irc", {}).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("The firmware keeps its files in /captures", whyReadOnly("/captures", {}).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("That's the card itself", whyReadOnly("/", {}).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", whyReadOnly("/my stuff", {}).c_str()); // a top-level folder of the user's own
|
|
}
|
|
|
|
// Where something may be put (paste, new folder): not into the cache, not into itself.
|
|
void test_destinations() {
|
|
TEST_ASSERT_EQUAL_STRING("", whyNotInto("/gemini/saved/a.gmi", "/notes").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", whyNotInto("/gemini/saved/a.gmi", "/").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/cache is the Gemini App's working space", whyNotInto("/notes/a.txt", "/gemini/cache").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A folder can't go inside itself", whyNotInto("/gemini/saved", "/gemini/saved/sub").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("A folder can't go inside itself", whyNotInto("/gemini/saved", "/gemini/saved").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("It's already there", whyNotInto("/gemini/saved/a.gmi", "/gemini/saved").c_str());
|
|
}
|
|
|
|
// A copy next to the original needs another name: "a (2).gmi".
|
|
void test_copy_names() {
|
|
TEST_ASSERT_EQUAL_STRING("a (2).gmi", copyName("a.gmi", 2).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("README (3)", copyName("README", 3).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("archive.tar (2).gz", copyName("archive.tar.gz", 2).c_str());
|
|
}
|
|
|
|
// Q134: which viewer opens a file.
|
|
void test_kinds() {
|
|
TEST_ASSERT_TRUE(kindOf("2026-10-06.log") == FileKind::Text);
|
|
TEST_ASSERT_TRUE(kindOf("index.gmi") == FileKind::Text);
|
|
TEST_ASSERT_TRUE(kindOf("2026-10-05.CSV") == FileKind::Text);
|
|
TEST_ASSERT_TRUE(kindOf("note.txt") == FileKind::Text);
|
|
TEST_ASSERT_TRUE(kindOf("20261006-071500.gpx") == FileKind::Gpx);
|
|
TEST_ASSERT_TRUE(kindOf("20261005-183340.pcap") == FileKind::Pcap);
|
|
TEST_ASSERT_TRUE(kindOf("roro9stack-v0.8.1.ota") == FileKind::Ota);
|
|
TEST_ASSERT_TRUE(kindOf("firmware.bin") == FileKind::Unknown); // sniffed: text or a hex dump
|
|
TEST_ASSERT_TRUE(opensAtEnd("2026-10-06.log"));
|
|
TEST_ASSERT_FALSE(opensAtEnd("index.gmi"));
|
|
}
|
|
|
|
void test_looks_like_text() {
|
|
const char text[] = "line one\nline two with an \xC3\xA9\r\n\ttabbed";
|
|
TEST_ASSERT_TRUE(looksLikeText(reinterpret_cast<const uint8_t*>(text), sizeof text - 1));
|
|
const uint8_t binary[] = {0xE9, 0x05, 0x02, 0x00, 0x10, 0x00, 0x00, 0x40};
|
|
TEST_ASSERT_FALSE(looksLikeText(binary, sizeof binary));
|
|
TEST_ASSERT_TRUE(looksLikeText(nullptr, 0)); // an empty file reads as text
|
|
}
|
|
|
|
void test_rm_takes_its_switches_like_unix() {
|
|
auto a = parseRm("/notes/a.txt");
|
|
TEST_ASSERT_FALSE(a.recursive);
|
|
TEST_ASSERT_FALSE(a.force);
|
|
TEST_ASSERT_EQUAL_STRING("/notes/a.txt", a.path.c_str());
|
|
a = parseRm("-r /gemini/saved");
|
|
TEST_ASSERT_TRUE(a.recursive);
|
|
TEST_ASSERT_FALSE(a.force);
|
|
TEST_ASSERT_EQUAL_STRING("/gemini/saved", a.path.c_str());
|
|
for (const char* both : {"-rf /x", "-fr /x", "-r -f /x", "-f -r /x", "-R -f /x"}) {
|
|
a = parseRm(both);
|
|
TEST_ASSERT_TRUE(a.recursive);
|
|
TEST_ASSERT_TRUE(a.force);
|
|
TEST_ASSERT_EQUAL_STRING("/x", a.path.c_str());
|
|
}
|
|
a = parseRm("-f /a file with spaces.txt ");
|
|
TEST_ASSERT_TRUE(a.force);
|
|
TEST_ASSERT_EQUAL_STRING("/a file with spaces.txt", a.path.c_str());
|
|
a = parseRm("-x /odd"); // not a switch of rm: taken as the name
|
|
TEST_ASSERT_FALSE(a.force);
|
|
TEST_ASSERT_EQUAL_STRING("-x /odd", a.path.c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", parseRm("-rf").path.c_str());
|
|
}
|
|
|
|
void test_a_pattern_matches_names() {
|
|
TEST_ASSERT_TRUE(globMatch("*.txt", "notes.txt"));
|
|
TEST_ASSERT_TRUE(globMatch("*.TXT", "notes.txt")); // the card doesn't tell cases apart
|
|
TEST_ASSERT_FALSE(globMatch("*.txt", "notes.txt.bak"));
|
|
TEST_ASSERT_TRUE(globMatch("*", "anything"));
|
|
TEST_ASSERT_TRUE(globMatch("*", ""));
|
|
TEST_ASSERT_TRUE(globMatch("a?c", "abc"));
|
|
TEST_ASSERT_FALSE(globMatch("a?c", "ac"));
|
|
TEST_ASSERT_FALSE(globMatch("a?c", "abbc"));
|
|
TEST_ASSERT_TRUE(globMatch("2026-10-0?.log", "2026-10-07.log"));
|
|
TEST_ASSERT_TRUE(globMatch("a*b*c", "a-then-b-then-c"));
|
|
TEST_ASSERT_TRUE(globMatch("a*b*c", "abbbc"));
|
|
TEST_ASSERT_FALSE(globMatch("a*b*c", "a-then-c"));
|
|
TEST_ASSERT_TRUE(globMatch("*a", "banana")); // the star has to give back and try again
|
|
TEST_ASSERT_FALSE(globMatch("*ab", "banana"));
|
|
TEST_ASSERT_TRUE(globMatch("file-000?.txt", "file-0001.txt"));
|
|
TEST_ASSERT_TRUE(globMatch("exact", "EXACT"));
|
|
TEST_ASSERT_FALSE(globMatch("", "x"));
|
|
TEST_ASSERT_TRUE(globMatch("**", "x"));
|
|
}
|
|
|
|
void test_a_path_with_a_pattern_is_taken_apart() {
|
|
std::string folder, pattern;
|
|
TEST_ASSERT_TRUE(splitGlob("/notes/*.txt", folder, pattern));
|
|
TEST_ASSERT_EQUAL_STRING("/notes", folder.c_str());
|
|
TEST_ASSERT_EQUAL_STRING("*.txt", pattern.c_str());
|
|
TEST_ASSERT_TRUE(splitGlob("/*", folder, pattern));
|
|
TEST_ASSERT_EQUAL_STRING("/", folder.c_str());
|
|
TEST_ASSERT_EQUAL_STRING("*", pattern.c_str());
|
|
TEST_ASSERT_FALSE(splitGlob("/notes/a.txt", folder, pattern)); // no pattern
|
|
TEST_ASSERT_FALSE(splitGlob("/*/a.txt", folder, pattern)); // not in a folder's name
|
|
TEST_ASSERT_FALSE(splitGlob("/n*/a?.txt", folder, pattern));
|
|
TEST_ASSERT_FALSE(splitGlob("*.txt", folder, pattern)); // a path starts at the top
|
|
TEST_ASSERT_TRUE(hasGlob("a?"));
|
|
TEST_ASSERT_FALSE(hasGlob("/plain/path"));
|
|
}
|
|
|
|
int main() {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_path_parts);
|
|
RUN_TEST(test_inside);
|
|
RUN_TEST(test_names_checked);
|
|
RUN_TEST(test_read_only);
|
|
RUN_TEST(test_destinations);
|
|
RUN_TEST(test_copy_names);
|
|
RUN_TEST(test_kinds);
|
|
RUN_TEST(test_looks_like_text);
|
|
RUN_TEST(test_rm_takes_its_switches_like_unix);
|
|
RUN_TEST(test_a_pattern_matches_names);
|
|
RUN_TEST(test_a_path_with_a_pattern_is_taken_apart);
|
|
return UNITY_END();
|
|
}
|