#include "navigation.hpp" #include "process.hpp" #include "repo_tool_around.hpp" #include "repo_tool_find.hpp" #include "repo_tool_map.hpp" #include #include #include #include TEST(Location, FinalColonAndNumericValidation) { const auto windows = rl::parse_location("C:\\src\\repo\\file.cpp:417"); ASSERT_TRUE(windows.has_value()); EXPECT_EQ(windows->file.string(), "C:\\src\\repo\\file.cpp"); EXPECT_EQ(windows->line, 417U); EXPECT_EQ(rl::parse_location("src/file.cpp:1")->line, 1U); for (const auto value : {"file", ":1", "file:0", "file:-1", "file:12x", "file:", "file:999999999999999999999999"}) { EXPECT_FALSE(rl::parse_location(value).has_value()) << value; } } TEST(Map, HeadingsRankBeforeBodyAndBoundariesAreRespected) { const std::vector lines{"# Map", "## Other", "cloth mentioned here", "### CLOTH", "preferred route", "### Next", "unrelated route"}; const auto output = rl::map_sections(lines, "cloth"); EXPECT_TRUE(output.starts_with("### CLOTH\npreferred route\n")); EXPECT_NE(output.find("cloth mentioned here"), std::string::npos); EXPECT_EQ(output.find("unrelated route"), std::string::npos); EXPECT_EQ(rl::map_sections(lines, "missing"), "No relevant project-map section found.\n"); } TEST(Map, LimitsSectionsLinesAndBytes) { EXPECT_NE(rl::map_sections({"## route 1", "## route 2", "## route 3", "## route 4"}, "route").find("TRUNCATED"), std::string::npos); std::vector lines(200, "route"); lines[0] = "## route"; const auto output = rl::map_sections(lines, "route"); EXPECT_LT(output.size(), 1000U); EXPECT_NE(output.find("TRUNCATED"), std::string::npos); EXPECT_LT(rl::map_sections({"## route", std::string(20000, 'x')}, "route").size(), 16460U); } TEST(Map, FencedHeadingsAreNotSectionBoundaries) { const auto output = rl::map_sections({"## route", "```markdown", "## example", "```", "tail", "## other", "omit"}, "route"); EXPECT_NE(output.find("tail"), std::string::npos); EXPECT_EQ(output.find("omit"), std::string::npos); } namespace { class Repository : public ::testing::Test { protected: std::filesystem::path previous; std::filesystem::path root; std::ostringstream output; std::ostringstream errors; std::streambuf* old_output = nullptr; std::streambuf* old_errors = nullptr; void SetUp() override { std::error_code code; previous = std::filesystem::current_path(code); ASSERT_FALSE(code) << code.message(); const auto temporary = std::filesystem::temp_directory_path(code); ASSERT_FALSE(code) << code.message(); root = temporary / ("repo-lens-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(root / ".git", code); ASSERT_FALSE(code) << code.message(); std::filesystem::create_directories(root / "nested", code); ASSERT_FALSE(code) << code.message(); std::filesystem::create_directories(root / "docs", code); ASSERT_FALSE(code) << code.message(); std::ofstream(root / "docs" / "PROJECTMAP.md") << "## route\nPath: src/\n"; std::ofstream file(root / "file with spaces.txt"); for (int i = 1; i <= 80; ++i) { file << "source " << i << '\n'; } std::filesystem::current_path(root / "nested", code); ASSERT_FALSE(code) << code.message(); old_output = std::cout.rdbuf(output.rdbuf()); old_errors = std::cerr.rdbuf(errors.rdbuf()); } void TearDown() override { if (old_output) { std::cout.rdbuf(old_output); } if (old_errors) { std::cerr.rdbuf(old_errors); } std::error_code code; std::filesystem::current_path(previous, code); EXPECT_FALSE(code) << code.message(); std::filesystem::remove_all(root, code); EXPECT_FALSE(code) << code.message(); } }; } // namespace TEST_F(Repository, RootDiscoveryAndDefaultSourceWindow) { std::filesystem::path found; std::string error; ASSERT_TRUE(rl::repository_root(found, error)) << error; EXPECT_EQ(found, root); const std::vector args{"file with spaces.txt:40"}; EXPECT_EQ(rl::RepoLensToolAround{}.execute(args), 0); EXPECT_TRUE(output.str().starts_with("FILE file with spaces.txt\nLINES 10-70\nTARGET 40\n10 | source 10\n")); EXPECT_TRUE(output.str().ends_with("70 | source 70\n")); } TEST_F(Repository, SourceWindowClampsAtBothEnds) { const std::vector start{"file with spaces.txt:1"}; EXPECT_EQ(rl::RepoLensToolAround{}.execute(start), 0); EXPECT_NE(output.str().find("LINES 1-31"), std::string::npos); output.str(""); const auto absolute = (root / "file with spaces.txt").string() + ":80"; const std::vector end{absolute, "--before", "0", "--after", "30"}; EXPECT_EQ(rl::RepoLensToolAround{}.execute(end), 0); EXPECT_NE(output.str().find("LINES 80-80"), std::string::npos); } TEST_F(Repository, InvalidAroundRequestsFail) { for (const auto location : {"missing:1", "file with spaces.txt:81", "file with spaces.txt:0"}) { const std::vector args{location}; EXPECT_NE(rl::RepoLensToolAround{}.execute(args), 0); } const std::vector too_large{"file with spaces.txt:1", "--before", "1001"}; EXPECT_NE(rl::RepoLensToolAround{}.execute(too_large), 0); const std::vector negative{"file with spaces.txt:1", "--after", "-1"}; EXPECT_NE(rl::RepoLensToolAround{}.execute(negative), 0); const std::vector missing{"file with spaces.txt:1", "--after"}; EXPECT_NE(rl::RepoLensToolAround{}.execute(missing), 0); } TEST_F(Repository, MapWorksFromNestedDirectory) { const std::vector args{"ROUTE"}; EXPECT_EQ(rl::RepoLensToolMap{}.execute(args), 0); EXPECT_EQ(output.str(), "## route\nPath: src/\n"); } TEST_F(Repository, FindLiteralTruncationAndNoMatches) { const auto version = rl::run_process({"rg", "--version"}, root, [](std::string_view) {}); if (version.exit_code != 0) { GTEST_SKIP() << "ripgrep unavailable: " << version.error; } { std::ofstream file(root / "matches.txt"); for (int i = 0; i < 25; ++i) { file << "literal.* literal.*\n"; } std::ofstream(root / ".gitignore") << "ignored.txt\n"; std::ofstream(root / "ignored.txt") << "literal.*\n"; } const std::vector args{"literal.*"}; EXPECT_EQ(rl::RepoLensToolFind{}.execute(args), 0); EXPECT_NE(output.str().find("MATCHES 50\nSHOWN 20\nTRUNCATED"), std::string::npos); EXPECT_NE(output.str().find("matches.txt:1\n"), std::string::npos); EXPECT_EQ(output.str().find("ignored.txt:"), std::string::npos); output.str(""); const std::vector absent{"nothing matches this"}; EXPECT_EQ(rl::RepoLensToolFind{}.execute(absent), 0); EXPECT_NE(output.str().find("MATCHES 0\nSHOWN 0"), std::string::npos); } TEST_F(Repository, ProcessPreservesArgumentsStreamsAndExitCode) { std::string captured; const auto executable = std::filesystem::path(REPO_TEST_EXECUTABLE).string(); const std::string argument = R"(spaces "quotes" & $() \)"; const auto result = rl::run_process({executable, "--process-fixture", argument}, root, [&](std::string_view chunk) { captured += chunk; }); EXPECT_EQ(result.exit_code, 7); EXPECT_EQ(captured, argument); EXPECT_EQ(result.error, "fixture stderr"); } TEST_F(Repository, RecoverableFailuresReturnErrors) { std::vector lines; std::string error; EXPECT_FALSE(rl::read_lines(root / "missing", lines, error)); EXPECT_NE(error.find("Cannot open file"), std::string::npos); const auto missing = rl::run_process({"repo-lens-missing-executable-12345"}, root, [](std::string_view) {}); EXPECT_NE(missing.exit_code, 0); EXPECT_FALSE(missing.error.empty()); const auto empty = rl::run_process({}, root, [](std::string_view) {}); EXPECT_NE(empty.exit_code, 0); std::error_code code; std::filesystem::remove(root / ".git", code); ASSERT_FALSE(code) << code.message(); std::filesystem::path found; EXPECT_FALSE(rl::repository_root(found, error)); EXPECT_NE(error.find("No repository root"), std::string::npos); }