Public Access
The editor held the whole note in memory and stopped at 16 KB. It now keeps a window of the file around the cursor, and the rest on the card as a list of pieces (notes::NoteDocument). Memory with a note open is what it was. Up to 64 KB a save rewrites the file, as before. Above, the five-second save appends what changed to <note>.edit, and the file is rewritten on leaving the note, with a progress bar. After a power cut, opening the note picks the edit up where it was saved; a rewrite cut short is finished or dropped, never half applied. Also: Ctrl with Fn+Up/Down go to the start and end of the note; the consoles' `key` command takes ctrl-, alt- and shift-; the Storage App's `e` no longer refuses a big file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
522 lines
20 KiB
C++
522 lines
20 KiB
C++
#include <unity.h>
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <random>
|
|
#include <string>
|
|
|
|
#include "note_document.h"
|
|
|
|
using namespace roro::notes;
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
namespace {
|
|
// A card in memory. `budget`: how many more bytes it takes before the power is cut: the write
|
|
// that crosses it is left half done, and nothing works after it.
|
|
struct FakeCard : NoteCard {
|
|
std::map<std::string, std::string> files;
|
|
int64_t budget = -1;
|
|
bool dead = false;
|
|
uint64_t free = 1ull << 30;
|
|
int reads = 0;
|
|
|
|
bool spend(size_t n) {
|
|
if (dead) return false;
|
|
if (budget < 0) return true;
|
|
if (static_cast<int64_t>(n) <= budget) return budget -= static_cast<int64_t>(n), true;
|
|
return false;
|
|
}
|
|
bool size(const std::string& path, uint32_t& size) override {
|
|
auto it = files.find(path);
|
|
if (dead || it == files.end()) return false;
|
|
size = static_cast<uint32_t>(it->second.size());
|
|
return true;
|
|
}
|
|
size_t read(const std::string& path, uint32_t at, uint8_t* into, size_t len) override {
|
|
auto it = files.find(path);
|
|
if (dead || it == files.end() || at > it->second.size()) return 0;
|
|
reads++;
|
|
size_t n = std::min(len, it->second.size() - at);
|
|
std::copy(it->second.begin() + at, it->second.begin() + at + static_cast<long>(n), into);
|
|
return n;
|
|
}
|
|
bool create(const std::string& path) override {
|
|
if (!spend(1)) return dead = true, false;
|
|
files[path].clear();
|
|
return true;
|
|
}
|
|
bool append(const std::string& path, const uint8_t* data, size_t len) override {
|
|
if (dead) return false;
|
|
if (!spend(len)) {
|
|
files[path].append(reinterpret_cast<const char*>(data), static_cast<size_t>(std::max<int64_t>(0, budget)));
|
|
return dead = true, false;
|
|
}
|
|
files[path].append(reinterpret_cast<const char*>(data), len);
|
|
return true;
|
|
}
|
|
bool remove(const std::string& path) override {
|
|
if (!spend(1)) return dead = true, false;
|
|
return files.erase(path) > 0;
|
|
}
|
|
bool rename(const std::string& from, const std::string& to) override {
|
|
if (!spend(1)) return dead = true, false;
|
|
if (!files.count(from) || files.count(to)) return false;
|
|
files[to] = files[from];
|
|
files.erase(from);
|
|
return true;
|
|
}
|
|
uint64_t freeBytes() override { return free; }
|
|
void powerBack() {
|
|
dead = false;
|
|
budget = -1;
|
|
}
|
|
};
|
|
|
|
constexpr int kCols = 38, kRows = 8;
|
|
const char* const kNote = "/notes/big.txt";
|
|
|
|
std::string lines(int from, int count) {
|
|
std::string s;
|
|
char b[32];
|
|
for (int i = 0; i < count; i++) {
|
|
std::snprintf(b, sizeof b, "line %06d\n", from + i);
|
|
s += b;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Each line is 12 bytes: "line 000000\n".
|
|
constexpr uint32_t kLine = 12;
|
|
|
|
bool rewrite(NoteDocument& d) {
|
|
std::string why;
|
|
if (!d.rewriteStart(why)) return false;
|
|
for (int guard = 0; guard < 100000; guard++) {
|
|
int p = d.rewriteStep(why);
|
|
if (p == 100) return true;
|
|
if (p < 0) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void settle(NoteDocument& d) {
|
|
std::string why;
|
|
if (d.wantsMove()) TEST_ASSERT_TRUE_MESSAGE(d.move(why), why.c_str());
|
|
}
|
|
|
|
void type(NoteDocument& d, const std::string& s) {
|
|
for (char c : s) {
|
|
TEST_ASSERT_TRUE(d.text().insert(static_cast<uint8_t>(c)));
|
|
settle(d);
|
|
}
|
|
}
|
|
|
|
std::string currentRow(NoteDocument& d) { return d.text().rows()[static_cast<size_t>(d.text().cursorRow())]; }
|
|
} // namespace
|
|
|
|
void test_a_small_note_is_all_window() {
|
|
FakeCard card;
|
|
card.files[kNote] = "Hello\nworld\n";
|
|
NoteDocument d(card, kCols, kRows);
|
|
TEST_ASSERT_EQUAL_STRING("", d.open(kNote).c_str());
|
|
TEST_ASSERT_FALSE(d.windowed());
|
|
TEST_ASSERT_FALSE(d.dirty());
|
|
TEST_ASSERT_TRUE(d.wantsRewrite());
|
|
type(d, "Oh! ");
|
|
TEST_ASSERT_TRUE(d.dirty());
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_FALSE(d.dirty());
|
|
TEST_ASSERT_EQUAL_STRING("Oh! Hello\nworld\n", card.files[kNote].c_str());
|
|
TEST_ASSERT_EQUAL_size_t(1, card.files.size()); // no side file, no temporary file left
|
|
}
|
|
|
|
void test_a_new_note_has_no_file_until_it_is_rewritten() {
|
|
FakeCard card;
|
|
NoteDocument d(card, kCols, kRows);
|
|
type(d, "A list");
|
|
TEST_ASSERT_TRUE(d.dirty());
|
|
std::string why;
|
|
TEST_ASSERT_FALSE(d.rewriteStart(why));
|
|
TEST_ASSERT_FALSE(d.journal(why));
|
|
TEST_ASSERT_EQUAL_size_t(0, card.files.size());
|
|
d.setPath("/notes/a-list.txt");
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_EQUAL_STRING("A list", card.files["/notes/a-list.txt"].c_str());
|
|
type(d, "!");
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_EQUAL_STRING("A list!", card.files["/notes/a-list.txt"].c_str());
|
|
}
|
|
|
|
void test_a_big_file_opens_as_a_window() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 20000); // 240 KB
|
|
NoteDocument d(card, kCols, kRows);
|
|
TEST_ASSERT_EQUAL_STRING("", d.open(kNote).c_str());
|
|
TEST_ASSERT_TRUE(d.windowed());
|
|
TEST_ASSERT_EQUAL_UINT32(240000, d.size());
|
|
TEST_ASSERT_TRUE(d.text().size() <= NoteText::kMaxBytes / 2 + 200);
|
|
TEST_ASSERT_EQUAL_STRING("line 000000", d.text().rows()[0].c_str());
|
|
TEST_ASSERT_FALSE(d.wantsRewrite());
|
|
TEST_ASSERT_FALSE(d.dirty());
|
|
}
|
|
|
|
void test_walking_down_and_back_up_through_windows() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 5000);
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
for (int i = 1; i <= 3000; i++) {
|
|
d.text().down();
|
|
settle(d);
|
|
if (i % 500 == 0) {
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(i) * kLine, d.cursor());
|
|
TEST_ASSERT_EQUAL_STRING(lines(i, 1).substr(0, 11).c_str(), currentRow(d).c_str());
|
|
TEST_ASSERT_EQUAL_INT(kRows - 1, d.text().cursorRow()); // the screen didn't jump when the window moved
|
|
}
|
|
}
|
|
for (int i = 2999; i >= 0; i--) {
|
|
d.text().up();
|
|
settle(d);
|
|
if (i % 500 == 0) {
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(i) * kLine, d.cursor());
|
|
TEST_ASSERT_EQUAL_STRING(lines(i, 1).substr(0, 11).c_str(), currentRow(d).c_str());
|
|
}
|
|
}
|
|
TEST_ASSERT_FALSE(d.dirty()); // looking isn't editing
|
|
TEST_ASSERT_EQUAL_size_t(1, card.files.size()); // and writes nothing
|
|
}
|
|
|
|
void test_start_and_end() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 5000) + "the end";
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
std::string why;
|
|
TEST_ASSERT_TRUE(d.jump(d.size(), why));
|
|
TEST_ASSERT_EQUAL_UINT32(d.size(), d.cursor());
|
|
TEST_ASSERT_EQUAL_STRING("the end", currentRow(d).c_str());
|
|
TEST_ASSERT_EQUAL_INT(kRows - 1, d.text().cursorRow());
|
|
TEST_ASSERT_EQUAL_INT(99, d.percent());
|
|
TEST_ASSERT_TRUE(d.jump(0, why));
|
|
TEST_ASSERT_EQUAL_UINT32(0, d.cursor());
|
|
TEST_ASSERT_EQUAL_STRING("line 000000", currentRow(d).c_str());
|
|
}
|
|
|
|
void test_an_edit_in_the_middle_is_rewritten_into_the_file() {
|
|
FakeCard card;
|
|
std::string was = lines(0, 20000);
|
|
card.files[kNote] = was;
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
std::string why;
|
|
TEST_ASSERT_TRUE(d.jump(120000, why));
|
|
type(d, "HERE ");
|
|
TEST_ASSERT_TRUE(d.jump(0, why)); // the changed window goes to the side file
|
|
TEST_ASSERT_TRUE(card.files.count(std::string(kNote) + ".edit"));
|
|
TEST_ASSERT_EQUAL_STRING(was.c_str(), card.files[kNote].c_str()); // the file isn't touched until the rewrite
|
|
type(d, "TOP ");
|
|
TEST_ASSERT_TRUE(d.filePending());
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
std::string want = "TOP " + was.substr(0, 120000) + "HERE " + was.substr(120000);
|
|
TEST_ASSERT_TRUE(want == card.files[kNote]);
|
|
TEST_ASSERT_EQUAL_size_t(1, card.files.size());
|
|
TEST_ASSERT_FALSE(d.filePending());
|
|
TEST_ASSERT_FALSE(d.dirty());
|
|
// And it goes on from there.
|
|
TEST_ASSERT_TRUE(d.jump(d.size(), why));
|
|
type(d, "END");
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_TRUE(want + "END" == card.files[kNote]);
|
|
}
|
|
|
|
void test_typing_never_fills_the_note() {
|
|
FakeCard card;
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.setPath("/notes/long.txt");
|
|
std::string want;
|
|
for (int i = 0; i < 4000; i++) { // 48 KB, three windows' worth
|
|
std::string l = lines(i, 1);
|
|
for (char c : l) {
|
|
if (!d.text().insert(static_cast<uint8_t>(c))) TEST_FAIL_MESSAGE("the window was full");
|
|
if (d.wantsMove()) {
|
|
std::string why;
|
|
if (d.path().empty() || !d.windowed()) TEST_ASSERT_TRUE(rewrite(d)); // what the editor does: a file first
|
|
TEST_ASSERT_TRUE_MESSAGE(d.move(why), why.c_str());
|
|
}
|
|
}
|
|
want += l;
|
|
}
|
|
TEST_ASSERT_TRUE(d.windowed());
|
|
TEST_ASSERT_EQUAL_UINT32(48000, d.size());
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_TRUE(want == card.files["/notes/long.txt"]);
|
|
}
|
|
|
|
void test_a_journal_is_picked_up_after_a_power_cut() {
|
|
FakeCard card;
|
|
std::string was = lines(0, 20000);
|
|
card.files[kNote] = was;
|
|
std::string why;
|
|
{
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
d.jump(60000, why);
|
|
type(d, "one ");
|
|
TEST_ASSERT_TRUE(d.journal(why));
|
|
TEST_ASSERT_FALSE(d.dirty());
|
|
TEST_ASSERT_TRUE(d.filePending());
|
|
d.jump(180000, why);
|
|
type(d, "two ");
|
|
TEST_ASSERT_TRUE(d.journal(why));
|
|
type(d, "never saved");
|
|
} // the power goes
|
|
TEST_ASSERT_EQUAL_STRING(was.c_str(), card.files[kNote].c_str());
|
|
NoteDocument d(card, kCols, kRows);
|
|
std::string told;
|
|
TEST_ASSERT_EQUAL_STRING("", d.open(kNote, &told).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("Your unsaved changes are back", told.c_str());
|
|
TEST_ASSERT_EQUAL_UINT32(240008, d.size());
|
|
TEST_ASSERT_EQUAL_UINT32(180004, d.cursor()); // where it was
|
|
TEST_ASSERT_TRUE(d.filePending());
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_TRUE(was.substr(0, 60000) + "one " + was.substr(60000, 119996) + "two " + was.substr(179996) == card.files[kNote]);
|
|
TEST_ASSERT_EQUAL_size_t(1, card.files.size());
|
|
}
|
|
|
|
void test_a_cut_at_any_byte_keeps_the_note_or_the_last_save() {
|
|
std::string was = lines(0, 9000); // 108 KB
|
|
std::string saved1 = was.substr(0, 50000) + "first " + was.substr(50000);
|
|
std::string saved2 = "second " + saved1;
|
|
int outcomes[3] = {0, 0, 0};
|
|
for (int64_t budget = 0; budget < 240000; budget += 997) {
|
|
FakeCard card;
|
|
card.files[kNote] = was;
|
|
std::string why;
|
|
bool journaled1 = false, journaled2 = false, rewritten = false;
|
|
{
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
card.budget = budget;
|
|
d.jump(50000, why);
|
|
for (char c : std::string("first ")) d.text().insert(static_cast<uint8_t>(c));
|
|
journaled1 = d.journal(why);
|
|
if (journaled1 && d.jump(0, why)) {
|
|
for (char c : std::string("second ")) d.text().insert(static_cast<uint8_t>(c));
|
|
journaled2 = d.journal(why);
|
|
if (journaled2) rewritten = rewrite(d);
|
|
}
|
|
}
|
|
card.powerBack();
|
|
// What the Notes list does with a temporary file that has lost its note.
|
|
if (!card.files.count(kNote) && card.files.count(std::string(kNote) + ".tmp")) card.rename(std::string(kNote) + ".tmp", kNote);
|
|
NoteDocument d(card, kCols, kRows);
|
|
TEST_ASSERT_EQUAL_STRING("", d.open(kNote).c_str());
|
|
TEST_ASSERT_TRUE(rewrite(d) || !d.dirty());
|
|
if (d.filePending()) TEST_ASSERT_TRUE(rewrite(d));
|
|
const std::string& now = card.files[kNote];
|
|
int which = now == was ? 0 : now == saved1 ? 1 : now == saved2 ? 2 : -1;
|
|
char msg[64];
|
|
std::snprintf(msg, sizeof msg, "budget %ld", static_cast<long>(budget));
|
|
TEST_ASSERT_TRUE_MESSAGE(which >= 0, msg); // never anything else
|
|
if (rewritten) TEST_ASSERT_EQUAL_INT_MESSAGE(2, which, msg); // what was said to be saved is there
|
|
else if (journaled2) TEST_ASSERT_EQUAL_INT_MESSAGE(2, which, msg);
|
|
else if (journaled1) TEST_ASSERT_TRUE_MESSAGE(which >= 1, msg);
|
|
TEST_ASSERT_EQUAL_size_t_MESSAGE(1, card.files.size(), msg); // and nothing is left lying about
|
|
outcomes[which]++;
|
|
}
|
|
TEST_ASSERT_TRUE(outcomes[0] > 0 && outcomes[1] > 0 && outcomes[2] > 0);
|
|
}
|
|
|
|
void test_a_file_changed_elsewhere_sets_the_edits_aside() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 20000);
|
|
std::string why;
|
|
{
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
d.jump(60000, why);
|
|
type(d, "typed ");
|
|
TEST_ASSERT_TRUE(d.journal(why));
|
|
}
|
|
card.files[kNote] = lines(7, 100); // someone put another file there
|
|
NoteDocument d(card, kCols, kRows);
|
|
std::string told;
|
|
TEST_ASSERT_EQUAL_STRING("", d.open(kNote, &told).c_str());
|
|
TEST_ASSERT_TRUE(told.find(".edit.lost") != std::string::npos);
|
|
TEST_ASSERT_TRUE(card.files.count(std::string(kNote) + ".edit.lost"));
|
|
TEST_ASSERT_FALSE(card.files.count(std::string(kNote) + ".edit"));
|
|
TEST_ASSERT_TRUE(card.files[std::string(kNote) + ".edit.lost"].find("typed ") != std::string::npos); // it can be read
|
|
TEST_ASSERT_EQUAL_UINT32(1200, d.size());
|
|
TEST_ASSERT_FALSE(d.filePending());
|
|
}
|
|
|
|
void test_crlf_becomes_lf_everywhere() {
|
|
FakeCard card;
|
|
std::string crlf, lf;
|
|
for (int i = 0; i < 6000; i++) {
|
|
std::string l = lines(i, 1);
|
|
lf += l;
|
|
crlf += l.substr(0, 11) + "\r\n";
|
|
}
|
|
card.files[kNote] = crlf;
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
for (int i = 1; i <= 1500; i++) { // through several windows, looking only
|
|
d.text().down();
|
|
settle(d);
|
|
}
|
|
TEST_ASSERT_EQUAL_STRING("line 001500", currentRow(d).c_str());
|
|
TEST_ASSERT_EQUAL_INT(0, d.text().cursorCol());
|
|
type(d, "x");
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
std::string want = lf;
|
|
want.insert(1500 * kLine, "x");
|
|
TEST_ASSERT_TRUE(want == card.files[kNote]);
|
|
}
|
|
|
|
void test_windows_never_cut_a_character() {
|
|
FakeCard card;
|
|
std::string text;
|
|
while (text.size() < 80000) text += "\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"; // é, €, a face: no space, no newline
|
|
card.files[kNote] = text;
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
std::string why;
|
|
for (uint32_t to : {30001u, 12345u, 79990u, 40003u, 5u}) {
|
|
TEST_ASSERT_TRUE(d.jump(to, why));
|
|
const std::string& w = d.text().text();
|
|
TEST_ASSERT_TRUE((static_cast<uint8_t>(w[0]) & 0xC0) != 0x80);
|
|
for (size_t i = 0; i < w.size();) { // whole characters, to the last
|
|
uint8_t c = static_cast<uint8_t>(w[i]);
|
|
size_t n = c >= 0xF0 ? 4 : c >= 0xE0 ? 3 : c >= 0xC0 ? 2 : 1;
|
|
TEST_ASSERT_TRUE(c < 0x80 || c >= 0xC0);
|
|
TEST_ASSERT_TRUE(i + n <= w.size());
|
|
i += n;
|
|
}
|
|
TEST_ASSERT_TRUE((static_cast<uint8_t>(w[d.text().cursor()]) & 0xC0) != 0x80 || d.text().cursor() == w.size());
|
|
type(d, "a");
|
|
}
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_EQUAL_size_t(text.size() + 5, card.files[kNote].size());
|
|
}
|
|
|
|
void test_random_edits_match_a_plain_string() {
|
|
std::mt19937 rng(47);
|
|
for (int round = 0; round < 6; round++) {
|
|
FakeCard card;
|
|
std::string model;
|
|
while (model.size() < 30000u + 20000u * static_cast<unsigned>(round)) {
|
|
int words = 1 + static_cast<int>(rng() % 30);
|
|
for (int w = 0; w < words; w++) model += std::string(1 + rng() % 9, static_cast<char>('a' + rng() % 26)) + " ";
|
|
model += rng() % 4 ? "\n" : "\n\n";
|
|
}
|
|
card.files[kNote] = model;
|
|
std::string saved = model; // what a power cut would leave
|
|
NoteDocument* d = new NoteDocument(card, kCols, kRows);
|
|
d->open(kNote);
|
|
std::string why;
|
|
for (int step = 0; step < 6000; step++) {
|
|
unsigned op = rng() % 100;
|
|
NoteText& t = d->text();
|
|
if (op < 40) {
|
|
char c = rng() % 8 ? static_cast<char>('A' + rng() % 26) : '\n';
|
|
uint32_t at = d->cursor();
|
|
TEST_ASSERT_TRUE(t.insert(static_cast<uint8_t>(c)));
|
|
model.insert(at, 1, c);
|
|
} else if (op < 50) {
|
|
uint32_t at = d->cursor();
|
|
t.backspace();
|
|
if (at > 0 && d->cursor() == at - 1) model.erase(at - 1, 1);
|
|
else TEST_ASSERT_EQUAL_UINT32(at, d->cursor());
|
|
} else if (op < 60) t.left();
|
|
else if (op < 70) t.right();
|
|
else if (op < 78) t.up();
|
|
else if (op < 86) t.down();
|
|
else if (op < 90) t.pageUp();
|
|
else if (op < 94) t.pageDown();
|
|
else if (op < 96) TEST_ASSERT_TRUE(d->jump(rng() % (d->size() + 1), why));
|
|
else if (op < 98) {
|
|
if (d->dirty()) {
|
|
TEST_ASSERT_TRUE(d->wantsRewrite() ? rewrite(*d) : d->journal(why));
|
|
saved = model;
|
|
}
|
|
} else if (op == 98) {
|
|
TEST_ASSERT_TRUE(rewrite(*d));
|
|
saved = model;
|
|
TEST_ASSERT_TRUE(model == card.files[kNote]);
|
|
} else { // the power goes, and comes back
|
|
delete d;
|
|
d = new NoteDocument(card, kCols, kRows);
|
|
TEST_ASSERT_EQUAL_STRING("", d->open(kNote).c_str());
|
|
model = saved;
|
|
}
|
|
settle(*d);
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(model.size()), d->size());
|
|
// The window is the note's text at its place.
|
|
TEST_ASSERT_TRUE(d->cursor() <= model.size());
|
|
uint32_t start = d->cursor() - static_cast<uint32_t>(d->text().cursor());
|
|
TEST_ASSERT_TRUE(model.compare(start, d->text().size(), d->text().text()) == 0);
|
|
}
|
|
TEST_ASSERT_TRUE(rewrite(*d));
|
|
TEST_ASSERT_TRUE(model == card.files[kNote]);
|
|
TEST_ASSERT_EQUAL_size_t(1, card.files.size());
|
|
delete d;
|
|
}
|
|
}
|
|
|
|
void test_a_full_card_and_a_missing_file() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 20000);
|
|
card.free = 1000;
|
|
NoteDocument d(card, kCols, kRows);
|
|
TEST_ASSERT_TRUE(d.open(kNote).find("Not enough room") != std::string::npos);
|
|
TEST_ASSERT_TRUE(d.open("/notes/none.txt").find("refused") != std::string::npos);
|
|
card.files["/notes/small.txt"] = "small";
|
|
TEST_ASSERT_EQUAL_STRING("", d.open("/notes/small.txt").c_str()); // a small one still opens
|
|
type(d, "x");
|
|
std::string why;
|
|
TEST_ASSERT_FALSE(d.rewriteStart(why));
|
|
TEST_ASSERT_EQUAL_STRING("the card is full", why.c_str());
|
|
TEST_ASSERT_TRUE(d.dirty());
|
|
}
|
|
|
|
void test_the_side_file_and_the_piece_list_ask_for_a_rewrite() {
|
|
FakeCard card;
|
|
card.files[kNote] = lines(0, 20000);
|
|
NoteDocument d(card, kCols, kRows);
|
|
d.open(kNote);
|
|
std::string why;
|
|
int saves = 0;
|
|
while (!d.wantsRewrite() && saves < 1000) {
|
|
type(d, "x");
|
|
TEST_ASSERT_TRUE(d.journal(why));
|
|
saves++;
|
|
}
|
|
TEST_ASSERT_TRUE(saves > 50 && saves < 300); // about 1 MB of 8 KB windows
|
|
TEST_ASSERT_TRUE(rewrite(d));
|
|
TEST_ASSERT_FALSE(d.wantsRewrite());
|
|
TEST_ASSERT_EQUAL_size_t(240000 + static_cast<size_t>(saves), card.files[kNote].size());
|
|
}
|
|
|
|
int main() {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_a_small_note_is_all_window);
|
|
RUN_TEST(test_a_new_note_has_no_file_until_it_is_rewritten);
|
|
RUN_TEST(test_a_big_file_opens_as_a_window);
|
|
RUN_TEST(test_walking_down_and_back_up_through_windows);
|
|
RUN_TEST(test_start_and_end);
|
|
RUN_TEST(test_an_edit_in_the_middle_is_rewritten_into_the_file);
|
|
RUN_TEST(test_typing_never_fills_the_note);
|
|
RUN_TEST(test_a_journal_is_picked_up_after_a_power_cut);
|
|
RUN_TEST(test_a_cut_at_any_byte_keeps_the_note_or_the_last_save);
|
|
RUN_TEST(test_a_file_changed_elsewhere_sets_the_edits_aside);
|
|
RUN_TEST(test_crlf_becomes_lf_everywhere);
|
|
RUN_TEST(test_windows_never_cut_a_character);
|
|
RUN_TEST(test_random_edits_match_a_plain_string);
|
|
RUN_TEST(test_a_full_card_and_a_missing_file);
|
|
RUN_TEST(test_the_side_file_and_the_piece_list_ask_for_a_rewrite);
|
|
return UNITY_END();
|
|
}
|