Files
Flora/tests/db_test.cpp
T

241 lines
11 KiB
C++
Raw Normal View History

// Тест 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;
}