#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/test_helpers_catch2.h" #if defined(_WIN32) && __has_include() #include #define YAMS_ORT_API_VERSION ORT_API_VERSION #else #define YAMS_ORT_API_VERSION 0 #endif #ifdef _WIN32 TEST_CASE("GrepService - Windows disabled due to ONNX runtime instability", "[grep][windows][skip]") { SUCCEED("GrepService tests are disabled on Windows pending ONNX Runtime cleanup fixes; this " "keeps the suite green."); } #else #define SKIP_GREP_ON_WINDOWS() ((void)0) using namespace yams; using namespace yams::app::services; using namespace yams::metadata; using namespace yams::api; namespace { class FlakyMetadataRepository final : public MetadataRepository { public: explicit FlakyMetadataRepository(ConnectionPool& pool) : MetadataRepository(pool) {} void setGetAllMetadataFailures(std::size_t count) { getAllMetadataFailures_ = count; } void setQueryDocumentsFailures(std::size_t count) { queryFailures_ = count; } void setFindDocumentsByTagsFailures(std::size_t count) { findDocumentsByTagsFailures_ = count; } Result> getAllMetadata(int64_t documentId) override { if (yams::test::consume_failure(getAllMetadataFailures_)) { return Error{ErrorCode::NotInitialized, "metadata warming up"}; } return MetadataRepository::getAllMetadata(documentId); } Result> queryDocuments(const metadata::DocumentQueryOptions& options) override { if (yams::test::consume_failure(queryFailures_)) { return Error{ErrorCode::DatabaseError, "database is locked"}; } return MetadataRepository::queryDocuments(options); } Result>> getMetadataForDocuments(std::span documentIds) override { if (yams::test::consume_failure(getAllMetadataFailures_)) { return Error{ErrorCode::NotInitialized, "metadata warming up"}; } return MetadataRepository::getMetadataForDocuments(documentIds); } Result> findDocumentsByTags(const std::vector& tags, bool matchAll = false) override { if (yams::test::consume_failure(findDocumentsByTagsFailures_)) { return Error{ErrorCode::NotInitialized, "tag index warming up"}; } return MetadataRepository::findDocumentsByTags(tags, matchAll); } Result> findDocumentByExactPath(const std::string& path) override { return MetadataRepository::findDocumentByExactPath(path); } private: std::size_t getAllMetadataFailures_{0}; std::size_t queryFailures_{0}; std::size_t findDocumentsByTagsFailures_{0}; }; class RetrieveBytesFailStore final : public IContentStore { public: explicit RetrieveBytesFailStore(std::shared_ptr inner) : inner_(std::move(inner)) {} Result store(const std::filesystem::path& path, const ContentMetadata& metadata, api::ProgressCallback progress) override { return inner_->store(path, metadata, progress); } Result retrieve(const std::string& hash, const std::filesystem::path& outputPath, api::ProgressCallback progress) override { return inner_->retrieve(hash, outputPath, progress); } Result storeStream(std::istream& stream, const ContentMetadata& metadata, api::ProgressCallback progress) override { return inner_->storeStream(stream, metadata, progress); } Result retrieveStream(const std::string& hash, std::ostream& output, api::ProgressCallback progress) override { return inner_->retrieveStream(hash, output, progress); } Result storeBytes(std::span data, const ContentMetadata& metadata) override { return inner_->storeBytes(data, metadata); } Result> retrieveBytes(const std::string&) override { return Error{ErrorCode::NotFound, "retrieveBytes disabled for test"}; } Result> retrieveBytesPrefix(const std::string&, std::size_t) override { return Error{ErrorCode::NotFound, "retrieveBytesPrefix disabled for test"}; } Result retrieveRaw(const std::string& hash) override { return inner_->retrieveRaw(hash); } std::future> retrieveRawAsync(const std::string& hash) override { return inner_->retrieveRawAsync(hash); } Result exists(const std::string& hash) const override { return inner_->exists(hash); } Result remove(const std::string& hash) override { return inner_->remove(hash); } Result getMetadata(const std::string& hash) const override { return inner_->getMetadata(hash); } Result updateMetadata(const std::string& hash, const ContentMetadata& metadata) override { return inner_->updateMetadata(hash, metadata); } std::vector> storeBatch(const std::vector& paths, const std::vector& metadata) override { return inner_->storeBatch(paths, metadata); } std::vector> removeBatch(const std::vector& hashes) override { return inner_->removeBatch(hashes); } ContentStoreStats getStats() const override { return inner_->getStats(); } HealthStatus checkHealth() const override { return inner_->checkHealth(); } Result verify(api::ProgressCallback progress) override { return inner_->verify(progress); } Result compact(api::ProgressCallback progress) override { return inner_->compact(progress); } Result garbageCollect(api::ProgressCallback progress) override { return inner_->garbageCollect(progress); } private: std::shared_ptr inner_; }; struct GrepFixture { GrepFixture() { tmpDir_ = std::filesystem::temp_directory_path() / ("grep_test_" + std::to_string(::getpid())); std::filesystem::create_directories(tmpDir_); db_ = std::make_unique(); auto dbPath = tmpDir_ / "yams.db"; REQUIRE(db_->open(dbPath.string(), ConnectionMode::Create)); pool_ = std::make_unique(dbPath.string(), ConnectionPoolConfig{}); repo_ = std::make_shared(*pool_); KnowledgeGraphStoreConfig kgConfig; kgConfig.enable_wal = false; auto kgResult = makeSqliteKnowledgeGraphStore(*pool_, kgConfig); REQUIRE(kgResult.has_value()); kgStore_ = std::shared_ptr(std::move(kgResult).value()); repo_->setKnowledgeGraphStore(kgStore_); MigrationManager mm(*db_); REQUIRE(mm.initialize()); mm.registerMigrations(YamsMetadataMigrations::getAllMigrations()); REQUIRE(mm.migrate()); ContentStoreBuilder builder; auto storeRes = builder.withStoragePath(tmpDir_ / "storage") .withCompression(false) .withDeduplication(false) .build(); REQUIRE(storeRes); store_ = std::shared_ptr( const_cast&>(storeRes.value()).release()); ctx_.service_manager = nullptr; ctx_.store = store_; ctx_.metadataRepo = repo_; ctx_.searchEngine = nullptr; ctx_.kgStore = kgStore_; grepService_ = makeGrepService(ctx_); } ~GrepFixture() { grepService_.reset(); ctx_.metadataRepo.reset(); ctx_.store.reset(); ctx_.searchEngine.reset(); ctx_.kgStore.reset(); repo_.reset(); kgStore_.reset(); pool_.reset(); db_.reset(); store_.reset(); std::filesystem::remove_all(tmpDir_); } void addDocument(std::string_view name, std::string_view content) { auto path = tmpDir_ / name; std::filesystem::create_directories(path.parent_path()); std::ofstream{path} << content; auto docService = makeDocumentService(ctx_); StoreDocumentRequest req; req.path = path.string(); REQUIRE(docService->store(req)); } void addHotContent(std::string_view name, std::string_view content, std::string_view mimeType = "text/plain") { (void)mimeType; auto path = (tmpDir_ / name).string(); auto docRes = repo_->findDocumentByExactPath(path); REQUIRE(docRes.has_value()); REQUIRE(docRes.value().has_value()); DocumentContent docContent; docContent.documentId = docRes.value()->id; docContent.contentText = std::string(content); docContent.contentLength = static_cast(docContent.contentText.size()); docContent.extractionMethod = "test"; docContent.language = "en"; REQUIRE(repo_->insertContent(docContent)); } auto grep(const GrepRequest& req) const { return grepService_->grep(req); } void seedRelationsForFile(const std::string& filePath, const std::string& hash) const { REQUIRE(kgStore_ != nullptr); REQUIRE_FALSE(filePath.empty()); REQUIRE_FALSE(hash.empty()); KGNode fileNode; fileNode.nodeKey = "path:file:" + filePath; fileNode.type = std::string("file"); fileNode.label = filePath; auto fileNodeId = kgStore_->upsertNode(fileNode); REQUIRE(fileNodeId.has_value()); KGNode docNode; docNode.nodeKey = "doc:" + hash; docNode.type = std::string("document"); docNode.label = filePath; auto docNodeId = kgStore_->upsertNode(docNode); REQUIRE(docNodeId.has_value()); KGNode symbolNode; symbolNode.nodeKey = "symbol:test:" + hash; symbolNode.type = std::string("symbol"); symbolNode.label = std::string("SeededSymbol"); auto symbolNodeId = kgStore_->upsertNode(symbolNode); REQUIRE(symbolNodeId.has_value()); KGNode moduleNode; moduleNode.nodeKey = "module:test:" + hash; moduleNode.type = std::string("module"); moduleNode.label = std::string("SeededModule"); auto moduleNodeId = kgStore_->upsertNode(moduleNode); REQUIRE(moduleNodeId.has_value()); KGEdge versionEdge; versionEdge.srcNodeId = fileNodeId.value(); versionEdge.dstNodeId = docNodeId.value(); versionEdge.relation = "has_version"; REQUIRE(kgStore_->addEdge(versionEdge).has_value()); KGEdge usesOne; usesOne.srcNodeId = fileNodeId.value(); usesOne.dstNodeId = symbolNodeId.value(); usesOne.relation = "uses"; REQUIRE(kgStore_->addEdge(usesOne).has_value()); KGEdge usesTwo; usesTwo.srcNodeId = fileNodeId.value(); usesTwo.dstNodeId = moduleNodeId.value(); usesTwo.relation = "uses"; REQUIRE(kgStore_->addEdge(usesTwo).has_value()); } std::filesystem::path tmpDir_; std::unique_ptr db_; std::unique_ptr pool_; std::shared_ptr repo_; std::shared_ptr kgStore_; std::shared_ptr store_; AppContext ctx_; std::shared_ptr grepService_; }; } // namespace TEST_CASE("GrepService - Basic Functionality", "[grep][service][basic]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("a.txt", "alpha beta gamma\nhello world\nregex target\n"); fixture.addDocument("b.txt", "semantic related content about programming\n"); SECTION("Regex-only mode finds pattern matches") { GrepRequest req; req.pattern = "regex"; req.regexOnly = true; req.lineNumbers = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().totalMatches > 0); CHECK(res.value().semanticMatches == 0); CHECK(res.value().executionTimeMs >= 0); REQUIRE(res.value().searchStats.contains("metadata_operations")); CHECK(res.value().searchStats.at("metadata_operations") != "0"); REQUIRE(res.value().searchStats.contains("latency_ms")); REQUIRE(res.value().searchStats.contains("match_render_us")); REQUIRE(res.value().searchStats.contains("matches_rendered")); CHECK(std::stoll(res.value().searchStats.at("match_render_us")) >= 0); CHECK(std::stoll(res.value().searchStats.at("matches_rendered")) > 0); } SECTION("Relation metadata is added for matched files") { auto docRes = fixture.repo_->findDocumentByExactPath((fixture.tmpDir_ / "a.txt").string()); REQUIRE(docRes.has_value()); REQUIRE(docRes.value().has_value()); fixture.seedRelationsForFile(docRes.value()->filePath, docRes.value()->sha256Hash); GrepRequest req; req.pattern = "regex"; req.regexOnly = true; auto res = fixture.grep(req); REQUIRE(res); auto it = std::find_if(res.value().results.begin(), res.value().results.end(), [](const GrepFileResult& file) { return file.fileName == "a.txt"; }); REQUIRE(it != res.value().results.end()); REQUIRE(it->metadata.contains("relation_count")); CHECK(it->metadata.at("relation_count") == "3"); REQUIRE(it->metadata.contains("relation_types")); CHECK(it->metadata.at("relation_types").find("uses:2") != std::string::npos); REQUIRE(it->metadata.contains("relation_summary")); CHECK(it->metadata.at("relation_summary").find("uses(2)") != std::string::npos); REQUIRE(it->metadata.contains("primary_relation")); CHECK(it->metadata.at("primary_relation") == "uses"); REQUIRE(res.value().searchStats.contains("relation_results_enriched")); CHECK(res.value().searchStats.at("relation_results_enriched") != "0"); } SECTION("Literal short pattern matches with boundaries") { fixture.addDocument("c.txt", "alpha beta alpha\nalpha-beta\n"); GrepRequest req; req.pattern = "alpha"; req.literalText = true; req.word = true; // Scope grep to only c.txt to avoid counting matches in a.txt req.paths = {(fixture.tmpDir_ / "c.txt").string()}; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().totalMatches == 2); REQUIRE(res.value().searchStats.contains("retrieval_mode")); CHECK(res.value().searchStats.at("retrieval_mode") == "literal"); CHECK(res.value().searchStats.at("fts_literal_filter") == "off"); } SECTION("Hybrid mode includes semantic suggestions") { // Skip semantic search on Windows - ONNX initialization hangs #ifdef _WIN32 SKIP("Semantic search unavailable on Windows - ONNX init issues"); #endif GrepRequest req; req.pattern = "how to learn programming"; req.regexOnly = false; req.semanticLimit = 2; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().semanticMatches >= 0); REQUIRE(res.value().searchStats.contains("retrieval_mode")); CHECK(res.value().searchStats.at("retrieval_mode") == "semantic"); } SECTION("Path filters normalize relative segments") { auto res = fixture.grep({ .pattern = "hello", .paths = {(fixture.tmpDir_ / "./a.txt").string()}, }); REQUIRE(res); REQUIRE_FALSE(res.value().results.empty()); CHECK(res.value().results.front().fileName == "a.txt"); CHECK(res.value().totalMatches > 0); } SECTION("Subpath filters match correctly") { std::filesystem::create_directory(fixture.tmpDir_ / "sub"); fixture.addDocument("sub/c.txt", "subpath content"); auto res = fixture.grep({ .pattern = "subpath", .paths = {"sub/c.txt"}, }); REQUIRE(res); REQUIRE_FALSE(res.value().results.empty()); CHECK(res.value().results.front().fileName == "c.txt"); CHECK(res.value().totalMatches > 0); } } TEST_CASE("GrepService - Output Modes", "[grep][service][modes]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("a.txt", "programming content\n"); SECTION("Paths-only and file-set projections are deterministic") { fixture.addDocument("c.txt", "needle content c\n"); fixture.addDocument("b.txt", "needle content b\n"); fixture.addDocument("z.txt", "haystack only\n"); GrepRequest req; req.pattern = "needle"; req.regexOnly = true; req.pathsOnly = true; auto res = fixture.grep(req); REQUIRE(res); const std::vector expectedWith = { std::filesystem::weakly_canonical(fixture.tmpDir_ / "b.txt").string(), std::filesystem::weakly_canonical(fixture.tmpDir_ / "c.txt").string(), }; CHECK(res.value().filesWith == expectedWith); CHECK(res.value().pathsOnly == expectedWith); GrepRequest withoutReq; withoutReq.pattern = "needle"; withoutReq.regexOnly = true; withoutReq.filesWithoutMatch = true; auto withoutRes = fixture.grep(withoutReq); REQUIRE(withoutRes); const std::vector expectedWithout = { std::filesystem::weakly_canonical(fixture.tmpDir_ / "a.txt").string(), std::filesystem::weakly_canonical(fixture.tmpDir_ / "z.txt").string(), }; CHECK(withoutRes.value().filesWithout == expectedWithout); } SECTION("Count mode allows semantic suggestions") { // Skip semantic search on Windows - ONNX initialization hangs #ifdef _WIN32 SKIP("Semantic search unavailable on Windows - ONNX init issues"); #endif GrepRequest req; req.pattern = "programming"; req.regexOnly = false; req.semanticLimit = 2; req.count = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().semanticMatches >= 0); } SECTION("Files-only mode allows semantic suggestions") { // Skip semantic search on Windows - ONNX initialization hangs #ifdef _WIN32 SKIP("Semantic search unavailable on Windows - ONNX init issues"); #endif GrepRequest req; req.pattern = "programming"; req.regexOnly = false; req.semanticLimit = 2; req.filesWithMatches = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().semanticMatches >= 0); } SECTION("Paths-only mode allows semantic suggestions") { // Skip semantic search on Windows - ONNX initialization hangs #ifdef _WIN32 SKIP("Semantic search unavailable on Windows - ONNX init issues"); #endif GrepRequest req; req.pattern = "programming"; req.regexOnly = false; req.semanticLimit = 2; req.pathsOnly = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().semanticMatches >= 0); } } TEST_CASE("GrepService - Error Handling", "[grep][service][reliability]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("a.txt", "alpha beta gamma\n"); SECTION("Retries transient metadata errors") { auto flakyRepo = std::make_shared(*fixture.pool_); auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%"); REQUIRE(docsRes); REQUIRE_FALSE(docsRes.value().empty()); const auto docId = docsRes.value().front().id; REQUIRE(flakyRepo->setMetadata(docId, "force_cold", MetadataValue("true"))); flakyRepo->setGetAllMetadataFailures(1); fixture.repo_ = flakyRepo; fixture.ctx_.metadataRepo = flakyRepo; fixture.grepService_ = makeGrepService(fixture.ctx_); GrepRequest req; req.pattern = "alpha"; req.literalText = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().totalMatches > 0); } SECTION("Propagates tag query failures after retry exhaustion") { auto flakyRepo = std::make_shared(*fixture.pool_); auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%"); REQUIRE(docsRes); REQUIRE_FALSE(docsRes.value().empty()); const auto docId = docsRes.value().front().id; REQUIRE( flakyRepo->setMetadata(docId, "tag:ready_flag", MetadataValue(std::string("true")))); flakyRepo->setFindDocumentsByTagsFailures(8); fixture.repo_ = flakyRepo; fixture.ctx_.metadataRepo = flakyRepo; fixture.grepService_ = makeGrepService(fixture.ctx_); auto res = fixture.grep({ .pattern = "alpha", .literalText = true, .tags = {"ready_flag"}, }); REQUIRE_FALSE(res); CHECK(res.error().code == ErrorCode::NotInitialized); CHECK_FALSE(res.error().message.empty()); } SECTION("Tag-filtered grep tolerates metadata warming when tag index query succeeds") { auto flakyRepo = std::make_shared(*fixture.pool_); auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%"); REQUIRE(docsRes); REQUIRE_FALSE(docsRes.value().empty()); const auto docId = docsRes.value().front().id; REQUIRE( flakyRepo->setMetadata(docId, "tag:ready_flag", MetadataValue(std::string("true")))); flakyRepo->setGetAllMetadataFailures(8); fixture.repo_ = flakyRepo; fixture.ctx_.metadataRepo = flakyRepo; fixture.grepService_ = makeGrepService(fixture.ctx_); auto res = fixture.grep({ .pattern = "alpha", .literalText = true, .tags = {"ready_flag"}, }); REQUIRE(res); CHECK(res.value().totalMatches > 0); } } TEST_CASE("GrepService - Falls back to extracted content when blob unavailable", "[grep][service][session]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("session_hot.txt", "alpha from metadata\n"); fixture.addHotContent("session_hot.txt", "alpha from metadata\n"); fixture.store_ = std::make_shared(fixture.store_); fixture.ctx_.store = fixture.store_; fixture.grepService_ = makeGrepService(fixture.ctx_); auto coldRes = fixture.grep({ .pattern = "alpha", .literalText = true, .useSession = false, }); REQUIRE(coldRes); CHECK(coldRes.value().totalMatches == 1); auto sessionRes = fixture.grep({ .pattern = "alpha", .literalText = true, .useSession = true, }); REQUIRE(sessionRes); CHECK(sessionRes.value().totalMatches == 1); REQUIRE(sessionRes.value().filesWith.size() == 1); CHECK(sessionRes.value().filesWith.front().find("session_hot.txt") != std::string::npos); } TEST_CASE("GrepService - Path-scoped grep prefers latest indexed version", "[grep][service][versioning]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; yams::test::ScopedEnvVar hotCap("YAMS_GREP_MAX_DOCS_HOT", std::string("1")); yams::test::ScopedEnvVar coldCap("YAMS_GREP_MAX_DOCS_COLD", std::string("1")); fixture.addDocument("versioned.txt", "old-token only\n"); fixture.addDocument("versioned.txt", "new-token only\n"); auto docs = metadata::queryDocumentsByPattern(*fixture.repo_, "%versioned.txt"); REQUIRE(docs); REQUIRE(docs.value().size() >= 2); auto res = fixture.grep({ .pattern = "new-token", .paths = {"versioned.txt"}, .literalText = true, }); REQUIRE(res); CHECK(res.value().totalMatches == 1); REQUIRE(res.value().filesWith.size() == 1); CHECK(res.value().filesWith.front().find("versioned.txt") != std::string::npos); } TEST_CASE("GrepService - Broad regex grep does not stop at candidate cap", "[grep][service][versioning]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; yams::test::ScopedEnvVar hotCap("YAMS_GREP_MAX_DOCS_HOT", std::string("2")); yams::test::ScopedEnvVar coldCap("YAMS_GREP_MAX_DOCS_COLD", std::string("0")); yams::test::ScopedEnvVar budget("YAMS_GREP_TIME_BUDGET_MS", std::string("10000")); fixture.addDocument("old_match.txt", "rare_token_42\n"); fixture.addDocument("newer_a.txt", "alpha\n"); fixture.addDocument("newer_b.txt", "beta\n"); fixture.addDocument("newer_c.txt", "gamma\n"); auto res = fixture.grep({ .pattern = "rare_token_[0-9]+", .regexOnly = true, }); REQUIRE(res); CHECK(res.value().totalMatches == 1); REQUIRE(res.value().filesWith.size() == 1); CHECK(res.value().filesWith.front().find("old_match.txt") != std::string::npos); } TEST_CASE("GrepService - Workspace scope rejects generated retrieval artifacts", "[grep][service][scope]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("src/authored.cpp", "shared_scope_token authored\n"); fixture.addDocument("meson.build", "shared_scope_token build definition\n"); fixture.addDocument("bin/tool.py", "shared_scope_token tool\n"); fixture.addDocument("build/fold/findings.sarif", "shared_scope_token fold\n"); fixture.addDocument(".pi/tasks/agent.json", "shared_scope_token agent\n"); fixture.addDocument("audit_artifacts/report.json", "shared_scope_token audit\n"); auto res = fixture.grep({ .pattern = "shared_scope_token.*", .scopePathPrefix = fixture.tmpDir_.string(), .regexOnly = true, .useSession = false, }); REQUIRE(res); REQUIRE(res.value().results.size() == 3); CHECK(std::ranges::none_of(res.value().results, [](const auto& result) { return utils::isGeneratedWorkspacePath(result.file); })); CHECK(res.value().searchStats.at("generated_candidates_rejected") == "3"); } TEST_CASE("GrepService - Edge Cases", "[grep][service][edge]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("edge.txt", "foo bar\nfoo-bar\nfoo_bar\n(hello) and (foo)\nHELLO world\n"); SECTION("Word boundary excludes hyphens") { GrepRequest req; req.pattern = "foo"; req.word = true; req.lineNumbers = true; req.withFilename = false; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().totalMatches > 0); for (const auto& fileResult : res.value().results) { for (const auto& match : fileResult.matches) { CHECK(match.line.find("foo-") == std::string::npos); } } } SECTION("Literal parentheses not interpreted as regex") { auto literalRes = fixture.grep({ .pattern = "(foo)", .literalText = true, }); REQUIRE(literalRes); CHECK(literalRes.value().totalMatches > 0); auto regexRes = fixture.grep({ .pattern = "(foo)", .literalText = false, }); REQUIRE(regexRes); CHECK(regexRes.value().totalMatches > 0); } SECTION("Ignore case matches uppercase and lowercase") { GrepRequest req; req.pattern = "HELLO"; req.ignoreCase = true; auto res = fixture.grep(req); REQUIRE(res); CHECK(res.value().totalMatches > 0); } } TEST_CASE("GrepService - Unicode Support", "[grep][service][unicode]") { SKIP_GREP_ON_WINDOWS(); GrepFixture fixture; fixture.addDocument("unicode.txt", "café\nCAFÉ\nnaïve\n(naïve)\nsmile 😊 end\n東京大学 CJK line\n"); SECTION("Literal unicode patterns match correctly") { auto cafeRes = fixture.grep({ .pattern = "café", .literalText = true, }); REQUIRE(cafeRes); CHECK(cafeRes.value().totalMatches > 0); auto emojiRes = fixture.grep({ .pattern = "😊", .literalText = true, }); REQUIRE(emojiRes); CHECK(emojiRes.value().totalMatches > 0); auto diacriticRes = fixture.grep({ .pattern = "(naïve)", .literalText = true, }); REQUIRE(diacriticRes); CHECK(diacriticRes.value().totalMatches > 0); } SECTION("Case-insensitive unicode matching (best effort)") { GrepRequest req; req.pattern = "café"; req.literalText = true; req.ignoreCase = true; auto res = fixture.grep(req); REQUIRE(res); if (res.value().totalMatches == 0) { SKIP("Unicode case-folding not available in this build"); } CHECK(res.value().totalMatches > 0); } } #endif // _WIN32