290 lines
11 KiB
C++
290 lines
11 KiB
C++
// CLI Option Isolation tests
|
|
// Catch2 migration from GTest (yams-3s4 / yams-cli)
|
|
//
|
|
// Tests for CLI11 option isolation - ensuring option descriptions
|
|
// do not leak into option values.
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <CLI/CLI.hpp>
|
|
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// ============================================================================
|
|
// Positional argument isolation tests
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Positional description does not leak into value",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string targetPath;
|
|
app.add_option("path", targetPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
#ifdef _WIN32
|
|
std::vector<const char*> args = {"test", "C:\\some\\path"};
|
|
const char* expectedPath = "C:\\some\\path";
|
|
#else
|
|
std::vector<const char*> args = {"test", "/some/path"};
|
|
const char* expectedPath = "/some/path";
|
|
#endif
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(targetPath == expectedPath);
|
|
CHECK(targetPath != "[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Positional without argument remains empty",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string targetPath;
|
|
app.add_option("path", targetPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
std::vector<const char*> args = {"test"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(targetPath.empty());
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Multiple positionals remain isolated",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::vector<std::string> targetPaths;
|
|
std::string legacyPath;
|
|
|
|
app.add_option("paths", targetPaths,
|
|
"File or directory paths to add (use '-' for stdin). Accepts multiple.");
|
|
app.add_option("path", legacyPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
std::vector<const char*> args = {"test", "file1.txt", "file2.txt"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
REQUIRE_FALSE(targetPaths.empty());
|
|
for (const auto& path : targetPaths) {
|
|
CHECK(path.find("[Deprecated]") == std::string::npos);
|
|
CHECK(path.find("File or directory") == std::string::npos);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Named option isolation tests
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Named option description does not leak into value",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string dataDir;
|
|
app.add_option("--data-dir", dataDir, "Data directory for storage");
|
|
|
|
std::vector<const char*> args = {"test", "--data-dir", "/custom/path"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(dataDir == "/custom/path");
|
|
CHECK(dataDir != "Data directory for storage");
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Named option with default val does not contain description",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string dataDir;
|
|
app.add_option("--data-dir", dataDir, "Data directory for storage")
|
|
->default_val("/default/path");
|
|
|
|
std::vector<const char*> args = {"test"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(dataDir == "/default/path");
|
|
CHECK(dataDir != "Data directory for storage");
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Named option without argument uses default",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string name;
|
|
app.add_option("-n,--name", name, "Name for the document (especially useful for stdin)")
|
|
->default_val("");
|
|
|
|
std::vector<const char*> args = {"test"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(name.empty());
|
|
CHECK(name != "Name for the document (especially useful for stdin)");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Subcommand isolation tests
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Subcommand option descriptions remain isolated",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string dataDir;
|
|
app.add_option("--data-dir", dataDir, "Data directory for storage")->default_val("/default");
|
|
|
|
auto* addCmd = app.add_subcommand("add", "Add document(s) to the store");
|
|
|
|
std::string targetPath;
|
|
addCmd->add_option("path", targetPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
std::string docName;
|
|
addCmd->add_option("-n,--name", docName, "Name for the document (especially useful for stdin)");
|
|
|
|
#ifdef _WIN32
|
|
std::vector<const char*> args = {"test", "add", "C:\\some\\file", "--name", "myfile"};
|
|
const char* expectedPath = "C:\\some\\file";
|
|
#else
|
|
std::vector<const char*> args = {"test", "add", "/some/file", "--name", "myfile"};
|
|
const char* expectedPath = "/some/file";
|
|
#endif
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(targetPath == expectedPath);
|
|
CHECK(docName == "myfile");
|
|
CHECK(dataDir == "/default");
|
|
|
|
CHECK(targetPath.find("[Deprecated]") == std::string::npos);
|
|
CHECK(docName.find("Name for the document") == std::string::npos);
|
|
CHECK(dataDir.find("Data directory") == std::string::npos);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge cases - the actual bug scenario
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Path does not become description when missing",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("YAMS");
|
|
|
|
std::string dataDir;
|
|
app.add_option("--data-dir,--storage", dataDir, "Data directory for storage")
|
|
->default_val("/home/user/.local/share/yams");
|
|
|
|
auto* addCmd = app.add_subcommand("add", "Add document(s) or directory to the content store");
|
|
|
|
std::vector<std::string> targetPaths;
|
|
std::string legacyTargetPath;
|
|
|
|
addCmd->add_option("paths", targetPaths,
|
|
"File or directory paths to add (use '-' for stdin). Accepts multiple.");
|
|
addCmd->add_option("path", legacyTargetPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
std::vector<const char*> args = {"yams", "add"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(targetPaths.empty());
|
|
CHECK(legacyTargetPath.empty());
|
|
CHECK(dataDir == "/home/user/.local/share/yams");
|
|
|
|
CHECK(legacyTargetPath.find("[Deprecated]") == std::string::npos);
|
|
CHECK(dataDir.find("Data directory") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Global option not polluted by subcommand descriptions",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("YAMS");
|
|
app.prefix_command();
|
|
|
|
std::string dataDir;
|
|
app.add_option("--data-dir", dataDir, "Data directory for storage")
|
|
->default_val("/default/data");
|
|
|
|
auto* addCmd = app.add_subcommand("add", "Add documents");
|
|
|
|
std::string targetPath;
|
|
addCmd->add_option("path", targetPath,
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)");
|
|
|
|
std::vector<const char*> args = {"yams", "--data-dir", "/custom", "add", "somefile.txt"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(dataDir == "/custom");
|
|
CHECK(targetPath == "somefile.txt");
|
|
|
|
CHECK(dataDir.find("[Deprecated]") == std::string::npos);
|
|
CHECK(targetPath.find("Data directory") == std::string::npos);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Value validation - ensure descriptions are never valid file paths
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Description strings are not valid paths",
|
|
"[cli][option_isolation][catch2]") {
|
|
std::vector<std::string> descriptions = {
|
|
"[Deprecated] Single path to file/directory (use '-' for stdin)",
|
|
"File or directory paths to add (use '-' for stdin). Accepts multiple.",
|
|
"Name for the document (especially useful for stdin)", "Data directory for storage"};
|
|
|
|
for (const auto& desc : descriptions) {
|
|
INFO("Checking description: " << desc);
|
|
|
|
std::filesystem::path p(desc);
|
|
|
|
bool hasSpecialChars =
|
|
desc.find('[') != std::string::npos || desc.find('(') != std::string::npos;
|
|
bool isLongDescription = desc.length() > 60;
|
|
bool containsMultipleSpaces = std::count(desc.begin(), desc.end(), ' ') >= 2;
|
|
bool hasDescriptionWords = desc.find("directory") != std::string::npos ||
|
|
desc.find("file") != std::string::npos ||
|
|
desc.find("path") != std::string::npos;
|
|
|
|
CHECK((hasSpecialChars || isLongDescription || containsMultipleSpaces ||
|
|
hasDescriptionWords));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("CLIOptionIsolation - Empty value does not become description",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string value;
|
|
auto* opt = app.add_option("--option", value, "This is the option description");
|
|
|
|
std::vector<const char*> args = {"test"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(value.empty());
|
|
CHECK(opt->get_description() == "This is the option description");
|
|
CHECK(value != opt->get_description());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Multiple values with same description pattern
|
|
// ============================================================================
|
|
|
|
TEST_CASE("CLIOptionIsolation - Multiple options with similar descriptions",
|
|
"[cli][option_isolation][catch2]") {
|
|
CLI::App app("Test");
|
|
|
|
std::string queryPath;
|
|
std::string namePath;
|
|
std::string deletePath;
|
|
|
|
app.add_option("--query", queryPath, "Read query from file path (use '-' to read from STDIN)");
|
|
app.add_option("--name", namePath, "Name of the document to retrieve (explicit flag form)");
|
|
app.add_option("--delete", deletePath, "Delete multiple documents by names (comma-separated)");
|
|
|
|
std::vector<const char*> args = {"test", "--query", "search.txt"};
|
|
app.parse(static_cast<int>(args.size()), const_cast<char**>(args.data()));
|
|
|
|
CHECK(queryPath == "search.txt");
|
|
CHECK(namePath.empty());
|
|
CHECK(deletePath.empty());
|
|
|
|
CHECK(queryPath.find("Read query from") == std::string::npos);
|
|
CHECK(namePath.find("Name of the document") == std::string::npos);
|
|
CHECK(deletePath.find("Delete multiple") == std::string::npos);
|
|
}
|