Public Access
F1 step 1: names, read-only rules and the packed listing (host-tested)
lib/files: path parts; names checked for rename and new folder; why something can't be renamed, moved or deleted (the Gemini cache, a file being written or a folder holding one, the firmware's top-level folders), and why it can't go into a folder; the viewer for a file by its name, with a sniff for text. FileList keeps a folder's entries packed, 256 at most, the first 256 by name whatever order the card lists them in, and sorts by name, date or size with folders first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
#include "file_list.h"
|
||||
|
||||
using namespace roro::files;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_entries() {
|
||||
FileList list;
|
||||
TEST_ASSERT_EQUAL_size_t(0, list.count());
|
||||
list.add("b.txt", 1200, 1791230400, false);
|
||||
list.add("saved", 0, 1791230000, true);
|
||||
TEST_ASSERT_EQUAL_size_t(2, list.count());
|
||||
list.sort(FileSort::Name);
|
||||
TEST_ASSERT_EQUAL_STRING("saved", list.name(0)); // folders first
|
||||
TEST_ASSERT_TRUE(list.folder(0));
|
||||
TEST_ASSERT_EQUAL_STRING("b.txt", list.name(1));
|
||||
TEST_ASSERT_EQUAL_UINT32(1200, list.size(1));
|
||||
TEST_ASSERT_EQUAL_UINT32(1791230400u, list.modified(1));
|
||||
}
|
||||
|
||||
// Q129: folders first, then by name whatever the case; `s` cycles name, date, size.
|
||||
void test_sorting() {
|
||||
FileList list;
|
||||
list.add("zeta.log", 10, 300, false);
|
||||
list.add("Alpha.log", 3000, 100, false);
|
||||
list.add("beta.log", 200, 200, false);
|
||||
list.add("tracks", 0, 50, true);
|
||||
list.add("Captures", 0, 60, true);
|
||||
|
||||
list.sort(FileSort::Name);
|
||||
const char* byName[] = {"Captures", "tracks", "Alpha.log", "beta.log", "zeta.log"};
|
||||
for (int i = 0; i < 5; i++) TEST_ASSERT_EQUAL_STRING(byName[i], list.name(i));
|
||||
|
||||
list.sort(FileSort::Date); // newest first, folders still first
|
||||
const char* byDate[] = {"Captures", "tracks", "zeta.log", "beta.log", "Alpha.log"};
|
||||
for (int i = 0; i < 5; i++) TEST_ASSERT_EQUAL_STRING(byDate[i], list.name(i));
|
||||
|
||||
list.sort(FileSort::Size); // biggest first
|
||||
const char* bySize[] = {"Captures", "tracks", "Alpha.log", "beta.log", "zeta.log"};
|
||||
for (int i = 0; i < 5; i++) TEST_ASSERT_EQUAL_STRING(bySize[i], list.name(i));
|
||||
}
|
||||
|
||||
// Q136: 256 entries at most. A bigger folder keeps the first 256 by name, whatever order the
|
||||
// card lists them in, and says how many more there are.
|
||||
void test_more_than_fits() {
|
||||
FileList list;
|
||||
char name[24];
|
||||
for (int i = 399; i >= 0; i--) { // the card's order: backwards
|
||||
std::snprintf(name, sizeof name, "2026-%03d.log", i);
|
||||
list.add(name, static_cast<uint32_t>(i), 1000 + i, false);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_size_t(256, list.count());
|
||||
TEST_ASSERT_EQUAL_size_t(144, list.more());
|
||||
list.sort(FileSort::Name);
|
||||
TEST_ASSERT_EQUAL_STRING("2026-000.log", list.name(0));
|
||||
TEST_ASSERT_EQUAL_STRING("2026-255.log", list.name(255));
|
||||
TEST_ASSERT_EQUAL_UINT32(255, list.size(255));
|
||||
// And it stays small: 400 names went through, the memory is for 256.
|
||||
TEST_ASSERT_TRUE(list.bytes() < 12 * 1024);
|
||||
}
|
||||
|
||||
void test_folders_are_kept_before_files_when_full() {
|
||||
FileList list;
|
||||
char name[24];
|
||||
for (int i = 0; i < 300; i++) {
|
||||
std::snprintf(name, sizeof name, "a%03d.log", i);
|
||||
list.add(name, 1, 1, false);
|
||||
}
|
||||
list.add("zz-folder", 0, 1, true); // sorts first, so it's among the 256
|
||||
list.sort(FileSort::Name);
|
||||
TEST_ASSERT_EQUAL_STRING("zz-folder", list.name(0));
|
||||
TEST_ASSERT_EQUAL_size_t(45, list.more());
|
||||
}
|
||||
|
||||
void test_find_and_clear() {
|
||||
FileList list;
|
||||
list.add("a.txt", 1, 1, false);
|
||||
list.add("b.txt", 2, 2, false);
|
||||
list.sort(FileSort::Name);
|
||||
TEST_ASSERT_EQUAL_INT(1, list.find("b.txt"));
|
||||
TEST_ASSERT_EQUAL_INT(-1, list.find("c.txt"));
|
||||
list.clear();
|
||||
TEST_ASSERT_EQUAL_size_t(0, list.count());
|
||||
TEST_ASSERT_EQUAL_size_t(0, list.more());
|
||||
}
|
||||
|
||||
// Q129, Q137: what a row shows on the right. A date before 2020 was never set.
|
||||
void test_display() {
|
||||
setenv("TZ", "UTC", 1);
|
||||
tzset();
|
||||
TEST_ASSERT_EQUAL_STRING("folder", rowDetail(true, 0, 0).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("1.2 KB 2026-10-05", rowDetail(false, 1200, 1791230400).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("0 B -", rowDetail(false, 0, 315532800).c_str()); // 1980-01-01, FAT's zero
|
||||
TEST_ASSERT_EQUAL_STRING("2026-10-05 20:00", formatStamp(1791230400).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("-", formatStamp(0).c_str());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_entries);
|
||||
RUN_TEST(test_sorting);
|
||||
RUN_TEST(test_more_than_fits);
|
||||
RUN_TEST(test_folders_are_kept_before_files_when_full);
|
||||
RUN_TEST(test_find_and_clear);
|
||||
RUN_TEST(test_display);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#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
|
||||
}
|
||||
|
||||
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);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user