yams/tests/unit/common/pattern_utils_catch2_test.cpp
trvon 8ad4ab29e0
fix(search): correct code navigation
Honor root-anchored and slash-qualified ignore patterns during live fallback. Center hydrated snippets on query matches and preserve symbol ranking through the daemon boundary.
2026-07-28 22:33:31 -04:00

180 lines
6.2 KiB
C++

// Tests for common glob/wildcard utilities.
#include <catch2/catch_test_macros.hpp>
#include <sstream>
#include <string>
#include <yams/common/gitignore.h>
#include <yams/common/pattern_utils.h>
using yams::common::brace_expand;
using yams::common::glob_match_path;
using yams::common::glob_to_sql_like;
using yams::common::has_wildcards;
using yams::common::match_segment;
using yams::common::matchesGitignore;
using yams::common::normalize_path;
using yams::common::parseGitignorePatterns;
using yams::common::split_patterns;
using yams::common::wildcard_match;
TEST_CASE("common::wildcard_match basics", "[common][pattern][wildcard][catch2]") {
CHECK(wildcard_match("", ""));
CHECK(wildcard_match("a", "a"));
CHECK_FALSE(wildcard_match("a", "b"));
CHECK(wildcard_match("abc", "a?c"));
CHECK_FALSE(wildcard_match("abc", "a?d"));
CHECK(wildcard_match("abc", "*"));
CHECK(wildcard_match("abc", "a*"));
CHECK(wildcard_match("abc", "*c"));
CHECK(wildcard_match("abc", "a*c"));
CHECK_FALSE(wildcard_match("abc", "a*d"));
// Backtracking behavior: star matches varying lengths.
CHECK(wildcard_match("abbbc", "a*bc"));
CHECK(wildcard_match("abbbc", "a*b*c"));
}
TEST_CASE("common::normalize_path normalizes separators and slashes",
"[common][pattern][path][catch2]") {
CHECK(normalize_path("a/b/c") == "a/b/c");
CHECK(normalize_path("a\\b\\c") == "a/b/c");
CHECK(normalize_path("a//b///c/") == "a/b/c");
CHECK(normalize_path("/") == "/");
}
TEST_CASE("common::brace_expand expands simple and nested sets",
"[common][pattern][brace][catch2]") {
{
const auto out = brace_expand("*.{cpp,hpp}");
REQUIRE(out.size() == 2);
CHECK(out[0] == "*.cpp");
CHECK(out[1] == "*.hpp");
}
{
const auto out = brace_expand("{a,{b,c}}.txt");
REQUIRE(out.size() == 3);
CHECK(out[0] == "a.txt");
CHECK(out[1] == "b.txt");
CHECK(out[2] == "c.txt");
}
{
const auto out = brace_expand("no_braces");
REQUIRE(out.size() == 1);
CHECK(out[0] == "no_braces");
}
}
TEST_CASE("common::match_segment supports ?, *, and character classes",
"[common][pattern][segment][catch2]") {
CHECK(match_segment("main.cpp", "*.cpp"));
CHECK(match_segment("a", "?"));
CHECK_FALSE(match_segment("", "?"));
CHECK(match_segment("a1", "a[0-9]"));
CHECK_FALSE(match_segment("aa", "a[0-9]"));
CHECK(match_segment("b", "[!a]"));
CHECK_FALSE(match_segment("a", "[!a]"));
CHECK(match_segment("x", "[xyz]"));
CHECK_FALSE(match_segment("q", "[xyz]"));
}
TEST_CASE("common::glob_match_path supports **, anchors, and brace alternatives",
"[common][pattern][glob][catch2]") {
// ** spans segments
CHECK(glob_match_path("src/main.cpp", "src/**/main.cpp"));
CHECK(glob_match_path("src/a/b/main.cpp", "src/**/main.cpp"));
// implicit leading ** when unanchored
CHECK(glob_match_path("a/b/c.cpp", "c.cpp"));
CHECK(glob_match_path("a/b/c.cpp", "*.cpp"));
// anchored patterns match from the beginning
CHECK_FALSE(glob_match_path("a/src/x.cpp", "^src/*.cpp"));
CHECK(glob_match_path("src/x.cpp", "^src/*.cpp"));
// brace expansion
CHECK(glob_match_path("include/x.hpp", "**/*.{cpp,hpp}"));
CHECK(glob_match_path("src/x.cpp", "**/*.{cpp,hpp}"));
CHECK_FALSE(glob_match_path("src/x.c", "**/*.{cpp,hpp}"));
// path normalization during matching
CHECK(glob_match_path("src\\a\\b\\x.cpp", "src/**/x.cpp"));
// unanchored patterns may match in the middle (implicit leading **)
CHECK(glob_match_path("a/b/x/b/c", "b/c"));
CHECK(glob_match_path("a/b/x/b/c", "b/*"));
CHECK_FALSE(glob_match_path("a/b/x/b/c", "^b/c"));
// ** at end can match zero or more segments
CHECK(glob_match_path("a/b", "a/**"));
CHECK(glob_match_path("a/b/c", "a/**"));
CHECK(glob_match_path("a", "a/**"));
// consecutive ** behaves the same
CHECK(glob_match_path("a/b/c", "a/**/**/c"));
// character classes within a segment
CHECK(glob_match_path("src/a1/file.txt", "src/a[0-9]/*.txt"));
CHECK_FALSE(glob_match_path("src/aa/file.txt", "src/a[0-9]/*.txt"));
// patterns with no path segments
CHECK_FALSE(glob_match_path("a/b/c.txt", ""));
CHECK_FALSE(glob_match_path("a/b/c.txt", "^"));
}
TEST_CASE("common::split_patterns trims and skips empties", "[common][pattern][split][catch2]") {
{
const auto out = split_patterns("");
CHECK(out.empty());
}
{
const auto out = split_patterns(" a , , b,, c ");
REQUIRE(out.size() == 3);
CHECK(out[0] == "a");
CHECK(out[1] == "b");
CHECK(out[2] == "c");
}
}
TEST_CASE("common::glob_to_sql_like converts glob metacharacters",
"[common][pattern][sql][catch2]") {
CHECK(glob_to_sql_like("*") == "%");
CHECK(glob_to_sql_like("?") == "_");
CHECK(glob_to_sql_like("a*b?c") == "a%b_c");
CHECK(glob_to_sql_like("no_wildcards") == "no_wildcards");
}
TEST_CASE("common::has_wildcards reports wildcard metacharacters",
"[common][pattern][wildcards][catch2]") {
CHECK_FALSE(has_wildcards("abc"));
CHECK(has_wildcards("a*c"));
CHECK(has_wildcards("a?c"));
}
TEST_CASE("common::gitignore patterns prune ignored working-tree paths",
"[common][pattern][gitignore][catch2]") {
std::istringstream input{"# transient agent state\n"
".claude/\n"
"*.tmp\n"
"/root-only.txt\n"
"config/generated/\n"
"!keep.tmp\n"};
const auto patterns = parseGitignorePatterns(input);
CHECK(matchesGitignore(".claude", patterns));
CHECK(matchesGitignore(".claude/worktrees/copy/src/search.cpp", patterns));
CHECK(matchesGitignore("build/output.tmp", patterns));
CHECK(matchesGitignore("root-only.txt", patterns));
CHECK_FALSE(matchesGitignore("nested/root-only.txt", patterns));
CHECK(matchesGitignore("config/generated/cache.bin", patterns));
CHECK_FALSE(matchesGitignore("nested/config/generated/cache.bin", patterns));
CHECK_FALSE(matchesGitignore("src/search.cpp", patterns));
}