yams/tests/unit/metadata/metadata_catch2_test.cpp

194 lines
7.5 KiB
C++

// Catch2 migration of metadata unit tests
// Migration: yams-3s4 / yams-aqc (metadata tests)
// Covers:
// - concepts_compile_test.cpp
// - database_statement_time_test.cpp
// - query_helpers_test.cpp
// - sql_build_select_test.cpp
// - metadata_value_variant_test.cpp
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <variant>
#include <yams/metadata/database.h>
#include <yams/metadata/document_metadata.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/metadata/query_helpers.h>
using namespace std::chrono;
using namespace yams::metadata;
// ============================================================================
// ConceptsCompileTest - verifies static_assert for FullMetadataStore concept
// ============================================================================
TEST_CASE("FullMetadataStore concept is satisfied", "[metadata][concepts]") {
// This test does no runtime checks; including the header ensures
// the compile-time static_assert for FullMetadataStore remains enforced.
SUCCEED();
}
// ============================================================================
// DatabaseStatementChronoTest - chrono bind/getTime roundtrip
// ============================================================================
TEST_CASE("Database statement chrono bind and getTime roundtrip", "[metadata][database]") {
Database db;
REQUIRE(db.open(":memory:", ConnectionMode::Memory).has_value());
REQUIRE(db.execute("CREATE TABLE t(ts INTEGER)").has_value());
// Use a stable epoch value to avoid flakiness
sys_seconds t = sys_seconds{seconds{1'234'567}};
// Insert via chrono bind
auto ins = db.prepare("INSERT INTO t(ts) VALUES(?)");
REQUIRE(ins.has_value());
Statement istmt = std::move(ins).value();
REQUIRE(istmt.bind(1, t).has_value());
REQUIRE(istmt.execute().has_value());
// Read back via getTime
auto sel = db.prepare("SELECT ts FROM t LIMIT 1");
REQUIRE(sel.has_value());
Statement sstmt = std::move(sel).value();
auto step = sstmt.step();
REQUIRE(step.has_value());
REQUIRE(step.value());
sys_seconds out = sstmt.getTime(0);
CHECK(out.time_since_epoch().count() == t.time_since_epoch().count());
}
// ============================================================================
// QueryHelpersTest - buildQueryOptionsForSqlLikePattern
// ============================================================================
TEST_CASE("Query helpers build exact path for exact pattern", "[metadata][query_helpers]") {
auto opts = buildQueryOptionsForSqlLikePattern("/notes/todo.md");
REQUIRE(opts.exactPath.has_value());
CHECK(*opts.exactPath == "/notes/todo.md");
CHECK_FALSE(opts.likePattern.has_value());
}
TEST_CASE("Query helpers set prefix flags for directory pattern", "[metadata][query_helpers]") {
auto opts = buildQueryOptionsForSqlLikePattern("/notes/%");
REQUIRE(opts.pathPrefix.has_value());
CHECK(*opts.pathPrefix == "/notes");
CHECK(opts.prefixIsDirectory);
CHECK(opts.includeSubdirectories);
CHECK_FALSE(opts.likePattern.has_value());
}
TEST_CASE("Query helpers target FTS for contains pattern", "[metadata][query_helpers]") {
auto opts = buildQueryOptionsForSqlLikePattern("%/todo.md");
REQUIRE(opts.containsFragment.has_value());
CHECK(*opts.containsFragment == "todo.md");
CHECK(opts.containsUsesFts);
}
TEST_CASE("Query helpers set extension filter for extension pattern", "[metadata][query_helpers]") {
auto opts = buildQueryOptionsForSqlLikePattern("%.md");
REQUIRE(opts.extension.has_value());
CHECK(*opts.extension == ".md");
}
TEST_CASE("Query helpers fallback keeps LIKE pattern", "[metadata][query_helpers]") {
auto opts = buildQueryOptionsForSqlLikePattern("%notes%2025%");
REQUIRE(opts.likePattern.has_value());
CHECK(*opts.likePattern == "%notes%2025%");
}
// ============================================================================
// SqlBuildSelectTest - buildSelect function
// ============================================================================
TEST_CASE("SQL build select with conditions and order", "[metadata][sql]") {
sql::QuerySpec spec;
spec.table = "documents";
spec.columns = {"id", "file_name"};
spec.conditions = {"mime_type = 'text/plain'", "file_extension = '.md'"};
spec.orderBy = std::optional<std::string>{"indexed_time DESC"};
spec.limit = 10;
spec.offset = 5;
auto sql = sql::buildSelect(spec);
CHECK(sql == "SELECT id, file_name FROM documents WHERE mime_type = 'text/plain' AND "
"file_extension = '.md' ORDER BY indexed_time DESC LIMIT 10 OFFSET 5");
}
TEST_CASE("SQL build select with FROM clause join and GROUP BY", "[metadata][sql]") {
sql::QuerySpec spec;
spec.from =
std::optional<std::string>{"tree_changes tc JOIN tree_diffs td ON tc.diff_id = td.diff_id"};
spec.table = "tree_changes"; // ignored when from is set
spec.columns = {"change_type", "old_path"};
spec.conditions = {"td.base_snapshot_id = ?", "td.target_snapshot_id = ?"};
spec.groupBy = std::optional<std::string>{"change_type"};
spec.orderBy = std::optional<std::string>{"tc.change_id"};
spec.limit = 100;
spec.offset = 10;
auto sql = sql::buildSelect(spec);
CHECK(sql ==
"SELECT change_type, old_path FROM tree_changes tc JOIN tree_diffs td ON tc.diff_id "
"= td.diff_id WHERE td.base_snapshot_id = ? AND td.target_snapshot_id = ? GROUP BY "
"change_type ORDER BY tc.change_id LIMIT 100 OFFSET 10");
}
// ============================================================================
// MetadataValueVariantTest - MetadataValue constructors and variant operations
// ============================================================================
TEST_CASE("MetadataValue constructors and variant types", "[metadata][value]") {
SECTION("String value") {
MetadataValue s{"hello"};
auto vs = s.asVariant();
REQUIRE(std::holds_alternative<std::string>(vs));
CHECK(std::get<std::string>(vs) == "hello");
}
SECTION("Integer value") {
MetadataValue i{int64_t{42}};
auto vi = i.asVariant();
REQUIRE(std::holds_alternative<int64_t>(vi));
CHECK(std::get<int64_t>(vi) == 42);
CHECK(i.asInteger() == 42);
}
SECTION("Double value") {
MetadataValue d{3.14};
auto vd = d.asVariant();
REQUIRE(std::holds_alternative<double>(vd));
CHECK(std::get<double>(vd) == Catch::Approx(3.14));
}
SECTION("Boolean value") {
MetadataValue b{true};
auto vb = b.asVariant();
REQUIRE(std::holds_alternative<bool>(vb));
CHECK(std::get<bool>(vb) == true);
}
SECTION("Blob value") {
std::vector<uint8_t> blob = {1, 2, 3};
auto mv = MetadataValue::fromBlob(blob);
auto vv = mv.asVariant();
REQUIRE(std::holds_alternative<std::vector<uint8_t>>(vv));
CHECK(std::get<std::vector<uint8_t>>(vv).size() == 3u);
}
}
TEST_CASE("MetadataValue setVariant syncs legacy fields", "[metadata][value]") {
MetadataValue v;
SECTION("Integer variant syncs legacy") {
v.setVariant(int64_t{99});
CHECK(v.type == MetadataValueType::Integer);
CHECK(v.asInteger() == 99);
CHECK(v.value == std::string("99"));
}
SECTION("String variant syncs legacy") {
v.setVariant(std::string{"abc"});
CHECK(v.type == MetadataValueType::String);
CHECK(v.asString() == "abc");
}
}