* feat: search work and moving to experimental
* WIP
* feat(search): diagnostic logging pipeline — graph build, concepts, reranker gate
- New [concepts] per-50-query log showing extracted query concepts, type
distribution, and used_gliner flag.
- New [PIQ-entity] per-1000-dispatch + per-document completion log with
entity type counts (e.g., 'protein=3, gene=1, chemical=2').
- New [graph_rerank-gate] one-shot log showing why enableGraphRerank
is zeroed: shows shortQueryBudgeted, corpusWarming, bypassWarmingGate,
lexicalReady, and post-ingest inflight.
- New [graph_rerank] per-50-query log with window size, matched
candidates, applied count, concept count, and elapsed time.
- retrieval_quality_bench.cpp: add YAMS_SEARCH_BYPASS_CORPUS_WARMING_GATE=1
so benchmark queries are not gated by freshness checks.
- resolve merge conflicts from experimental branch.
* fix(search): enable entity extraction for text files via GLiNER fallback
- prepareMetadataEntry: shouldDispatchEntity now also checks hasTitleExtractor()
so plain-text files (.txt, .md) get entity extraction even when no binary
entity provider supports their extension.
- processEntityExtractionStage: when no ExternalEntityProvider matches,
fall back to processTitleExtractionStage which runs GLiNER on text.
- refreshStageAvailability: entity stage is now considered capable when
hasTitleExtractor() is true, so the entity poller gets concurrency budget
even if entityProviders_ is empty.
- kCombinedEntityTypes: add biomedical entity types (protein, gene, cell,
disease, chemical, drug, pathway, biological_process, biomarker, anatomy,
organism) so GLiNER extracts high-value biomedical entities instead of
only generic NL types (person, organization, concept).
* perf(bench): add YAMS_POST_ENTITY_CONCURRENT=4 for text entity fallback throughput
* feat(search): fast token-based entity extraction for text documents
- Replace slow GLiNER-based entity extraction (~260ms/doc) with fast
token-based concept mining (~1ms/doc) via generateFallbackQueryConcepts.
Uses sub-phrase mining + IDF-weighted token salience — the same function
already used for query-time concept fallback.
- Store entities with full KG scaffolding: KGNode with entity type/label,
KGAlias for query-time resolution, DeferredEdge from entity→document,
and DeferredDocEntity linking entity to document for getDocEntitiesForDocument.
- Add biomedical entity types to kCombinedEntityTypes in title extraction
(protein, gene, cell, disease, chemical, drug, pathway, etc.) so GLiNER
can output high-value biomedical entities when used.
- Add include for query_expansion.h to access generateFallbackQueryConcepts.
- bench: add YAMS_POST_ENTITY_CONCURRENT=4 for entity poller throughput.
Benchmark: 22K+ entities extracted across 5183 docs, ~41% high-value
biomedical. MRR 0.4281 (beats 0.4273 baseline by +0.2%).
* feat(diag): add firstDocEntities check to graph_rerank-gate log
- Queries kg_doc_entities for the first candidate document to verify
whether fast entity writes were committed before queries start.
- Confirmed: firstDocEntities=0 — entities stuck in WriteCoordinator
async queue when benchmark queries begin.
* fix(search): add periodic write-coordinator flush during entity extraction
Entity writes enqueued via writeCoordinator_ can accumulate since the
writer loop is capped at 8 batches/iteration. For 5K+ documents this
creates a backlog that outlasts the post-ingest drain wait — entities
are not committed to kg_doc_entities before benchmark queries start.
Add a periodic flush (every 128 batches, 500ms timeout) in the entity
extraction batch processor to drain the WC queue incrementally.
Diagnostic confirms firstDocEntities=0 persists — a synchronous
committed flush or batched WC enqueue is needed.
* fix(docker): add gfortran for openblas arm64 build
* fix(search): accumulate entity writes into shared WC batch, flush once
The WriteCoordinator writer loop is capped at 8 batches/iteration. With
5K+ per-doc entity writes, entities outlast the benchmark's post-ingest
drain and firstDocEntities=0 when the graph scorer runs.
- processEntityExtractionStage: add optional DeferredKGBatch* parameter.
Text-fallback appends entity nodes/aliases/edges/doc-entities to the
shared batch instead of creating per-doc WC batches.
- processEntityExtractionBatch: creates one DeferredKGBatch, passes it to
all stage calls in the batch, enqueues to WC once at end.
- Single-shot WC flush: after all stages go idle (totalInFlight==0),
calls writeCoordinator_->flush(15s) to drain remaining entity writes
before benchmark queries start.
- DeferredKGBatch forward-declared in PostIngestQueue header.
* ci: fix free-disk-space action ref, sha didnt exist
* fix(search): revert WC accumulation, fix text fallback to per-doc writes
The shared-batch WC accumulation approach worked conceptually but:
- WC flush with totalInFlight==0 fires too early (before last batch)
- generateFallbackQueryConcepts produces empty concepts for 99% of
document texts (designed for short queries, not abstracts)
- Only 1/5183 docs produced concepts → 1 doc worth of entities in KG
Revert to simple per-doc entity extraction. The enable=/disable=
graph reranker gate is still controlled by shortQueryBudgeted=1
which zeroes the graph reranker on the first query (intent=Code).
firstDocEntities=0 persists — entities need a different creation
strategy for document text (phrase mining too conservative).
* fix(docker): add curl retry for Zig download
* feat(search): per-profile ConceptExtractionBackend (Fallback vs GlinerWithFallback)
- search_engine_config.h: new ConceptExtractionBackend enum with Fallback
(sub-phrase mining + IDF tokens, <1ms) and GlinerWithFallback (GLiNER
first, fall back on empty/timeout). Default: GlinerWithFallback.
- search_tuner.h: SCIENTIFIC profile sets Fallback (GLiNER returns empty
for 99% of short biomedical claims). Other profiles keep default.
Wired through TunedParams.applyTo().
- search_engine.cpp: gate GLiNER concept extraction on the config backend.
When Fallback, conceptFuture is never created and concepts come directly
from generateFallbackQueryConcepts. Added [concept-backend] diagnostic.
- search_engine_builder.cpp: builder log now includes concept_backend
in the override-state startup message.
- search_tuner_catch2_test.cpp: verify SCIENTIFIC profile uses Fallback.
Note: builder correctly sets Fallback but query-time workingConfig still
shows GlinerWithFallback (int=1). This is a config-sync issue between
builder-time profile application and runtime config snapshot — the
tuner or a post-override config copy may reset the field. To be fixed.
* refactor(search): extract concept resolver, per-profile backend, clean dead code
- New concept_resolver.h/cpp: extract enrichWithFallbackConcepts from the
search engine monolith into a testable unit. Generates IDF-weighted
token concepts via generateFallbackQueryConcepts, deduplicates, and
appends to existing concepts.
- search_engine_config.h: new ConceptExtractionBackend enum (Fallback vs
GlinerWithFallback). SCIENTIFIC profile sets Fallback since GLiNER
extracts 0 concepts for 99% of short biomedical claims.
- search_tuner.h: wire conceptExtractionBackend through TunedParams,
applyTo, seedTunedParamsFromConfig (the one-line fix that resolved
the config-sync bug where profile Fallback was overridden at query
policy resolution time).
- search_engine.cpp: materializeConcepts lambda restructured to always
reach the fallback even when GLiNER future is invalid. Removed
~80 lines of diagnostic bloat ([concept-backend], [workingConfig-*],
per-50 [concepts]). GLiNER gate checks config backend.
- PostIngestQueue.cpp: removed dead fast-token entity extraction
(~150 lines of concept generation + KG scaffolding that produced
0 entities for 99% of documents). Reverted entity-stage gating
(entityCapable || hasTitleExtractor) and shouldDispatchEntity to
extension-only checks. Removed DeferredKGBatch forward decl.
- gliner_query_extractor: removed unused createGlinerExtractionFunc-
FromProvider fallback factory (~150 lines).
- ServiceManager.cpp: removed fallback call site for removed factory.
- builder log: removed concept_backend diagnostic line noise.
- search_tuner_catch2_test.cpp: verify SCIENTIFIC uses Fallback.
Net: ~400 lines removed, ~120 lines added (concept_resolver unit +
config wiring). GLiNER concept extraction savings: ~30-80ms/query
× 300 queries = 9-24s total for SCIENTIFIC profiles.
* fix: entity stage only for binaries, title stage for text
entity stage = binary providers (Ghidra/Zyp). title stage = GLiNER for all
text. No overlap — title already stores NL entities to KG.
* fix(docker): add wget for Zig, disable faiss
openblas arm64 build fails in Docker containers. YAMS_DISABLE_FAISS=1
env var skips faiss (and its openblas transitive dep). Zig download
now uses wget -c for resume on flaky ziglang.org connections.
* ci: add experimental branch to tests trigger
* ci: pin free-disk-space to v1.3.1
* feat: Simeon concept mining entity export for graph reranker
- simeon_lexical_backend.h: EntityCallback in Config. Called with
(docId, entityText, confidence) per concept match per doc.
- simeon_lexical_backend.cpp: after mine_concepts, iterate docs
via collect_doc_concepts + entity_callback.
- SearchEngineManager.cpp: enable concept_mining_enabled, wire
entity_callback batch-inserting DocEntity via KG store. Fix
kgStore capture-before-move race.
Note: simeon submodule has collect_doc_concepts in concept_mining.hpp
+ concept_mining.cpp (must commit to submodule separately).
* ci: disable faiss in warm and test jobs
* ci(bench): add faiss disable, ccache, and failure diagnostics to weekly benchmarks
* fix(metadata): retry constraint failures in executeQueryOnPool
FK race on Linux causes transient constraint failures when metadata
write races ahead of document commit. Previously treated as terminal.
Now retried with backoff like lock errors.
* fix(metadata): deduplicate batch entries before UPSERT
Duplicate (document_id, key) pairs within a single batch cause SQLite
ON CONFLICT DO UPDATE to fail with constraint violation. Dedup keeps
the last write per key.
* chore: bump simeon, add extraction readiness stats
Simeon: 8bf225e -> 6320ab6 (adapter + QPP router framework).
CorpusStats: add contentExtracted*, ftsIndexed*, titleCoverage,
languageCoverage, and KG topology richness fields.
* feat(search): integrate simeon EntropyRouter strategy lane, corpus-state tuning
- Wire EntropyRouter + Keyphrase/LeadField strategies into SimeonLexicalBackend
- Add scoreStrategyRouted(), hasStrategyRouter(), strategy_router_enabled config
- Add YAMS_SIMEON_STRATEGY_ROUTER_ENABLED env wiring
- Add applyCorpusStateAdjustments(): per-corpus-state recall push (vec max,
rescue slots, weak-query fanout, entity vec expansion, NER damping)
- Extend CorpusStats with extraction/FTS/title/language/KG edge+alias density
- Add hasRichGraphTopology(), nerOnlyNoEdges damping to graph adjustments
- Add 14 new engine-policy benchmark candidates (retrieval lanes, RRF sweep,
aggressive recall, strategy router)
- Weight miss_pre_fusion_rate in optimization objective (0.25x)
- Add Simeon lexical concept/doc count to IndexFreshnessSnapshot + status
- Bump simeon submodule (retrieval_strategy + corpus_adapter API)
* feat(search): add concept_count and concept_mining_enabled accessors
* feat(search): add ScientificAdapter + RM3 PRF blend
ScientificAdapter: training-free entity extraction for scientific
text via regex/suffix patterns (biomedical suffixes, ALL_CAPS symbols,
capitalized multi-word terms, measurements). Replaces TextAdapter
in strategy router build, providing entity lists for KeyphraseStrategy.
RM3: add score_with_prf blend path to scoreStrategyRouted at 0.30
weight when rm3_enabled=true. New benchmark candidate
scientific_strategy_rm3.
Strategy router + ScientificAdapter: mrr=0.4514 ndcg=0.4789 recall=0.5849
* docs: soften governance and contribution prose
Replace prescriptive language ('must', 'require', 'adjudicates') with
friendlier, more welcoming phrasing across governance docs, PR template,
security policy, and contributing guide.
* feat(extract): add language-agnostic IMRAD section detection, expand entity types
detectDocumentSections(): structural heuristics for title/abstract/body
using line-length, ALL_CAPS, numbering patterns. No word matching.
Works across all languages and scripts.
deriveTitle(): add IMRAD detection step before first-line fallback.
isHighValueGraphType(): use ScientificAdapter suffix detection (40+
biomedical suffixes) instead of hardcoded 11 types.
* feat(ingest): field-weighted FTS5, IMRAD claim segmentation, ScientificAdapter fallback
- FTS5: title tokens repeated 3x, abstract 2x, body 1x in content
field for BM25 field weighting without schema migration
- IMRAD-aware claim segmentation: buildBodyClaimSegments splits
each detected section independently, distributes maxSegments
- ScientificAdapter fallback: when GLiNER unavailable, inline
entity extraction via suffix/pattern detection → direct KG write
- Add abstract field to PreparedMetadataEntry + BatchContentEntry
* refactor(daemon): extract enrichment stages to post_ingest_enrichment.cpp
Split PostIngestQueue.cpp (3145 lines) into:
- PostIngestQueue.cpp (~3084 lines): coordinator, lifecycle, queue
management, extraction pipeline, gradient limiters
- post_ingest_enrichment.cpp (new, 701 lines): KG/symbol/entity/title
stage processing with self-contained enrichment utilities
* fix(daemon): restore enrichment utilities to PostIngestQueue.cpp
Processing methods still in PostIngestQueue.cpp reference entity/NL
utilities (isUsefulNlEntity, buildBodyClaimSegments, etc.) that were
removed during decomposition. Restore them to anonymous namespace.
* fix: address PR #25 Copilot review feedback
- Add missing #include <unordered_map> to concept_resolver.cpp
- Batch entity writes per doc in SearchEngineManager (reduces write amp)
- Remove non-existent CorpusAdapter API claims from README
- Replace absolute paths with repo-relative in daemon-download-jobs.md
- Revert plugin-zyp default to false (noisy warnings)
- Fix grammar in extraction-repair-pipeline.md heading
- Add gitignore entries for .wrangler, .claude, .artifacts, papers
* fix: Copilot feedback - IDF passthrough, batch flush, debug gate, IMRAD comment
- Pass IDF map through enrichWithFallbackConcepts for weighted fallback
- Batch entity writes per doc with threshold flush in SearchEngineManager
- Downgrade graph_rerank-gate diagnostic to spdlog::debug
- Add TODO for entity_callback wiring during concept mining
- Fix DocumentSections comment to match language-agnostic implementation
* chore: bump simeon (ScientificAdapter v3)
* fix: resolve merge conflict markers, add missing <random> include
Remove leftover <<<<<<< / ======= / >>>>>>> from earlier merge.
Add #include <random> to ingestion_throughput_bench.cpp.
* chore: trigger CI after simeon submodule push
|
||
|---|---|---|
| .. | ||
| acceptance | ||
| benchmarks | ||
| common | ||
| fixtures | ||
| integration | ||
| manual | ||
| mobile | ||
| plugins | ||
| scripts | ||
| sdk | ||
| stress | ||
| support | ||
| unit | ||
| .clang-tidy | ||
| CATCH2_PATTERNS.md | ||
| meson.build | ||
| README.md | ||
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-hocstd::ofstreamusage 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:
- Build Matrix: Tests on Ubuntu, macOS, Windows
- Test Execution: All test categories
- Coverage Upload: Results sent to Codecov
- Benchmark Regression: Compares with baseline
- 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
-
Test Naming: Use descriptive names that explain what is being tested
- Good:
ExtractTextFromMultiPagePDF - Bad:
Test1
- Good:
-
Test Independence: Tests should not depend on execution order
- Use
SetUp()andTearDown()for initialization/cleanup - Don't share state between tests
- Use
-
Assertions: Use appropriate Catch2 macros
REQUIRE(...): Fatal failures that abort the testCHECK(...): Non-fatal failures that continue the testSKIP("reason"): Skip a test case at runtime- Filter by tag:
./test_binary "[tag]"(replaces--gtest_filter)
-
Test Data: Use generators and fixtures instead of hardcoded data
- Ensures reproducibility
- Simplifies maintenance
-
Error Testing: Always test error conditions
- Invalid input
- Resource failures
- Boundary conditions
Performance Testing
-
Benchmark Isolation: Minimize external factors
- Use
benchmark::DoNotOptimize()to prevent optimization - Run multiple iterations for stability
- Use
-
Regression Detection: Set appropriate thresholds
- Time-based: 10% increase is regression
- Throughput: 10% decrease is regression
-
Environment Recording: Capture system information
- Platform, compiler, build type
- Hardware specifications
- Resource availability
Integration Testing
-
Realistic Scenarios: Test actual user workflows
- Complete operations end-to-end
- Include error recovery paths
-
Concurrency Testing: Verify thread safety
- Multiple readers/writers
- Race condition detection
- Deadlock prevention
-
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:
- Write tests first (TDD approach)
- Cover happy path and error cases
- Add benchmarks for performance-critical code
- Document test purpose in comments
- 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