yams/tests
Trevon 7076e4b283
feat(daemon): shutdown hardening + test helper linkage fixes (#44)
* perf(daemon): coalesce metadata writes

* refactor(metadata): split repository ops

* fix(metadata): harden repository invariants

* feat: test coverage and continued refactor work in metadata code

* fix(graph): handle path-like explore queries

- prefer file path symbol lookup for graph explore\n- sanitize alias FTS queries before MATCH

* fix(daemon): quiesce shutdown background work

- Interrupt pending DB acquires before worker shutdown
- Wait for tracked background coroutines and timers to drain

* fix(daemon): cancel repair work on shutdown

Blocked embed and post-ingest queues could keep salvage and\nstuck-doc repair alive during shutdown. Consume cancellation in\nrepair retry loops and lock the integrated ServiceManager path\nwith regressions so shutdown fails fast instead of drifting.

* perf(metadata): reduce sqlite lock contention

* perf(metadata): flatten batch content retries

* fix(daemon): align shutdown wait budgets

A low shutdown timeout override could expire before ServiceManager\nfinished its 5s+30s WorkCoordinator join path. Share the join\nbudget constants, clamp graceful lifecycle waits to that floor,\nand lock the behavior with a daemon regression.

* perf(metadata): bound feedback write waits

* perf(metadata): prep batch content before tx

* fix(daemon): honor no-embeddings tasks

Post-ingest queue tasks dropped the per-task noEmbeddings flag,\nso focused extraction tests still emitted embed jobs and Windows\nCI exercised extra async paths. Preserve the flag through the bus,\nskip embed dispatch when requested, and wake the extraction poller\nimmediately after successful queueing.

* perf(daemon): batch fallback metadata writes

* feat(perf,docs): optimizations for metadata writes, prompt updates and test improvements

* perf(metadata): flatten insert retries

* fix(metadata): clear CI portability blockers

Use the shared portable ScopedEnvVar in metadata search-query helper tests, add mutators for test reuse, and normalize Statement::bindAll integral dispatch so long long arguments bind consistently across platforms. Add focused regression coverage for the bindAll long long path.

* perf(metadata): batch writes and steady bench

* fix(daemon): skip inactive post-ingest pollers

* perf(metadata): tighten tag and path caches

* perf(metadata): cache small batch upserts

* fix(daemon): harden embedded host teardown

* feat(docs): updating docs policies and language

* perf(metadata): tune doc update retries

* perf(metadata): bound download retries

* perf(metadata): bound download metadata waits

* bench(metadata): scope post-ingest content path

* fix(daemon): export embedded test hooks

* fix(daemon): harden test helper linkage

* fix(build): add missing standard includes
2026-06-08 16:29:44 -04:00
..
acceptance PBI-043: Integrate tree_diff benchmarks into nightly CI (Task 043-16) 2025-10-07 10:52:11 -04:00
benchmarks feat(daemon): shutdown hardening + test helper linkage fixes (#44) 2026-06-08 16:29:44 -04:00
common feat(daemon): shutdown hardening + test helper linkage fixes (#44) 2026-06-08 16:29:44 -04:00
fixtures improvements for v0.7.10 2025-12-11 22:07:22 -05:00
integration Assertion hardening, metadata correctness, & daemon lifecycle fixes (#41) 2026-06-04 12:53:42 -04:00
manual feat: test cleanups 2026-03-23 19:43:09 -04:00
mobile updating boost variable exposure 2026-05-03 17:03:34 -04:00
plugins Stabilize sanitizer lanes and daemon concurrency (#40) 2026-06-01 12:14:43 -04:00
scripts chore: stabilize CI, faiss, metadata, docker, and simeon (#25) 2026-05-07 21:07:59 -04:00
sdk improvements for v0.7.10 2025-12-11 22:07:22 -05:00
stress build(ci,tests): cleaning build system and updates related to ci test failures and compat 2026-04-25 14:48:44 -04:00
support fix(tests): stabilize debug suite 2026-05-21 14:08:46 -04:00
unit feat(daemon): shutdown hardening + test helper linkage fixes (#44) 2026-06-08 16:29:44 -04:00
.clang-tidy feat: search improvements, UBSan meson option, CI quality job, and fix clang-tidy findings 2026-04-06 23:11:03 -04:00
CATCH2_PATTERNS.md fix(docs,ci): addressing breaking changes in zig v0.16.x in Dockerfile, docs update and code cleanup 2026-04-25 11:27:18 -04:00
meson.build feat(daemon): shutdown hardening + test helper linkage fixes (#44) 2026-06-08 16:29:44 -04:00
README.md fix(docs,ci): addressing breaking changes in zig v0.16.x in Dockerfile, docs update and code cleanup 2026-04-25 11:27:18 -04:00

YAMS Test Suite Documentation

Overview

The YAMS test suite provides comprehensive testing coverage including unit tests, integration tests, performance benchmarks, and stress tests. This document explains how to write, run, and maintain tests for YAMS.

Quick Start

Running Tests (Meson)

# Configure with tests enabled
conan install . -of build/debug -s build_type=Debug -b missing
meson setup build/debug --native-file build/debug/build/Debug/generators/conan_meson_native.ini --buildtype=debug -Dbuild-tests=true
meson compile -C build/debug

# Run suites
meson test -C build/debug                 # all
meson test -C build/debug -t unit         # unit only
meson test -C build/debug -t integration  # integration only
meson test -C build/debug -t stress       # stress (long-running)

# List tests
meson test -C build/debug -l

Integration timeouts and stress iters (Meson options)

You can tune integration-suite timeouts without editing sources by passing Meson options at setup/configure time:

# Examples (defaults: services=900s, ui=600s, smoke=300s)
meson configure build/debug -Dintegration-timeout=900 -Dintegration-ui-timeout=600 -Dintegration-smoke-timeout=300 -Dstress-iters=100

# Or set at setup:
meson setup build/debug --buildtype=debug -Dbuild-tests=true \
  -Dintegration-timeout=900 -Dintegration-ui-timeout=600 -Dintegration-smoke-timeout=300 -Dstress-iters=100

Profiles via native files (no scripts)

You can also use Meson native files to version preset profiles:

# quicker local runs
meson setup build/fast --buildtype=debug -Dbuild-tests=true --native-file meson/profiles/ci-fast.ini
meson test -C build/fast integration_services -t 1

# nightly soak
meson setup build/soak --buildtype=debug -Dbuild-tests=true --native-file meson/profiles/nightly-soak.ini
meson test -C build/soak integration_services

Services Integration Suite (isolated)

The Services suite exercises add/list/get/grep via a live daemon in a temporary sandbox.

# Run only the services integration executable (isolated; serial)
meson test -C build/debug integration_services -v

# Notes
# - Requires AF_UNIX socket availability (macOS/Linux). If your environment forbids AF_UNIX
#   binds, affected tests will SKIP with an explanatory message.
# - Logs: build/debug/meson-logs/testlog.txt

UI/CLI Integration (services layer)

We mirror CLI expectations at the services layer to keep tests fast and deterministic (no external shell). The custom shard integration_ui runs these checks serially against a temporary daemon sandbox.

# Run only the UI/CLI expectations shard
meson test -C build/debug integration_ui -v

# What it includes
# - UiCliExpectationsIT.* (search/list/get structure and options)
# - GrepServiceExpectationsIT.* (regex pathsOnly, count mode, negatives)

# Notes
# - Tests bring up their own daemon with mock embeddings
# - AF_UNIX is required; tests skip with a helpful message if unavailable

Legacy CMake/CTest (for reference)

These commands are retained for contributors still using the legacy build. Prefer Meson going forward.

cmake -B build -DBUILD_TESTS=ON -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest -V

Running Benchmarks

The benchmark suite is now consolidated into a few executables (api_benchmarks, core_benchmarks, search_benchmarks).

# Run all benchmarks
cd build/tests/benchmarks
./run_all_benchmarks

# Run a specific benchmark suite
./core_benchmarks

# Pass arguments to the benchmark runner
./api_benchmarks --iterations 100 --output my_results.json

Test Organization

tests/
├── common/                 # Shared test utilities
│   ├── test_helpers.h     # Basic test helpers
│   ├── test_data_generator.h  # Test data generation
│   ├── fixture_manager.h  # Test fixture management
│   └── benchmark_tracker.h # Benchmark tracking
├── unit/                   # Unit tests for each component
│   ├── crypto/
│   ├── storage/
│   └── ...
├── integration/           # End-to-end and multi-component tests
│   ├── daemon/
│   └── ...
├── benchmarks/           # Performance benchmarks
│   ├── api_benchmarks.cpp
│   ├── core_benchmarks.cpp
│   └── search_benchmarks.cpp
├── stress/              # Stress tests
├── data/                # Test data files
├── fixtures/            # Test fixtures
└── scripts/             # Test automation scripts

Writing Tests

Unit Tests

Unit tests verify individual components in isolation. Place unit tests in tests/unit/<module>/. All tests use Catch2 v3; see tests/CATCH2_PATTERNS.md for canonical patterns.

#include <catch2/catch_test_macros.hpp>
#include <yams/extraction/pdf_extractor.h>
#include "tests/common/fixture_manager.h"

using namespace yams;
using namespace yams::test;

namespace {
struct PDFExtractorFixture {
    PDFExtractorFixture() { extractor_ = std::make_unique<extraction::PDFExtractor>(); }
    std::unique_ptr<extraction::PDFExtractor> extractor_;
};
} // namespace

TEST_CASE_METHOD(PDFExtractorFixture, "ExtractTextFromSimplePDF", "[pdf]") {
    auto fixture = FixtureManager::getSimplePDF();
    auto result = extractor_->extract(fixture.path);
    REQUIRE(result.has_value());
    CHECK_FALSE(result->text.empty());
    CHECK(result->metadata["pages"] == "1");
}

TEST_CASE_METHOD(PDFExtractorFixture, "HandleCorruptedPDF", "[pdf]") {
    auto fixture = FixtureManager::getCorruptedPDF();
    auto result = extractor_->extract(fixture.path);
    REQUIRE_FALSE(result.has_value());
    CHECK(result.error().code == ErrorCode::InvalidFormat);
}

Integration Tests

Integration tests verify multiple components working together. Place in tests/integration/.

#include <catch2/catch_test_macros.hpp>
#include "tests/common/test_helpers_catch2.h"

namespace {
struct DocumentLifecycleFixture {
    DocumentLifecycleFixture() { initializeYAMS(testDir); }
    std::filesystem::path testDir = yams::test::make_temp_dir("yams_doc_lifecycle");
};
} // namespace

TEST_CASE_METHOD(DocumentLifecycleFixture, "CompleteDocumentWorkflow", "[integration]") {
    auto addResult = runCommand("yams add test.txt --tags test");
    REQUIRE(addResult.isOk());

    std::string hash = extractHash(addResult.value());

    auto searchResult = runCommand("yams search test");
    REQUIRE(searchResult.value().contains(hash));

    auto updateResult = runCommand(
        "yams update --hash " + hash + " --metadata status=processed");
    REQUIRE(updateResult.isOk());

    auto getResult = runCommand("yams get " + hash);
    CHECK(getResult.value() == readFile("test.txt"));
}

Performance Benchmarks

Benchmarks use a custom BenchmarkBase class for consistency. Place new benchmarks in the appropriate consolidated file (core_benchmarks.cpp, search_benchmarks.cpp, etc.).

#include "benchmark_base.h"
#include <yams/crypto/hasher.h>

using namespace yams::benchmark;

// Define a benchmark class that inherits from a suitable base or BenchmarkBase itself
class HashingBenchmark : public BenchmarkBase {
public:
    HashingBenchmark(const std::string& name, size_t dataSize) 
        : BenchmarkBase("Hashing_" + name), dataSize_(dataSize) { 
        // Setup code here
        hasher_ = yams::crypto::createSHA256Hasher();
        data_.resize(dataSize_);
    }

protected:
    // The core work of the benchmark goes here
    size_t runIteration() override {
        hasher_->hash(data_);
        return 1; // Number of operations in this iteration
    }

private:
    std::unique_ptr<yams::crypto::IContentHasher> hasher_;
    std::vector<std::byte> data_;
    size_t dataSize_;
};

// In main(), instantiate and run the benchmark
int main() {
    BenchmarkBase::Config config;
    // ... configure ...
    auto bench = std::make_unique<HashingBenchmark>("SHA256_1MB", 1024 * 1024);
    bench->run();
    return 0;
}

Test Data Management

Using Test Data Generator

#include "tests/common/test_data_generator.h"

TestDataGenerator generator;

// Generate text document
std::string text = generator.generateTextDocument(1024);  // 1KB

// Generate PDF
auto pdfData = generator.generatePDF(5);  // 5 pages

// Generate corpus
auto corpus = generator.generateCorpus(100, 4096, 0.1);  // 100 docs, 4KB avg, 10% dups

// Generate metadata
auto metadata = generator.generateMetadata(10, {"string", "number", "boolean"});

Using Fixture Manager

#include "tests/common/fixture_manager.h"
#include "tests/common/test_data_generator.h"

using yams::test::FixtureManager;
using yams::test::TestDataGenerator;

// Get predefined fixtures
auto simplePDF = FixtureManager::getSimplePDF();
auto complexPDF = FixtureManager::getComplexPDF();
auto largeText = FixtureManager::getLargeTextFile();

// Create custom fixtures rooted in a temporary directory
FixtureManager manager;
TestDataGenerator generator;
auto textFixture = manager.createTextFixture("custom.txt", "content");
auto binaryFixture = manager.createBinaryFixture("data.bin", generator.generateRandomBytes(512));

// Create a synthetic corpus for search/metadata tests
auto fixtures = manager.createCorpus(50, 1024);  // 50 docs, 1KB each

Search Corpus Presets

Search-heavy suites (daemon search, CLI regressions, benchmarking) reuse a deterministic preset corpus so they can share documents, tags, and query workloads.

#include "tests/common/fixture_manager.h"
#include "tests/common/search_corpus_presets.h"

yams::test::FixtureManager fixtures(tmpDir);
auto spec = yams::test::defaultSearchCorpusSpec();
spec.commonTags.push_back("regression");
auto corpus = fixtures.createSearchCorpus(spec);

yams::app::services::DocumentIngestionService ingiter;
yams::app::services::AddOptions opts;
opts.socketPath = sock;
opts.explicitDataDir = storageDir;
opts.path = (fixtures.root() / spec.rootDirectory).string();
opts.recursive = true;
opts.noEmbeddings = false; // ensure vectors are generated
auto addRes = ingiter.addViaDaemon(opts);
ASSERT_TRUE(addRes);

createSearchCorpus returns both a flat list (corpus.fixtures) and a topic map (corpus.fixturesByTopic) so callers can pick targeted subsets (for example topic:python). All artifacts live under FixtureManager::root() and are removed when the manager is destroyed.

Tip: Integration smoke tests (daemon ingestion) and metadata-heavy unit suites (search service) are wired to FixtureManager; prefer extending those helpers over ad-hoc std::ofstream usage when adding new file-backed scenarios.

Code Coverage

Generating Coverage Reports

# Build with coverage enabled
cmake -B build -DENABLE_COVERAGE=ON
cmake --build build

# Run tests
cd build
ctest

# Generate coverage report
make coverage

# View HTML report
open coverage_report/index.html

# Generate LCOV report
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage_html

Coverage Goals

  • Unit tests: >80% line coverage
  • Integration tests: >70% scenario coverage
  • New features: Must include tests before merge
  • Critical paths: 100% coverage required

Continuous Integration

GitHub Actions Workflow

The CI pipeline runs on every push and pull request:

  1. Build Matrix: Tests on Ubuntu, macOS, Windows
  2. Test Execution: All test categories
  3. Coverage Upload: Results sent to Codecov
  4. Benchmark Regression: Compares with baseline
  5. Report Generation: Creates test artifacts

Pre-commit Hooks

Install pre-commit hooks to run tests locally:

# Install hook
cp tests/scripts/pre-commit.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

# Hook runs:
# - Quick unit tests
# - Code coverage check
# - Benchmark regression check

Best Practices

General Guidelines

  1. Test Naming: Use descriptive names that explain what is being tested

    • Good: ExtractTextFromMultiPagePDF
    • Bad: Test1
  2. Test Independence: Tests should not depend on execution order

    • Use SetUp() and TearDown() for initialization/cleanup
    • Don't share state between tests
  3. Assertions: Use appropriate Catch2 macros

    • REQUIRE(...): Fatal failures that abort the test
    • CHECK(...): Non-fatal failures that continue the test
    • SKIP("reason"): Skip a test case at runtime
    • Filter by tag: ./test_binary "[tag]" (replaces --gtest_filter)
  4. Test Data: Use generators and fixtures instead of hardcoded data

    • Ensures reproducibility
    • Simplifies maintenance
  5. Error Testing: Always test error conditions

    • Invalid input
    • Resource failures
    • Boundary conditions

Performance Testing

  1. Benchmark Isolation: Minimize external factors

    • Use benchmark::DoNotOptimize() to prevent optimization
    • Run multiple iterations for stability
  2. Regression Detection: Set appropriate thresholds

    • Time-based: 10% increase is regression
    • Throughput: 10% decrease is regression
  3. Environment Recording: Capture system information

    • Platform, compiler, build type
    • Hardware specifications
    • Resource availability

Integration Testing

  1. Realistic Scenarios: Test actual user workflows

    • Complete operations end-to-end
    • Include error recovery paths
  2. Concurrency Testing: Verify thread safety

    • Multiple readers/writers
    • Race condition detection
    • Deadlock prevention
  3. Data Integrity: Verify consistency

    • Transaction atomicity
    • Rollback behavior
    • Data persistence

Troubleshooting

Common Issues

Tests fail with "fixture not found"

  • Ensure test data directory exists
  • Check file permissions
  • Verify fixture manager initialization

Benchmarks show high variance

  • Increase iteration count
  • Close other applications
  • Use performance CPU governor
  • Run in release mode

Coverage reports missing files

  • Check CMake coverage flags
  • Ensure gcov/llvm-cov installed
  • Verify source file paths

Integration tests timeout

  • Check for deadlocks
  • Increase timeout limits
  • Verify resource cleanup

Debug Techniques

# Run single test case by tag (Catch2 filter)
./build/debug/tests/<test_binary> "[pdf]"

# Run under gdb and break inside a Catch2 TEST_CASE
gdb ./build/debug/tests/<test_binary>
(gdb) break Catch::RunContext::runCurrentTest
(gdb) run "[pdf]"

# Verbose test output
meson test -C build/debug -v --print-errorlogs

# Run with sanitizers (configure once)
meson setup build/asan --buildtype=debug -Dbuild-tests=true -Db_sanitize=address
meson test -C build/asan

Contributing Tests

When adding new features:

  1. Write tests first (TDD approach)
  2. Cover happy path and error cases
  3. Add benchmarks for performance-critical code
  4. Document test purpose in comments
  5. Update this README if adding new patterns

Checklist for New Tests

  • Unit tests for new components
  • Integration tests for workflows
  • Benchmarks for performance-critical paths
  • Error handling tests
  • Documentation in test files
  • Coverage meets requirements
  • CI passes on all platforms

Performance Baselines

Current performance baselines (as of v0.0.7):

Operation Baseline Unit Notes
SHA256 (1MB) 3.5 ms Single-threaded
PDF Extract (10 pages) 50 ms With PDFium
Search (10K docs) 25 ms Full-text search
Metadata Update 2 ms Single field
Document Ingestion 100 docs/s 1KB documents

Contact

For test-related questions:

  • Check existing test examples
  • Consult this documentation
  • Review CI logs for patterns
  • Open an issue with test label