Public Access
The Storage App browses the SD card: folders first with sizes and dates, three sorts, one item at a time with a clipboard (c, x, v), rename, delete after counting what's inside, new folder, details. A listing holds 256 entries and says when a folder has more. FileOps does the card's work for the App and the console alike, one operation at a time on the storage task in turns of about 150 ms, so Logs and Captures are still written during a long copy. A copy shows progress, can be cancelled (what it wrote is taken back) and compares sizes after. The read-only rules are checked there: the firmware's top-level folders, /gemini/cache, and files being written (a Track, a Capture, an upload, today's IRC Logs). A listing reads the folder straight from FatFs: through the Arduino File, 329 entries took over two seconds. Viewers by type: text read a screen at a time whatever the file's size (logs open at the end), a hex dump, a Capture's packets as the LoRa Scanner lists them, a Track's summary, and an Update File checked as an install would check it, without writing anything. Tab shows any file as hex or text. Settings > Storage is gone: usage, Storage Clean-up and Erase are the App's Maintenance, behind a warning. The Storage Warning points there. The Clock sets the system time whatever its source, so files are dated correctly with a GNSS Fix alone (Q137). Console: cp, mv, mkdir, du, cancel; rm takes folders and follows the rules; ls shows dates; Debug Builds get `sd fill`. 424 host tests. Checked on the device: docs/milestones/F1.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
157 lines
5.7 KiB
C++
157 lines
5.7 KiB
C++
#include <unity.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "text_pager.h"
|
|
|
|
using namespace roro::files;
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
static int reads = 0;
|
|
static size_t biggestRead = 0;
|
|
|
|
static TextPager pagerFor(const std::string& text, int cols, int rows) {
|
|
return TextPager(
|
|
[&text](uint32_t offset, uint8_t* into, size_t len) {
|
|
reads++;
|
|
biggestRead = std::max(biggestRead, len);
|
|
if (offset >= text.size()) return static_cast<size_t>(0);
|
|
size_t n = std::min(len, text.size() - offset);
|
|
std::memcpy(into, text.data() + offset, n);
|
|
return n;
|
|
},
|
|
static_cast<uint32_t>(text.size()), cols, rows);
|
|
}
|
|
|
|
void test_lines_and_wrapping() {
|
|
std::string text = "one\r\ntwo words that are too long\n\nlast";
|
|
TextPager p = pagerFor(text, 10, 8);
|
|
auto lines = p.lines();
|
|
TEST_ASSERT_EQUAL_size_t(6, lines.size());
|
|
TEST_ASSERT_EQUAL_STRING("one", lines[0].c_str()); // the \r is dropped
|
|
TEST_ASSERT_EQUAL_STRING("two words", lines[1].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("that are", lines[2].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("too long", lines[3].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", lines[4].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("last", lines[5].c_str());
|
|
TEST_ASSERT_TRUE(p.atEnd());
|
|
}
|
|
|
|
void test_a_word_too_long_is_cut_and_a_space_at_the_edge_is_eaten() {
|
|
std::string text = "abcdefghijklmno\nabcde fghij klm";
|
|
TextPager p = pagerFor(text, 5, 8);
|
|
auto lines = p.lines();
|
|
TEST_ASSERT_EQUAL_STRING("abcde", lines[0].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("fghij", lines[1].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("klmno", lines[2].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("abcde", lines[3].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("fghij", lines[4].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("klm", lines[5].c_str());
|
|
}
|
|
|
|
void test_accents_count_as_one_column_and_tabs_show_as_spaces() {
|
|
std::string text = "\xC3\xA9t\xC3\xA9 ici\n\ta\x01";
|
|
TextPager p = pagerFor(text, 3, 4);
|
|
auto lines = p.lines();
|
|
TEST_ASSERT_EQUAL_STRING("\xC3\xA9t\xC3\xA9", lines[0].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("ici", lines[1].c_str());
|
|
TEST_ASSERT_EQUAL_STRING(" a.", lines[2].c_str());
|
|
}
|
|
|
|
// Scrolling down to the end and back up passes through the same line starts.
|
|
void test_up_undoes_down() {
|
|
std::string text;
|
|
for (int i = 0; i < 300; i++) {
|
|
text += "line " + std::to_string(i) + " with some words to wrap around the edge of the screen";
|
|
text += i % 7 == 0 ? "\n\n" : "\n";
|
|
}
|
|
text += "a paragraph without any newline for a long while ";
|
|
for (int i = 0; i < 200; i++) text += "word" + std::to_string(i) + " ";
|
|
text += "\nthe end";
|
|
TextPager p = pagerFor(text, 38, 9);
|
|
std::vector<uint32_t> tops;
|
|
for (int guard = 0; guard < 5000 && !p.atEnd(); guard++) {
|
|
tops.push_back(p.top());
|
|
p.down();
|
|
}
|
|
TEST_ASSERT_TRUE(p.atEnd());
|
|
TEST_ASSERT_TRUE(tops.size() > 500);
|
|
TEST_ASSERT_EQUAL_STRING("the end", p.lines().back().c_str());
|
|
// Inside the long paragraph (further than the look-back from its start) lines may wrap from
|
|
// another place on the way up. Before it, every line start met on the way down is met again.
|
|
size_t paragraph = text.find("a paragraph without");
|
|
size_t expected = 0, met = 0;
|
|
for (uint32_t t : tops) expected += t <= paragraph;
|
|
for (int guard = 0; guard < 5000 && p.top() > 0; guard++) {
|
|
p.up();
|
|
if (p.top() > paragraph) continue;
|
|
TEST_ASSERT_TRUE(std::binary_search(tops.begin(), tops.end(), p.top()));
|
|
met++;
|
|
}
|
|
TEST_ASSERT_EQUAL_size_t(expected, met);
|
|
TEST_ASSERT_EQUAL_UINT32(0, p.top());
|
|
p.up();
|
|
TEST_ASSERT_EQUAL_UINT32(0, p.top());
|
|
}
|
|
|
|
void test_to_end_shows_the_last_lines() {
|
|
std::string text;
|
|
for (int i = 0; i < 100; i++) text += "20:0" + std::to_string(i % 10) + " <a> message " + std::to_string(i) + "\n";
|
|
TextPager p = pagerFor(text, 38, 9);
|
|
p.toEnd();
|
|
auto lines = p.lines();
|
|
TEST_ASSERT_EQUAL_size_t(9, lines.size());
|
|
TEST_ASSERT_EQUAL_STRING("20:09 <a> message 99", lines[8].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("20:01 <a> message 91", lines[0].c_str());
|
|
TEST_ASSERT_TRUE(p.atEnd());
|
|
p.down(); // nowhere to go
|
|
TEST_ASSERT_EQUAL_STRING("20:01 <a> message 91", p.lines()[0].c_str());
|
|
TEST_ASSERT_TRUE(p.percent() > 80);
|
|
}
|
|
|
|
void test_a_megabyte_is_read_a_kilobyte_at_a_time() {
|
|
std::string text;
|
|
while (text.size() < 1024 * 1024) text += "2026-10-05 20:00:00 <someone> says something here\n";
|
|
reads = 0;
|
|
biggestRead = 0;
|
|
TextPager p = pagerFor(text, 38, 9);
|
|
p.toEnd();
|
|
TEST_ASSERT_EQUAL_size_t(9, p.lines().size());
|
|
p.up(20);
|
|
p.down(5);
|
|
TEST_ASSERT_TRUE(biggestRead <= 1024);
|
|
TEST_ASSERT_TRUE(reads < 20);
|
|
p.toStart();
|
|
TEST_ASSERT_EQUAL_STRING("2026-10-05 20:00:00 <someone> says", p.lines()[0].c_str());
|
|
}
|
|
|
|
void test_empty_and_short_files() {
|
|
std::string empty;
|
|
TextPager p = pagerFor(empty, 38, 9);
|
|
TEST_ASSERT_EQUAL_size_t(0, p.lines().size());
|
|
p.down();
|
|
p.up();
|
|
p.toEnd();
|
|
TEST_ASSERT_TRUE(p.atEnd());
|
|
std::string one = "no newline";
|
|
TextPager q = pagerFor(one, 38, 9);
|
|
q.toEnd();
|
|
TEST_ASSERT_EQUAL_STRING("no newline", q.lines()[0].c_str());
|
|
}
|
|
|
|
int main() {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_lines_and_wrapping);
|
|
RUN_TEST(test_a_word_too_long_is_cut_and_a_space_at_the_edge_is_eaten);
|
|
RUN_TEST(test_accents_count_as_one_column_and_tabs_show_as_spaces);
|
|
RUN_TEST(test_up_undoes_down);
|
|
RUN_TEST(test_to_end_shows_the_last_lines);
|
|
RUN_TEST(test_a_megabyte_is_read_a_kilobyte_at_a_time);
|
|
RUN_TEST(test_empty_and_short_files);
|
|
return UNITY_END();
|
|
}
|