Files
roro9stack/test/test_text_pager/test_text_pager.cpp
T
twislaandClaude Opus 5.5 b7aed8e91c F1 steps 2-6: the Storage App, its viewers, and Maintenance moved in (#3)
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
2026-10-06 08:45:43 +02:00

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();
}