Files
Flora/tests/db_test.cpp
T
grendervill fdf59f5b39 SQLite-база хранилища: свойства, ~sql-блоки, настройки хранилища
- Frontmatter: шапка --- ключ: значение (источник правды свойств)
- VaultSettings: .flora/settings.json (журнал, sqlEnabled, sqlMaxRows)
- Database: files/tags/file_tags/links/properties, только SELECT/WITH
  на read-only соединении (mode=ro + query_only + зонд), лимит строк
- MdParser: ~sql-блоки таблицей (parseBlocks + Renderer), шапка скрыта в просмотре
- Окно: per-window Database/VaultSettings, sync при сохранении, дебаунс reindex
- QML: VaultSettingsDialog, FilePropertiesDialog, меню, автообновление ~sql
- FileStore.journalDir, DocumentManager.reloadDocument
- Тест flora_db_test (23/23 зелёных)
2026-09-07 16:30:06 +03:00

241 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Тест SQLite-слоя: Frontmatter, VaultSettings, Database (индекс, запросы,
// свойства) и ~sql-блоки в MdParser (таблица / fallback / ошибка).
#include <QCoreApplication>
#include <QTemporaryDir>
#include <QDir>
#include <QFile>
#include <cstdio>
#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("<table") || !html.contains(">a</td>") || !html.contains(">b</td>"))
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("<table"))
return fail("sql render", h.left(300));
// ошибка запроса → красный бокс, не падение
const QVariantList be = p.parseBlocks("```~sql\nSELECT * FROM нет_таблицы\n```\n", vault, vault);
if (!be[0].toMap().value("html").toString().contains("SQL:"))
return fail("sql errbox");
}
// --- MdParser: ~sql без базы → исходник как код ---
{
MdParser p;
const QVariantList blocks = p.parseBlocks("```~sql\nSELECT 1\n```\n", vault, vault);
if (blocks.size() != 1 || blocks[0].toMap().value("type").toString() != "code")
return fail("sql fallback");
}
// --- MdParser: шапка не светится в просмотре ---
{
MdParser p;
const QString h = p.render("---\nстатус: секрет\n---\n\n# Заголовок\n", vault, vault);
if (h.contains("секрет") || !h.contains("Заголовок"))
return fail("fm hidden", h.left(200));
}
// --- Свойства в файл без шапки: тело сохраняется ---
{
writeFile(vault + "/plain.md", "# Обычная\n\nТекст без шапки\n");
index.rebuild(vault);
db.reindex();
QVariantMap np;
np.insert("k", "v");
if (!db.saveFileProperties(vault + "/plain.md", np).isEmpty())
return fail("plain props save");
QFile f(vault + "/plain.md");
f.open(QIODevice::ReadOnly);
const QString t = QString::fromUtf8(f.readAll());
if (!t.contains("Текст без шапки") || !t.contains("k: v"))
return fail("plain body kept", t.left(200));
}
std::printf("db_test OK\n");
return 0;
}