254 lines
9.8 KiB
C++
254 lines
9.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright 2025 YAMS Contributors
|
|
|
|
// Unit tests for path_tree removePathTreeForDocument functionality
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
|
|
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/metadata/connection_pool.h>
|
|
#include <yams/metadata/metadata_repository.h>
|
|
#include <yams/metadata/migration.h>
|
|
|
|
using namespace yams::metadata;
|
|
|
|
namespace {
|
|
|
|
std::filesystem::path tempTestDir(const char* prefix) {
|
|
const char* t = std::getenv("YAMS_TEST_TMPDIR");
|
|
auto base = (t && *t) ? std::filesystem::path(t) : std::filesystem::temp_directory_path();
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(base, ec);
|
|
auto ts = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
return base / (std::string(prefix) + std::to_string(ts));
|
|
}
|
|
|
|
struct PathTreeDeleteFixture {
|
|
PathTreeDeleteFixture() {
|
|
testDir_ = tempTestDir("yams_path_tree_delete_catch2_test_");
|
|
std::filesystem::create_directories(testDir_);
|
|
dbPath_ = testDir_ / "test.db";
|
|
|
|
ConnectionPoolConfig poolCfg;
|
|
poolCfg.minConnections = 1;
|
|
poolCfg.maxConnections = 2;
|
|
|
|
pool_ = std::make_unique<ConnectionPool>(dbPath_.string(), poolCfg);
|
|
auto initRes = pool_->initialize();
|
|
REQUIRE(initRes.has_value());
|
|
|
|
repository_ = std::make_shared<MetadataRepository>(*pool_);
|
|
|
|
// Run migrations
|
|
auto connRes = pool_->acquire();
|
|
REQUIRE(connRes.has_value());
|
|
auto& db = **connRes.value();
|
|
yams::metadata::MigrationManager mgr(db);
|
|
mgr.registerMigrations(yams::metadata::YamsMetadataMigrations::getAllMigrations());
|
|
auto initRes2 = mgr.initialize();
|
|
REQUIRE(initRes2.has_value());
|
|
auto migrate = mgr.migrate();
|
|
REQUIRE(migrate.has_value());
|
|
}
|
|
|
|
~PathTreeDeleteFixture() {
|
|
repository_.reset();
|
|
pool_.reset();
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(testDir_, ec);
|
|
}
|
|
|
|
DocumentInfo makeDocumentWithPath(const std::string& path, const std::string& hash) {
|
|
DocumentInfo doc;
|
|
doc.sha256Hash = hash;
|
|
doc.filePath = path;
|
|
doc.mimeType = "text/plain";
|
|
doc.fileExtension = ".txt";
|
|
doc.fileSize = 100;
|
|
auto now =
|
|
std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());
|
|
doc.indexedTime = now;
|
|
doc.modifiedTime = now;
|
|
doc.createdTime = now;
|
|
return doc;
|
|
}
|
|
|
|
std::filesystem::path testDir_;
|
|
std::filesystem::path dbPath_;
|
|
std::unique_ptr<ConnectionPool> pool_;
|
|
std::shared_ptr<MetadataRepository> repository_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("PathTreeDelete: remove document decrements count", "[unit][metadata][path_tree]") {
|
|
PathTreeDeleteFixture fix;
|
|
|
|
auto docInfo = fix.makeDocumentWithPath(
|
|
"/src/example.txt", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
|
auto docInsert = fix.repository_->insertDocument(docInfo);
|
|
REQUIRE(docInsert.has_value());
|
|
auto docId = docInsert.value();
|
|
docInfo.id = docId;
|
|
|
|
auto upsert = fix.repository_->upsertPathTreeForDocument(docInfo, docId, true, {});
|
|
REQUIRE(upsert.has_value());
|
|
|
|
auto lookup = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(lookup.has_value());
|
|
REQUIRE(lookup.value().has_value());
|
|
CHECK(lookup.value()->docCount == 1);
|
|
|
|
auto remove = fix.repository_->removePathTreeForDocument(docInfo, docId, {});
|
|
REQUIRE(remove.has_value());
|
|
|
|
auto afterRemove = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(afterRemove.has_value());
|
|
REQUIRE(afterRemove.value().has_value());
|
|
CHECK(afterRemove.value()->docCount == 0);
|
|
}
|
|
|
|
TEST_CASE("PathTreeDelete: remove document deletes empty nodes", "[unit][metadata][path_tree]") {
|
|
PathTreeDeleteFixture fix;
|
|
|
|
auto docInfo = fix.makeDocumentWithPath(
|
|
"/src/lib/example.txt", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
|
|
auto docInsert = fix.repository_->insertDocument(docInfo);
|
|
REQUIRE(docInsert.has_value());
|
|
auto docId = docInsert.value();
|
|
docInfo.id = docId;
|
|
|
|
auto upsert = fix.repository_->upsertPathTreeForDocument(docInfo, docId, true, {});
|
|
REQUIRE(upsert.has_value());
|
|
|
|
auto srcLookup = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(srcLookup.has_value());
|
|
REQUIRE(srcLookup.value().has_value());
|
|
|
|
auto libLookup = fix.repository_->findPathTreeNode(srcLookup.value()->id, "lib");
|
|
REQUIRE(libLookup.has_value());
|
|
REQUIRE(libLookup.value().has_value());
|
|
|
|
auto remove = fix.repository_->removePathTreeForDocument(docInfo, docId, {});
|
|
REQUIRE(remove.has_value());
|
|
|
|
auto leafLookup = fix.repository_->findPathTreeNodeByFullPath("/src/lib/example.txt");
|
|
REQUIRE(leafLookup.has_value());
|
|
CHECK_FALSE(leafLookup.value().has_value());
|
|
|
|
auto libAfter = fix.repository_->findPathTreeNode(srcLookup.value()->id, "lib");
|
|
REQUIRE(libAfter.has_value());
|
|
CHECK_FALSE(libAfter.value().has_value());
|
|
}
|
|
|
|
TEST_CASE("PathTreeDelete: remove document recalculates centroid", "[unit][metadata][path_tree]") {
|
|
PathTreeDeleteFixture fix;
|
|
|
|
auto doc1 = fix.makeDocumentWithPath(
|
|
"/src/file1.txt", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC");
|
|
auto doc1Insert = fix.repository_->insertDocument(doc1);
|
|
REQUIRE(doc1Insert.has_value());
|
|
doc1.id = doc1Insert.value();
|
|
|
|
auto doc2 = fix.makeDocumentWithPath(
|
|
"/src/file2.txt", "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD");
|
|
auto doc2Insert = fix.repository_->insertDocument(doc2);
|
|
REQUIRE(doc2Insert.has_value());
|
|
doc2.id = doc2Insert.value();
|
|
|
|
std::vector<float> embedding1{1.0F, 2.0F, 3.0F};
|
|
std::vector<float> embedding2{5.0F, 6.0F, 7.0F};
|
|
|
|
auto upsert1 = fix.repository_->upsertPathTreeForDocument(
|
|
doc1, doc1.id, true, std::span<const float>(embedding1.data(), embedding1.size()));
|
|
REQUIRE(upsert1.has_value());
|
|
|
|
auto upsert2 = fix.repository_->upsertPathTreeForDocument(
|
|
doc2, doc2.id, true, std::span<const float>(embedding2.data(), embedding2.size()));
|
|
REQUIRE(upsert2.has_value());
|
|
|
|
auto srcNode = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(srcNode.has_value());
|
|
REQUIRE(srcNode.value().has_value());
|
|
CHECK(srcNode.value()->centroidWeight == 2);
|
|
REQUIRE(srcNode.value()->centroid.size() == 3);
|
|
|
|
// Expected centroid: [(1+5)/2, (2+6)/2, (3+7)/2] = [3.0, 4.0, 5.0]
|
|
CHECK(srcNode.value()->centroid[0] == Catch::Approx(3.0F).epsilon(0.01));
|
|
CHECK(srcNode.value()->centroid[1] == Catch::Approx(4.0F).epsilon(0.01));
|
|
CHECK(srcNode.value()->centroid[2] == Catch::Approx(5.0F).epsilon(0.01));
|
|
|
|
auto remove = fix.repository_->removePathTreeForDocument(
|
|
doc1, doc1.id, std::span<const float>(embedding1.data(), embedding1.size()));
|
|
REQUIRE(remove.has_value());
|
|
|
|
auto srcAfter = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(srcAfter.has_value());
|
|
REQUIRE(srcAfter.value().has_value());
|
|
CHECK(srcAfter.value()->centroidWeight == 1);
|
|
REQUIRE(srcAfter.value()->centroid.size() == 3);
|
|
CHECK(srcAfter.value()->centroid[0] == Catch::Approx(5.0F).epsilon(0.01));
|
|
CHECK(srcAfter.value()->centroid[1] == Catch::Approx(6.0F).epsilon(0.01));
|
|
CHECK(srcAfter.value()->centroid[2] == Catch::Approx(7.0F).epsilon(0.01));
|
|
}
|
|
|
|
TEST_CASE("PathTreeDelete: remove nonexistent document succeeds", "[unit][metadata][path_tree]") {
|
|
PathTreeDeleteFixture fix;
|
|
|
|
auto docInfo = fix.makeDocumentWithPath(
|
|
"/nonexistent/file.txt",
|
|
"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE");
|
|
auto docInsert = fix.repository_->insertDocument(docInfo);
|
|
REQUIRE(docInsert.has_value());
|
|
docInfo.id = docInsert.value();
|
|
|
|
auto remove = fix.repository_->removePathTreeForDocument(docInfo, docInfo.id, {});
|
|
CHECK(remove.has_value());
|
|
}
|
|
|
|
TEST_CASE("PathTreeDelete: remove document keeps shared ancestors", "[unit][metadata][path_tree]") {
|
|
PathTreeDeleteFixture fix;
|
|
|
|
auto doc1 = fix.makeDocumentWithPath(
|
|
"/src/dir1/file1.txt", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
|
auto doc1Insert = fix.repository_->insertDocument(doc1);
|
|
REQUIRE(doc1Insert.has_value());
|
|
doc1.id = doc1Insert.value();
|
|
|
|
auto doc2 = fix.makeDocumentWithPath(
|
|
"/src/dir2/file2.txt", "1111111111111111111111111111111111111111111111111111111111111111");
|
|
auto doc2Insert = fix.repository_->insertDocument(doc2);
|
|
REQUIRE(doc2Insert.has_value());
|
|
doc2.id = doc2Insert.value();
|
|
|
|
auto upsert1 = fix.repository_->upsertPathTreeForDocument(doc1, doc1.id, true, {});
|
|
REQUIRE(upsert1.has_value());
|
|
|
|
auto upsert2 = fix.repository_->upsertPathTreeForDocument(doc2, doc2.id, true, {});
|
|
REQUIRE(upsert2.has_value());
|
|
|
|
auto srcBefore = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(srcBefore.has_value());
|
|
REQUIRE(srcBefore.value().has_value());
|
|
CHECK(srcBefore.value()->docCount == 2);
|
|
|
|
auto remove = fix.repository_->removePathTreeForDocument(doc1, doc1.id, {});
|
|
REQUIRE(remove.has_value());
|
|
|
|
auto srcAfter = fix.repository_->findPathTreeNode(PathTreeNode::kNullParent, "src");
|
|
REQUIRE(srcAfter.has_value());
|
|
REQUIRE(srcAfter.value().has_value());
|
|
CHECK(srcAfter.value()->docCount == 1);
|
|
|
|
auto dir1After = fix.repository_->findPathTreeNodeByFullPath("/src/dir1");
|
|
REQUIRE(dir1After.has_value());
|
|
CHECK_FALSE(dir1After.value().has_value());
|
|
|
|
auto dir2After = fix.repository_->findPathTreeNodeByFullPath("/src/dir2");
|
|
REQUIRE(dir2After.has_value());
|
|
CHECK(dir2After.value().has_value());
|
|
}
|