// Тест SQLite-слоя: Frontmatter, VaultSettings, Database (индекс, запросы, // свойства) и ~sql-блоки в MdParser (таблица / fallback / ошибка). #include #include #include #include #include #include "core/Frontmatter.h" #include "core/VaultSettings.h" #include "core/VaultIndex.h" #include "db/Database.h" #include "markdown/MdParser.h" static int fail(const char *msg, const QString &detail = QString()) { std::fprintf(stderr, "FAIL: %s | %s\n", msg, qPrintable(detail)); return 1; } static void writeFile(const QString &path, const QString &text) { QFile f(path); f.open(QIODevice::WriteOnly | QIODevice::Truncate); f.write(text.toUtf8()); } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("FloraTest"); QCoreApplication::setOrganizationName("FloraTest"); // --- Frontmatter: парс --- { Frontmatter::Parsed fp = Frontmatter::parse( "---\nстатус: в работе\nприоритет: 1\n# коммент\nмусор без двоеточия\n---\n\nТело\n"); if (!fp.hasFrontmatter) return fail("fm has"); if (fp.props.value("статус").toString() != "в работе") return fail("fm статус", fp.props.value("статус").toString()); if (fp.props.value("приоритет").toString() != "1") return fail("fm приоритет"); if (fp.raw.size() != 2) return fail("fm raw", QString::number(fp.raw.size())); if (!fp.body.contains("Тело")) return fail("fm body", fp.body); } // --- Frontmatter: нет шапки / незакрытая (body при этом целый!) --- { Frontmatter::Parsed a = Frontmatter::parse("Просто текст\n"); if (a.hasFrontmatter || !a.props.isEmpty()) return fail("fm no fence"); if (a.body != "Просто текст\n") return fail("fm body kept", a.body); Frontmatter::Parsed b = Frontmatter::parse("---\nключ: 1\nбез закрытия\n"); if (b.hasFrontmatter) return fail("fm unclosed = текст"); } // --- Frontmatter: roundtrip --- { QVariantMap props; props.insert("b", "2"); props.insert("a", "1"); const QString s = Frontmatter::serialize(props, QStringList() << "# raw", "Тело"); Frontmatter::Parsed fp = Frontmatter::parse(s); if (fp.props.value("a").toString() != "1" || fp.props.value("b").toString() != "2") return fail("fm roundtrip props", s); if (fp.raw.size() != 1 || fp.body.trimmed() != "Тело") return fail("fm roundtrip raw/body", s); } // --- VaultSettings: дефолты + persist --- QTemporaryDir dir; if (!dir.isValid()) return fail("temp dir"); const QString vault = dir.path(); { VaultSettings vs; if (!vs.open(vault)) return fail("vs open"); if (vs.journalDir() != "Журнал" || !vs.sqlEnabled() || vs.sqlMaxRows() != 50) return fail("vs defaults"); vs.setJournalDir("../../etc"); // мусор → дефолт if (vs.journalDir() != "Журнал") return fail("vs sanitize", vs.journalDir()); vs.setJournalDir("Дневник"); vs.setSqlMaxRows(20); if (!vs.save()) return fail("vs save"); VaultSettings vs2; vs2.open(vault); if (vs2.journalDir() != "Дневник" || vs2.sqlMaxRows() != 20) return fail("vs roundtrip"); } // --- Хранилище: два файла со свойствами, тегами и ссылкой --- writeFile(vault + "/a.md", "---\nстатус: в работе\n---\n\nТекст #тег1 [[b]]\n"); writeFile(vault + "/b.md", "# Бэ\n\nТекст #тег1 #тег2\n"); VaultIndex index; index.rebuild(vault); if (index.fileCount() != 2) return fail("index count", QString::number(index.fileCount())); Database db; db.setIndex(&index); if (!db.open(vault)) return fail("db open"); // open сам подхватил готовый индекс QVariantMap st = db.stats(); if (st.value("files").toInt() != 2) return fail("db files", QString::number(st.value("files").toInt())); if (st.value("tags").toInt() != 2) return fail("db tags", QString::number(st.value("tags").toInt())); if (st.value("links").toInt() != 1) return fail("db links", QString::number(st.value("links").toInt())); if (st.value("properties").toInt() != 1) return fail("db props", QString::number(st.value("properties").toInt())); // --- SELECT работает (включая многоколоночные: строки не сплющиваются) --- { QVariantMap r = db.queryTable("SELECT title FROM files ORDER BY title"); if (!r.value("error").toString().isEmpty()) return fail("select err", r.value("error").toString()); if (r.value("columns").toStringList() != QStringList() << "title") return fail("select cols", r.value("columns").toStringList().join(",")); const QVariantList rows = r.value("rows").toList(); if (rows.size() != 2) return fail("select rows"); if (rows[0].toList().size() != 1 || rows[0].toList()[0].toString() != "a") return fail("select cell"); } { QVariantMap r = db.queryTable("SELECT path, title FROM files ORDER BY title"); const QVariantList rows = r.value("rows").toList(); if (rows.size() != 2 || rows[0].toList().size() != 2) return fail("select shape"); if (!rows[0].toList()[0].toString().endsWith("/a.md") || rows[0].toList()[1].toString() != "a") return fail("select cells", rows[0].toList()[0].toString()); } // --- Пишущие запросы режутся --- { QVariantMap r = db.queryTable("INSERT INTO tags (name) VALUES ('x')"); if (r.value("error").toString().isEmpty()) return fail("insert не зарезан"); QVariantMap r2 = db.queryTable("SELECT * FROM tags; DELETE FROM tags"); if (r2.value("error").toString().isEmpty()) return fail("multi не зарезан"); // SELECT с запрещённым словом внутри строки — можно (фильтр по коду, не по строке) QVariantMap r3 = db.queryTable("SELECT 'update' AS u"); if (!r3.value("error").toString().isEmpty()) return fail("строка update зря зарезана", r3.value("error").toString()); } // --- Свойства: чтение + запись --- { QVariantMap p = db.fileProperties(vault + "/a.md"); if (p.value("статус").toString() != "в работе") return fail("props read"); QVariantMap np; np.insert("статус", "готово"); np.insert("оценка", "5"); const QString err = db.saveFileProperties(vault + "/a.md", np); if (!err.isEmpty()) return fail("props save", err); index.updateFile(vault + "/a.md"); db.syncFile(vault + "/a.md"); QVariantMap p2 = db.fileProperties(vault + "/a.md"); if (p2.value("статус").toString() != "готово" || p2.value("оценка").toString() != "5") return fail("props roundtrip"); // тело и ссылка не пострадали QFile f(vault + "/a.md"); f.open(QIODevice::ReadOnly); const QString t = QString::fromUtf8(f.readAll()); if (!t.contains("[[b]]") || !t.contains("#тег1")) return fail("props body", t); // плохое имя свойства QVariantMap bad; bad.insert("плохо имя!", "x"); if (db.saveFileProperties(vault + "/a.md", bad).isEmpty()) return fail("props bad key"); } // --- MdParser: ~sql с базой → таблица --- { MdParser p; p.setDatabaseForVault(vault, &db); const QVariantList blocks = p.parseBlocks( "Текст\n\n```~sql\nSELECT title FROM files ORDER BY title\n```\n", vault, vault); if (blocks.size() != 2) return fail("sql blocks", QString::number(blocks.size())); if (blocks[1].toMap().value("type").toString() != "html") return fail("sql type", blocks[1].toMap().value("type").toString()); const QString html = blocks[1].toMap().value("html").toString(); // title = имя файла без расширения if (!html.contains("a") || !html.contains(">b")) return fail("sql table", html.left(300)); // plain render тоже таблицей const QString h = p.render("```~sql\nSELECT title FROM files\n```\n", vault, vault); if (!h.contains("