yams/tests/unit/metadata/tree_differ_catch2_test.cpp

373 lines
11 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2025 YAMS Contributors
#include <catch2/catch_test_macros.hpp>
#include <yams/metadata/tree_builder.h>
#include <yams/metadata/tree_differ.h>
namespace fs = std::filesystem;
using namespace yams::metadata;
namespace {
TreeNode createEmptyTree() {
return TreeNode();
}
TreeNode createSimpleTree() {
TreeNode tree;
TreeEntry entry;
entry.mode = 0100644;
entry.name = "file1.txt";
entry.hash = "0000000000000000000000000000000000000000000000000000000000000001";
entry.isDirectory = false;
entry.size = 100;
tree.addEntry(entry);
return tree;
}
TreeNode createTreeWithMultipleFiles() {
TreeNode tree;
TreeEntry f1;
f1.mode = 0100644;
f1.name = "a.txt";
f1.hash = "000000000000000000000000000000000000000000000000000000000000000a";
f1.isDirectory = false;
f1.size = 50;
tree.addEntry(f1);
TreeEntry f2;
f2.mode = 0100644;
f2.name = "b.txt";
f2.hash = "000000000000000000000000000000000000000000000000000000000000000b";
f2.isDirectory = false;
f2.size = 75;
tree.addEntry(f2);
TreeEntry f3;
f3.mode = 0100644;
f3.name = "c.txt";
f3.hash = "000000000000000000000000000000000000000000000000000000000000000c";
f3.isDirectory = false;
f3.size = 120;
tree.addEntry(f3);
return tree;
}
} // namespace
TEST_CASE("TreeDiffer: empty trees produce no changes", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createEmptyTree();
TreeNode target = createEmptyTree();
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 0);
CHECK(result.value().filesAdded == 0);
CHECK(result.value().filesDeleted == 0);
CHECK(result.value().filesModified == 0);
}
TEST_CASE("TreeDiffer: identical trees produce no changes", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode tree = createSimpleTree();
auto result = differ.computeDiff(tree, tree);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 0);
CHECK(result.value().filesAdded == 0);
CHECK(result.value().filesDeleted == 0);
}
TEST_CASE("TreeDiffer: file added", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createEmptyTree();
TreeNode target = createSimpleTree();
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().filesAdded == 1);
CHECK(result.value().filesDeleted == 0);
const auto& change = result.value().changes[0];
CHECK(change.type == ChangeType::Added);
CHECK(change.newPath == "file1.txt");
CHECK(change.newHash == "0000000000000000000000000000000000000000000000000000000000000001");
CHECK_FALSE(change.isDirectory);
}
TEST_CASE("TreeDiffer: file deleted", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createSimpleTree();
TreeNode target = createEmptyTree();
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().filesAdded == 0);
CHECK(result.value().filesDeleted == 1);
const auto& change = result.value().changes[0];
CHECK(change.type == ChangeType::Deleted);
CHECK(change.oldPath == "file1.txt");
CHECK(change.oldHash == "0000000000000000000000000000000000000000000000000000000000000001");
}
TEST_CASE("TreeDiffer: file modified", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createSimpleTree();
// Create target with modified file hash
TreeNode target;
TreeEntry modified;
modified.mode = 0100644;
modified.name = "file1.txt";
modified.hash = "0000000000000000000000000000000000000000000000000000000000000011";
modified.isDirectory = false;
modified.size = 200;
target.addEntry(modified);
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().filesModified == 1);
const auto& change = result.value().changes[0];
CHECK(change.type == ChangeType::Modified);
CHECK(change.oldPath == "file1.txt");
CHECK(change.newPath == "file1.txt");
CHECK(change.oldHash == "0000000000000000000000000000000000000000000000000000000000000001");
CHECK(change.newHash == "0000000000000000000000000000000000000000000000000000000000000011");
}
TEST_CASE("TreeDiffer: file renamed with rename detection enabled",
"[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createSimpleTree();
TreeNode target;
// Same file, different name
TreeEntry renamed;
renamed.mode = 0100644;
renamed.name = "file1_renamed.txt";
renamed.hash = "0000000000000000000000000000000000000000000000000000000000000001"; // Same hash!
renamed.isDirectory = false;
renamed.size = 100;
target.addEntry(renamed);
DiffOptions options;
options.detectRenames = true;
auto result = differ.computeDiff(base, target, options);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().filesRenamed == 1);
const auto& change = result.value().changes[0];
CHECK(change.type == ChangeType::Renamed);
CHECK(change.oldPath == "file1.txt");
CHECK(change.newPath == "file1_renamed.txt");
CHECK(change.oldHash == "0000000000000000000000000000000000000000000000000000000000000001");
CHECK(change.newHash == "0000000000000000000000000000000000000000000000000000000000000001");
}
TEST_CASE("TreeDiffer: rename detection disabled shows delete+add",
"[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createSimpleTree();
TreeNode target;
TreeEntry renamed;
renamed.mode = 0100644;
renamed.name = "file1_renamed.txt";
renamed.hash = "0000000000000000000000000000000000000000000000000000000000000001";
renamed.isDirectory = false;
renamed.size = 100;
target.addEntry(renamed);
DiffOptions options;
options.detectRenames = false;
auto result = differ.computeDiff(base, target, options);
REQUIRE(result.has_value());
// Should see a delete and an add
CHECK(result.value().changes.size() == 2);
CHECK(result.value().filesDeleted == 1);
CHECK(result.value().filesAdded == 1);
}
TEST_CASE("TreeDiffer: multiple changes", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createTreeWithMultipleFiles();
TreeNode target;
// Keep a.txt (unchanged)
TreeEntry a;
a.mode = 0100644;
a.name = "a.txt";
a.hash = "000000000000000000000000000000000000000000000000000000000000000a";
a.isDirectory = false;
a.size = 50;
target.addEntry(a);
// Modify b.txt
TreeEntry b;
b.mode = 0100644;
b.name = "b.txt";
b.hash = "00000000000000000000000000000000000000000000000000000000000000bb";
b.isDirectory = false;
b.size = 80;
target.addEntry(b);
// Delete c.txt (not in target)
// Add d.txt
TreeEntry d;
d.mode = 0100644;
d.name = "d.txt";
d.hash = "000000000000000000000000000000000000000000000000000000000000000d";
d.isDirectory = false;
d.size = 90;
target.addEntry(d);
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
// Should have: 1 modified, 1 deleted, 1 added
CHECK(result.value().changes.size() == 3);
CHECK(result.value().filesAdded == 1);
CHECK(result.value().filesDeleted == 1);
CHECK(result.value().filesModified == 1);
}
TEST_CASE("TreeDiffer: exclude patterns", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base;
TreeNode target;
// Add files to target
TreeEntry f1;
f1.mode = 0100644;
f1.name = "file.txt";
f1.hash = "0000000000000000000000000000000000000000000000000000000000000001";
f1.isDirectory = false;
f1.size = 100;
target.addEntry(f1);
TreeEntry f2;
f2.mode = 0100644;
f2.name = "file.log";
f2.hash = "0000000000000000000000000000000000000000000000000000000000000002";
f2.isDirectory = false;
f2.size = 50;
target.addEntry(f2);
DiffOptions options;
options.excludePatterns = {"*.log"};
auto result = differ.computeDiff(base, target, options);
REQUIRE(result.has_value());
// Should only see file.txt added (file.log excluded)
CHECK(result.value().changes.size() == 1);
CHECK(result.value().changes[0].newPath == "file.txt");
}
TEST_CASE("TreeDiffer: directory changes", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base;
TreeNode target;
// Add directory to target
TreeEntry dir;
dir.mode = 040755;
dir.name = "subdir";
dir.hash = "0000000000000000000000000000000000000000000000000000000000d00001";
dir.isDirectory = true;
dir.size = -1;
target.addEntry(dir);
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().dirsAdded == 1);
CHECK(result.value().changes[0].isDirectory);
}
TEST_CASE("TreeDiffer: diff from serialized data", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base = createSimpleTree();
TreeNode target = createEmptyTree();
// Serialize trees
std::vector<uint8_t> baseData = base.serialize();
std::vector<uint8_t> targetData = target.serialize();
auto result = differ.computeDiffFromData(baseData, targetData);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 1);
CHECK(result.value().filesDeleted == 1);
}
TEST_CASE("TreeDiffer: large diff with many files", "[unit][metadata][tree_differ]") {
TreeDiffer differ;
TreeNode base;
TreeNode target;
// Add 100 files to base
for (int i = 0; i < 100; ++i) {
TreeEntry entry;
entry.mode = 0100644;
entry.name = "file" + std::to_string(i) + ".txt";
// Create proper 64-char hash with index encoded in last 2 digits
char hashBuf[65];
snprintf(hashBuf, sizeof(hashBuf), "%062d%02d", 0, i);
entry.hash = hashBuf;
entry.isDirectory = false;
entry.size = i * 10;
base.addEntry(entry);
}
// Add 100 different files to target
for (int i = 0; i < 100; ++i) {
TreeEntry entry;
entry.mode = 0100644;
entry.name = "newfile" + std::to_string(i) + ".txt";
// Create proper 64-char hash with different prefix
char hashBuf[65];
snprintf(hashBuf, sizeof(hashBuf), "ff%060d%02d", 0, i);
entry.hash = hashBuf;
entry.isDirectory = false;
entry.size = i * 15;
target.addEntry(entry);
}
auto result = differ.computeDiff(base, target);
REQUIRE(result.has_value());
CHECK(result.value().changes.size() == 200);
CHECK(result.value().filesDeleted == 100);
CHECK(result.value().filesAdded == 100);
}
TEST_CASE("TreeDiffer: changeTypeToString", "[unit][metadata][tree_differ]") {
CHECK(std::string(changeTypeToString(ChangeType::Added)) == "Added");
CHECK(std::string(changeTypeToString(ChangeType::Deleted)) == "Deleted");
CHECK(std::string(changeTypeToString(ChangeType::Modified)) == "Modified");
CHECK(std::string(changeTypeToString(ChangeType::Renamed)) == "Renamed");
CHECK(std::string(changeTypeToString(ChangeType::Moved)) == "Moved");
}