// Checks how the list of forgotten words survives restarts, crashes and failed // writes. g++ -std=c++20 -Isrc tests/blockstore_test.cpp -o /tmp/blockstore_test #include #include #include #include #include #include #include #include "blockstore.h" using fcitx::m3ui::BlockStore; namespace fs = std::filesystem; static int failures = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); \ failures++; \ } \ } while (0) static std::string slurp(const fs::path &p) { std::ifstream in(p); std::stringstream ss; ss << in.rdbuf(); return ss.str(); } int main() { char tmpl[] = "/tmp/blockstore_test.XXXXXX"; const fs::path dir = mkdtemp(tmpl); const fs::path file = dir / "m3ui" / "blocked-words.txt"; { // words survive a restart, which is a new store on the same file BlockStore first(file); CHECK(first.empty()); CHECK(first.add("伟大实践")); CHECK(first.add("你不能")); CHECK(first.contains("你不能")); BlockStore afterRestart(file); CHECK(afterRestart.size() == 2); CHECK(afterRestart.contains("伟大实践") && afterRestart.contains("你不能")); CHECK(slurp(file) == "伟大实践\n你不能\n"); } { // the file is private and no temporary file is left behind struct stat st {}; CHECK(::stat(file.c_str(), &st) == 0); CHECK((st.st_mode & 0777) == 0600); int stray = 0; for (const auto &e : fs::directory_iterator(file.parent_path())) { stray += e.path().filename() != "blocked-words.txt"; } CHECK(stray == 0); } { // duplicates are not written twice, bad words are refused BlockStore s(file); CHECK(s.add("你不能")); CHECK(s.size() == 2); CHECK(!s.add("")); CHECK(!s.add("two\nlines")); CHECK(!s.add(std::string("nul\0byte", 8))); CHECK(!s.add(std::string(BlockStore::kMaxWordBytes + 1, 'x'))); CHECK(s.size() == 2); CHECK(slurp(file) == "伟大实践\n你不能\n"); } { // a hand edited file with Windows line endings and blank lines still loads const fs::path edited = dir / "edited.txt"; std::ofstream(edited, std::ios::binary) << "甲\r\n\r\n乙\r\n"; BlockStore s(edited); CHECK(s.size() == 2 && s.contains("甲") && s.contains("乙")); } { // a change made by someone else is noticed without a restart BlockStore running(file); CHECK(!running.contains("外部")); std::this_thread::sleep_for(std::chrono::milliseconds(20)); { std::ofstream out(file, std::ios::app); out << "外部\n"; } running.reloadIfChanged(); CHECK(running.contains("外部")); // adding after that keeps the outside change, nothing is overwritten CHECK(running.add("新词")); CHECK(slurp(file).find("外部") != std::string::npos); CHECK(slurp(file).find("新词") != std::string::npos); } { // a failed save keeps the word active and is retried, not forgotten const fs::path lockedDir = dir / "locked"; fs::create_directories(lockedDir); const fs::path lockedFile = lockedDir / "blocked-words.txt"; BlockStore s(lockedFile); fs::permissions(lockedDir, fs::perms::owner_read | fs::perms::owner_exec); const bool saved = s.add("保留"); if (::geteuid() != 0) { CHECK(!saved); } CHECK(s.contains("保留")); // still hidden for this session fs::permissions(lockedDir, fs::perms::owner_all); s.reloadIfChanged(); // the retry CHECK(slurp(lockedFile) == "保留\n"); BlockStore restarted(lockedFile); CHECK(restarted.contains("保留")); } { // a file that vanishes while running is written again BlockStore s(file); const auto before = s.size(); fs::remove(file); s.reloadIfChanged(); CHECK(s.size() == before); CHECK(s.add("再来")); BlockStore again(file); CHECK(again.contains("再来") && again.contains("伟大实践")); } fs::remove_all(dir); std::printf(failures ? "%d check(s) failed\n" : "blockstore: all checks passed\n", failures); return failures ? 1 : 0; }