321 lines
11 KiB
C++
321 lines
11 KiB
C++
#include <string>
|
|
#include <vector>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <yams/app/services/literal_extractor.hpp>
|
|
|
|
using namespace yams::app::services;
|
|
|
|
TEST_CASE("LiteralExtractor: Pure literal patterns", "[literal_extractor]") {
|
|
SECTION("Simple literal") {
|
|
auto result = LiteralExtractor::extract("hello", false);
|
|
REQUIRE(result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "hello");
|
|
REQUIRE(result.longestLength == 5);
|
|
}
|
|
|
|
SECTION("Literal with spaces") {
|
|
auto result = LiteralExtractor::extract("hello world", false);
|
|
REQUIRE(result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "hello world");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LiteralExtractor: Patterns with metacharacters", "[literal_extractor]") {
|
|
SECTION("Literal prefix with metacharacter") {
|
|
auto result = LiteralExtractor::extract("class.*", false);
|
|
REQUIRE(!result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "class");
|
|
REQUIRE(result.longestLength == 5);
|
|
}
|
|
|
|
SECTION("Literal surrounded by metacharacters") {
|
|
auto result = LiteralExtractor::extract(".*error.*", false);
|
|
REQUIRE(!result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "error");
|
|
}
|
|
|
|
SECTION("Multiple literals separated by metacharacters") {
|
|
auto result = LiteralExtractor::extract("foo.*bar", false);
|
|
REQUIRE(!result.isComplete);
|
|
REQUIRE(result.literals.size() == 2);
|
|
// Should contain both literals
|
|
bool hasFoo = false, hasBar = false;
|
|
for (const auto& lit : result.literals) {
|
|
if (lit == "foo")
|
|
hasFoo = true;
|
|
if (lit == "bar")
|
|
hasBar = true;
|
|
}
|
|
REQUIRE(hasFoo);
|
|
REQUIRE(hasBar);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LiteralExtractor: Escaped characters", "[literal_extractor]") {
|
|
SECTION("Escaped dot becomes literal") {
|
|
auto result = LiteralExtractor::extract("file\\.txt", false);
|
|
REQUIRE(result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "file.txt");
|
|
}
|
|
|
|
SECTION("Escaped star becomes literal") {
|
|
auto result = LiteralExtractor::extract("x\\*y", false);
|
|
REQUIRE(result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "x*y");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LiteralExtractor: Special sequences", "[literal_extractor]") {
|
|
SECTION("Word boundary breaks literal") {
|
|
auto result = LiteralExtractor::extract("\\bword\\b", false);
|
|
REQUIRE(!result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "word");
|
|
}
|
|
|
|
SECTION("Digit class breaks literal") {
|
|
auto result = LiteralExtractor::extract("id\\d+", false);
|
|
REQUIRE(!result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "id");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LiteralExtractor: Case insensitivity", "[literal_extractor]") {
|
|
SECTION("Converts to lowercase when ignoreCase=true") {
|
|
auto result = LiteralExtractor::extract("FooBar", true);
|
|
REQUIRE(result.isComplete);
|
|
REQUIRE(result.literals.size() == 1);
|
|
REQUIRE(result.literals[0] == "foobar");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LiteralExtractor: longest() helper", "[literal_extractor]") {
|
|
SECTION("Returns longest literal") {
|
|
auto result = LiteralExtractor::extract("a.*longer.*b", false);
|
|
REQUIRE(result.longest() == "longer");
|
|
}
|
|
|
|
SECTION("Empty result returns empty string") {
|
|
auto result = LiteralExtractor::extract("\\d+", false);
|
|
REQUIRE(result.empty());
|
|
}
|
|
}
|
|
|
|
// --- Boyer-Moore-Horspool Tests ---
|
|
|
|
TEST_CASE("BMHSearcher: Basic literal search", "[bmh]") {
|
|
SECTION("Find single occurrence") {
|
|
BMHSearcher searcher("hello", false);
|
|
std::string text = "say hello world";
|
|
REQUIRE(searcher.find(text) == 4);
|
|
}
|
|
|
|
SECTION("Pattern not found") {
|
|
BMHSearcher searcher("goodbye", false);
|
|
std::string text = "say hello world";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("Multiple occurrences") {
|
|
BMHSearcher searcher("the", false);
|
|
std::string text = "the quick brown fox jumps over the lazy dog";
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 2);
|
|
REQUIRE(matches[0] == 0);
|
|
REQUIRE(matches[1] == 31);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Case sensitivity", "[bmh]") {
|
|
SECTION("Case-sensitive match") {
|
|
BMHSearcher searcher("Hello", false);
|
|
std::string text = "say hello world";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("Case-insensitive match") {
|
|
BMHSearcher searcher("Hello", true);
|
|
std::string text = "say hello world";
|
|
REQUIRE(searcher.find(text) == 4);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Edge cases", "[bmh]") {
|
|
SECTION("Empty pattern") {
|
|
BMHSearcher searcher("", false);
|
|
std::string text = "anything";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("Pattern longer than text") {
|
|
BMHSearcher searcher("very long pattern", false);
|
|
std::string text = "short";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("Single character pattern") {
|
|
BMHSearcher searcher("x", false);
|
|
std::string text = "axbxc";
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 2);
|
|
REQUIRE(matches[0] == 1);
|
|
REQUIRE(matches[1] == 3);
|
|
}
|
|
|
|
SECTION("Pattern at start") {
|
|
BMHSearcher searcher("start", false);
|
|
std::string text = "start of text";
|
|
REQUIRE(searcher.find(text) == 0);
|
|
}
|
|
|
|
SECTION("Pattern at end") {
|
|
BMHSearcher searcher("end", false);
|
|
std::string text = "text at end";
|
|
REQUIRE(searcher.find(text) == 8);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Performance characteristics", "[bmh]") {
|
|
SECTION("Skips efficiently on mismatches") {
|
|
BMHSearcher searcher("pattern", false);
|
|
// Text with no matches - should skip through quickly
|
|
std::string text(10000, 'x');
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("Handles repeated patterns") {
|
|
BMHSearcher searcher("abc", false);
|
|
std::string text = "abcabcabcabcabc";
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 5);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Real-world patterns", "[bmh]") {
|
|
SECTION("Find TODO in code") {
|
|
BMHSearcher searcher("TODO", false);
|
|
std::string code = "// TODO: fix this\nint x = 0; // TODO: optimize";
|
|
auto matches = searcher.findAll(code);
|
|
REQUIRE(matches.size() == 2);
|
|
}
|
|
|
|
SECTION("Find function call") {
|
|
BMHSearcher searcher("malloc", false);
|
|
std::string code = "void* p = malloc(size);\nfree(p);";
|
|
REQUIRE(searcher.find(code) == 10);
|
|
}
|
|
|
|
SECTION("Find URL pattern") {
|
|
BMHSearcher searcher("https://", false);
|
|
std::string text = "Visit https://example.com or https://github.com";
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 2);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BMHSearcher: Long patterns (exercise SIMD memmem codepath)
|
|
//
|
|
// When the needle is >= 16 bytes, findBMH delegates to simdMemmem / simdMemmemCI
|
|
// instead of the scalar Boyer-Moore-Horspool loop.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BMHSearcher: Long case-sensitive pattern (SIMD path)", "[bmh][simd]") {
|
|
// 20-char needle — well above the 16-byte threshold.
|
|
const std::string needle = "namespace yams::app";
|
|
|
|
SECTION("found in middle of text") {
|
|
BMHSearcher searcher(needle, false);
|
|
std::string text = "// file header\nnamespace yams::app {\n// body\n}";
|
|
REQUIRE(searcher.find(text) == 15);
|
|
}
|
|
|
|
SECTION("found at start") {
|
|
BMHSearcher searcher(needle, false);
|
|
std::string text = "namespace yams::app::services { /* ... */ }";
|
|
REQUIRE(searcher.find(text) == 0);
|
|
}
|
|
|
|
SECTION("not found") {
|
|
BMHSearcher searcher(needle, false);
|
|
std::string text = "namespace yams::search { /* ... */ }";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("findAll in large text") {
|
|
BMHSearcher searcher(needle, false);
|
|
std::string text;
|
|
// Build a text with the needle at known offsets.
|
|
text += std::string(100, 'x'); // padding
|
|
text += needle; // match 1 at offset 100
|
|
text += std::string(200, 'y'); // padding
|
|
text += needle; // match 2 at offset 100+needle.size()+200
|
|
text += std::string(50, 'z'); // trailing padding
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 2);
|
|
REQUIRE(matches[0] == 100);
|
|
REQUIRE(matches[1] == 100 + needle.size() + 200);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Long case-insensitive pattern (SIMD CI path)", "[bmh][simd]") {
|
|
// 24-char needle — exercises simdMemmemCI via findBMH.
|
|
const std::string needle = "The Quick Brown Fox Jump";
|
|
|
|
SECTION("matches uppercase haystack") {
|
|
BMHSearcher searcher(needle, true);
|
|
std::string text = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG";
|
|
REQUIRE(searcher.find(text) == 0);
|
|
}
|
|
|
|
SECTION("matches mixed-case haystack") {
|
|
BMHSearcher searcher(needle, true);
|
|
std::string text = "see the quick BROWN Fox Jump here";
|
|
REQUIRE(searcher.find(text) == 4);
|
|
}
|
|
|
|
SECTION("not found") {
|
|
BMHSearcher searcher(needle, true);
|
|
std::string text = "the slow red cat sleeps on the warm mat";
|
|
REQUIRE(searcher.find(text) == std::string::npos);
|
|
}
|
|
|
|
SECTION("findAll in large text with mixed case") {
|
|
BMHSearcher searcher(needle, true);
|
|
std::string text;
|
|
text += std::string(64, '.');
|
|
text += "THE QUICK BROWN FOX JUMP"; // match 1 at 64
|
|
text += std::string(64, '.');
|
|
text += "the quick brown fox jump"; // match 2 at 64+24+64 = 152
|
|
text += std::string(64, '.');
|
|
auto matches = searcher.findAll(text);
|
|
REQUIRE(matches.size() == 2);
|
|
REQUIRE(matches[0] == 64);
|
|
REQUIRE(matches[1] == 152);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BMHSearcher: Exactly 16-byte pattern (boundary)", "[bmh][simd]") {
|
|
// 16 bytes is the exact threshold — should still use the SIMD path.
|
|
const std::string needle = "0123456789abcdef"; // exactly 16 bytes
|
|
REQUIRE(needle.size() == 16);
|
|
|
|
SECTION("case-sensitive") {
|
|
BMHSearcher searcher(needle, false);
|
|
std::string text = "prefix_0123456789abcdef_suffix";
|
|
REQUIRE(searcher.find(text) == 7);
|
|
}
|
|
|
|
SECTION("case-insensitive") {
|
|
BMHSearcher searcher(needle, true);
|
|
std::string text = "PREFIX_0123456789ABCDEF_SUFFIX";
|
|
REQUIRE(searcher.find(text) == 7);
|
|
}
|
|
}
|