yams/tests/unit/integrity/integrity_verifier_catch2_test.cpp
Trevon 4e97b31fef
Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
chore(warnings): clean up compiler warnings on GCC, Clang and Apple Clang (#279)
* chore(warnings): default-initialize aggregate members left out of brace-init

GCC -Wmissing-field-initializers fired about 1300 times where tests and
src build structs with a short positional brace list. Give the omitted
members an empty default member initializer. Value-initialization from
{} is what aggregate init already did for them, so behavior is
unchanged.

Signed-off-by: trvon <me@trevon.dev>

* chore(warnings): remove or annotate unused parameters, functions and variables

- init_command, s3_signer test, request_handler_lifetime test: comment out
  the unused parameter name.
- video_content_handler: path is only read under YAMS_HAVE_FFPROBE.
- vector_memory_scaling_bench: main() takes no arguments.
- daemon_status_render: delete unused neutralText helper.
- DaemonMetrics: mallocStackLoggingNoCompactEnabled is only called in the
  __APPLE__ block, so define it there.
- SocketServer: einval_streak only matters in the __APPLE__ EINVAL path.
- onnx_plugin_harness test, vector_backend_engine_compare bench,
  work_coordinator test: mark platform-conditional or busy-loop
  variables [[maybe_unused]].

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): fix sign-compare, range-loop, nodiscard and dangling-ref in tests

- search_admission_controller: count started workers in std::uint32_t so
  the comparison with the uint32 cap is same-signed.
- p2p_manager: iterate the invalid-host list by const reference.
- integrity_verifier: discard the nodiscard results the tests do not
  inspect with static_cast<void>.
- post_ingest_queue: findNode/findEdge take std::string_view so no
  temporary std::string is created for the call (GCC
  -Wdangling-reference false positive on the returned reference).
- topology_ablation_bench: the doc comment ended a line with a backslash,
  which continues the comment (-Wcomment); show the env export on its own
  line.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): drop redundant std::move of a const captured json

The coroutine lambda is not mutable, so the captured optional<json> is
const and std::move(*progressToken) already selected the copy assignment.
Drop the std::move so the code states what actually happens.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): mark __int128 use in db_integrity_stamp as an extension

-Wpedantic flags __int128. Use a __extension__ typedef, the GCC/Clang
idiom for an intentional extension, instead of the bare keyword.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): remove always-true unsigned >= 0 comparisons

GCC -Wtype-limits flagged 51 test assertions of the form
CHECK(unsignedValue >= 0) that can never fail, plus a dead
`totalCount.value() < 0` clause in the graph listByType handler (the
count is std::size_t). In the tests keep the evaluation as a
static_cast<void> so the accessors are still exercised; in the handler
drop the dead clause and keep the range check.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
2026-10-08 14:30:28 -06:00

361 lines
13 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include "../../common/test_helpers_catch2.h"
#include <yams/crypto/hasher.h>
#include <yams/integrity/verifier.h>
#include <yams/storage/reference_counter.h>
#include <yams/storage/storage_engine.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory>
#include <span>
#include <string>
#include <vector>
using namespace yams;
using namespace yams::integrity;
using namespace yams::storage;
namespace {
struct IntegrityVerifierFixture {
yams::test::TempDirGuard tempDirGuard{"yams_integrity_verifier_"};
std::filesystem::path testDir{tempDirGuard.path()};
std::unique_ptr<StorageEngine> storageEngine;
std::unique_ptr<ReferenceCounter> refCounter;
std::unique_ptr<IntegrityVerifier> verifier;
std::unique_ptr<crypto::IContentHasher> hasher;
std::vector<std::byte> testDataBytes;
std::string testDataString;
std::string testHash;
IntegrityVerifierFixture() {
StorageConfig config{
.basePath = testDir, .shardDepth = 2, .mutexPoolSize = 128, .enableCompression = false};
storageEngine = std::make_unique<StorageEngine>(std::move(config));
ReferenceCounter::Config refConfig;
refConfig.databasePath = testDir / "refs.db";
refCounter = std::make_unique<ReferenceCounter>(refConfig);
VerificationConfig verifyConfig;
verifyConfig.maxConcurrentVerifications = 2;
verifyConfig.blocksPerSecond = 100;
verifyConfig.enableAutoRepair = false;
verifier = std::make_unique<IntegrityVerifier>(*storageEngine, *refCounter, verifyConfig);
testDataBytes = yams::test::TestVectors::getHelloWorldData();
testDataString = "Hello World";
hasher = crypto::createSHA256Hasher();
testHash = hasher->hash(testDataBytes);
}
~IntegrityVerifierFixture() {
if (verifier && verifier->isRunning()) {
static_cast<void>(verifier->stopBackgroundVerification());
}
verifier.reset();
refCounter.reset();
storageEngine.reset();
}
IntegrityVerifierFixture(const IntegrityVerifierFixture&) = delete;
IntegrityVerifierFixture& operator=(const IntegrityVerifierFixture&) = delete;
std::string storeBlock(const std::vector<std::byte>& data) {
auto dataHash = hasher->hash(data);
auto storeResult =
storageEngine->store(dataHash, std::span<const std::byte>(data.data(), data.size()));
REQUIRE(storeResult.has_value());
auto refResult = refCounter->increment(dataHash, data.size());
REQUIRE(refResult.has_value());
return dataHash;
}
void storeTestBlock() { storeBlock(testDataBytes); }
void corruptTestBlock() {
// Mirror StorageEngine::getObjectPath sharding (shardDepth=2).
auto blockPath = testDir / "objects" / testHash.substr(0, 2) / testHash.substr(2);
if (std::filesystem::exists(blockPath)) {
std::ofstream file(blockPath, std::ios::binary | std::ios::out);
if (file.is_open()) {
std::string corruptData = "CORRUPTED DATA";
file.write(corruptData.data(),
yams::test::checked_streamsize(corruptData.size(),
"integrity corrupt fixture"));
file.close();
}
}
}
};
} // namespace
TEST_CASE_METHOD(IntegrityVerifierFixture, "ConstructorAndDestructor", "[integrity][verifier]") {
CHECK_FALSE(verifier->isRunning());
CHECK_FALSE(verifier->isPaused());
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == 0);
CHECK(stats.verificationErrorsTotal.load() == 0);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyValidBlock", "[integrity][verifier]") {
storeTestBlock();
auto result = verifier->verifyBlock(testHash);
CHECK(result.status == VerificationStatus::Passed);
CHECK(result.blockHash == testHash);
CHECK(result.blockSize == testDataBytes.size());
CHECK(result.errorDetails.empty());
CHECK(result.isSuccess());
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == 1);
CHECK(stats.verificationErrorsTotal.load() == 0);
CHECK(stats.bytesVerifiedTotal.load() == testDataBytes.size());
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyMissingBlock", "[integrity][verifier]") {
std::string missingHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
auto result = verifier->verifyBlock(missingHash);
CHECK(result.status == VerificationStatus::Missing);
CHECK(result.blockHash == missingHash);
CHECK(result.blockSize == 0);
CHECK_FALSE(result.errorDetails.empty());
CHECK_FALSE(result.isSuccess());
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == 0);
CHECK(stats.verificationErrorsTotal.load() == 1);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyCorruptedBlock", "[integrity][verifier]") {
storeTestBlock();
corruptTestBlock();
auto result = verifier->verifyBlock(testHash);
CHECK(result.status == VerificationStatus::Corrupted);
CHECK(result.blockHash == testHash);
CHECK(result.blockSize > 0);
CHECK_FALSE(result.errorDetails.empty());
CHECK_FALSE(result.isSuccess());
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == 0);
CHECK(stats.verificationErrorsTotal.load() == 1);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyMultipleBlocks", "[integrity][verifier]") {
std::vector<std::vector<std::byte>> testBlocks = {yams::test::TestVectors::getABCData(),
yams::test::TestVectors::getHelloWorldData()};
std::vector<std::string> hashes;
hashes.reserve(testBlocks.size());
for (const auto& data : testBlocks) {
hashes.push_back(storeBlock(data));
}
auto results = verifier->verifyBlocks(hashes);
REQUIRE(results.size() == testBlocks.size());
for (size_t i = 0; i < results.size(); ++i) {
CHECK(results[i].status == VerificationStatus::Passed);
CHECK(results[i].blockHash == hashes[i]);
CHECK(results[i].isSuccess());
}
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == testBlocks.size());
CHECK(stats.verificationErrorsTotal.load() == 0);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "BackgroundVerificationLifecycle",
"[integrity][verifier]") {
CHECK_FALSE(verifier->isRunning());
auto startResult = verifier->startBackgroundVerification();
CHECK(startResult.has_value());
CHECK(verifier->isRunning());
CHECK_FALSE(verifier->isPaused());
auto startAgainResult = verifier->startBackgroundVerification();
CHECK_FALSE(startAgainResult.has_value());
CHECK(startAgainResult.error() == ErrorCode::OperationInProgress);
auto stopResult = verifier->stopBackgroundVerification();
CHECK(stopResult.has_value());
CHECK_FALSE(verifier->isRunning());
auto stopAgainResult = verifier->stopBackgroundVerification();
CHECK(stopAgainResult.has_value());
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "PauseResumeVerification", "[integrity][verifier]") {
auto startResult = verifier->startBackgroundVerification();
CHECK(startResult.has_value());
CHECK(verifier->isRunning());
CHECK_FALSE(verifier->isPaused());
auto pauseResult = verifier->pauseVerification();
CHECK(pauseResult.has_value());
CHECK(verifier->isRunning());
CHECK(verifier->isPaused());
auto resumeResult = verifier->resumeVerification();
CHECK(resumeResult.has_value());
CHECK(verifier->isRunning());
CHECK_FALSE(verifier->isPaused());
static_cast<void>(verifier->stopBackgroundVerification());
auto pauseFailResult = verifier->pauseVerification();
CHECK_FALSE(pauseFailResult.has_value());
CHECK(pauseFailResult.error() == ErrorCode::InvalidOperation);
auto resumeFailResult = verifier->resumeVerification();
CHECK_FALSE(resumeFailResult.has_value());
CHECK(resumeFailResult.error() == ErrorCode::InvalidOperation);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "StatisticsAndReporting", "[integrity][verifier]") {
storeTestBlock();
const auto& initialStats = verifier->getStatistics();
CHECK(initialStats.blocksVerifiedTotal.load() == 0);
static_cast<void>(verifier->verifyBlock(testHash));
const auto& stats = verifier->getStatistics();
CHECK(stats.blocksVerifiedTotal.load() == 1);
CHECK(stats.totalVerificationTime.count() >= 0);
CHECK(stats.getVerificationRate() >= 0.0);
auto report = verifier->generateReport(std::chrono::hours{1});
CHECK(report.blocksVerified == 1);
CHECK(report.blocksPassed == 1);
CHECK(report.blocksFailed == 0);
CHECK(report.totalBytes > 0);
CHECK(report.getSuccessRate() > 0.99);
CHECK(report.getErrorRate() < 0.01);
verifier->resetStatistics();
const auto& resetStats = verifier->getStatistics();
CHECK(resetStats.blocksVerifiedTotal.load() == 0);
CHECK(resetStats.verificationErrorsTotal.load() == 0);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "RecentFailures", "[integrity][verifier]") {
storeTestBlock();
corruptTestBlock();
static_cast<void>(verifier->verifyBlock(testHash));
auto failures = verifier->getRecentFailures(10);
REQUIRE(failures.size() == 1);
CHECK(failures[0].status == VerificationStatus::Corrupted);
CHECK(failures[0].blockHash == testHash);
CHECK_FALSE(failures[0].isSuccess());
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "ConfigurationUpdate", "[integrity][verifier]") {
VerificationConfig newConfig;
newConfig.maxConcurrentVerifications = 8;
newConfig.blocksPerSecond = 500;
newConfig.enableAutoRepair = true;
CHECK_NOTHROW(verifier->updateConfig(newConfig));
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "CallbackFunctionality", "[integrity][verifier]") {
storeTestBlock();
bool progressCallbackCalled = false;
VerificationResult capturedResult;
verifier->setProgressCallback([&](const VerificationResult& result) {
progressCallbackCalled = true;
capturedResult = result;
});
bool alertCallbackCalled = false;
verifier->setAlertCallback(
[&](const IntegrityReport& /*report*/) { alertCallbackCalled = true; });
static_cast<void>(verifier->verifyBlock(testHash));
CHECK(progressCallbackCalled);
CHECK(capturedResult.blockHash == testHash);
CHECK(capturedResult.status == VerificationStatus::Passed);
(void)alertCallbackCalled;
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "SchedulingStrategyChange", "[integrity][verifier]") {
CHECK_NOTHROW(verifier->setSchedulingStrategy(SchedulingStrategy::ByAge));
CHECK_NOTHROW(verifier->setSchedulingStrategy(SchedulingStrategy::BySize));
CHECK_NOTHROW(verifier->setSchedulingStrategy(SchedulingStrategy::ByFailures));
CHECK_NOTHROW(verifier->setSchedulingStrategy(SchedulingStrategy::ByAccess));
CHECK_NOTHROW(verifier->setSchedulingStrategy(SchedulingStrategy::Balanced));
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyAllOperation", "[integrity][verifier]") {
storeTestBlock();
auto report = verifier->verifyAll();
CHECK(report.generatedAt.time_since_epoch().count() >= 0);
CHECK(report.duration.count() >= 0);
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyEmptyHash", "[integrity][verifier]") {
auto result = verifier->verifyBlock("");
CHECK(result.status != VerificationStatus::Passed);
CHECK_FALSE(result.isSuccess());
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "VerifyInvalidHash", "[integrity][verifier]") {
auto result = verifier->verifyBlock("invalid_hash");
CHECK(result.status != VerificationStatus::Passed);
CHECK_FALSE(result.isSuccess());
}
TEST_CASE_METHOD(IntegrityVerifierFixture, "MoveConstructorAndAssignment",
"[integrity][verifier]") {
VerificationConfig config;
auto verifier1 = std::make_unique<IntegrityVerifier>(*storageEngine, *refCounter, config);
auto verifier2 = std::move(*verifier1);
IntegrityVerifier verifier3(*storageEngine, *refCounter, config);
verifier3 = std::move(verifier2);
CHECK_FALSE(verifier3.isRunning());
}
// Regression: the pool used to set its stop flag without holding the queue mutex, so a worker
// between the wait predicate check and blocking could miss the shutdown notify and join()
// hung forever (seen as sporadic integrity_catch2 timeouts under parallel load). Churn
// construction/destruction so a reintroduced lost wakeup shows up as a hang/timeout here.
TEST_CASE_METHOD(IntegrityVerifierFixture, "Verifier shutdown does not lose the stop wakeup",
"[integrity][verifier][shutdown]") {
VerificationConfig config;
config.maxConcurrentVerifications = 8;
for (int i = 0; i < 400; ++i) {
IntegrityVerifier churn(*storageEngine, *refCounter, config);
CHECK_FALSE(churn.isRunning());
}
}