// CLI Prompt Utility tests // Catch2 migration from GTest (yams-3s4 / yams-cli) // // Tests for prompt_util.h - Interactive CLI prompt utilities. #include #include #include #include #include #include using namespace yams::cli; namespace { /** * RAII helper to redirect std::cin to a custom buffer for testing. */ class MockStdin { public: explicit MockStdin(const std::string& input) : buffer_(input), oldBuf_(std::cin.rdbuf()) { std::cin.rdbuf(buffer_.rdbuf()); } ~MockStdin() { std::cin.rdbuf(oldBuf_); std::cin.clear(); // Clear any error flags } MockStdin(const MockStdin&) = delete; MockStdin& operator=(const MockStdin&) = delete; private: std::istringstream buffer_; std::streambuf* oldBuf_; }; /** * RAII helper to suppress cout during tests. */ class SuppressCout { public: SuppressCout() : oldBuf_(std::cout.rdbuf()) { std::cout.rdbuf(sink_.rdbuf()); } ~SuppressCout() { std::cout.rdbuf(oldBuf_); } SuppressCout(const SuppressCout&) = delete; SuppressCout& operator=(const SuppressCout&) = delete; private: std::ostringstream sink_; std::streambuf* oldBuf_; }; } // namespace // ============================================================================ // prompt_yes_no() tests // ============================================================================ TEST_CASE("PromptUtil - YesNo accepts lowercase y", "[cli][prompt_util][catch2]") { MockStdin input("y\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?")); } TEST_CASE("PromptUtil - YesNo accepts uppercase Y", "[cli][prompt_util][catch2]") { MockStdin input("Y\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?")); } TEST_CASE("PromptUtil - YesNo accepts lowercase n", "[cli][prompt_util][catch2]") { MockStdin input("n\n"); SuppressCout suppress; CHECK_FALSE(prompt_yes_no("Continue?")); } TEST_CASE("PromptUtil - YesNo accepts uppercase N", "[cli][prompt_util][catch2]") { MockStdin input("N\n"); SuppressCout suppress; CHECK_FALSE(prompt_yes_no("Continue?")); } TEST_CASE("PromptUtil - YesNo empty input returns default yes", "[cli][prompt_util][catch2]") { MockStdin input("\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.defaultYes = true})); } TEST_CASE("PromptUtil - YesNo empty input returns default no", "[cli][prompt_util][catch2]") { MockStdin input("\n"); SuppressCout suppress; CHECK_FALSE(prompt_yes_no("Continue?", {.defaultYes = false})); } TEST_CASE("PromptUtil - YesNo invalid input returns default", "[cli][prompt_util][catch2]") { MockStdin input("maybe\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.defaultYes = true, .retryOnInvalid = false})); } TEST_CASE("PromptUtil - YesNo EOF returns default", "[cli][prompt_util][catch2]") { MockStdin input(""); // Empty = EOF SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.defaultYes = true})); } TEST_CASE("PromptUtil - YesNo custom yes chars", "[cli][prompt_util][catch2]") { MockStdin input("o\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.yesChars = "oO"})); } TEST_CASE("PromptUtil - YesNo custom no chars", "[cli][prompt_util][catch2]") { MockStdin input("x\n"); SuppressCout suppress; CHECK_FALSE(prompt_yes_no("Continue?", {.noChars = "xX"})); } TEST_CASE("PromptUtil - YesNo retry on invalid eventually accepts", "[cli][prompt_util][catch2]") { MockStdin input("maybe\ny\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.retryOnInvalid = true})); } TEST_CASE("PromptUtil - YesNo first character used", "[cli][prompt_util][catch2]") { MockStdin input("yes please\n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?")); } // ============================================================================ // prompt_input() tests // ============================================================================ TEST_CASE("PromptUtil - Input returns user input", "[cli][prompt_util][catch2]") { MockStdin input("hello world\n"); SuppressCout suppress; CHECK(prompt_input("Enter value:") == "hello world"); } TEST_CASE("PromptUtil - Input trims whitespace", "[cli][prompt_util][catch2]") { MockStdin input(" trimmed \n"); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.trimWhitespace = true}) == "trimmed"); } TEST_CASE("PromptUtil - Input preserves whitespace when disabled", "[cli][prompt_util][catch2]") { MockStdin input(" not trimmed \n"); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.trimWhitespace = false}) == " not trimmed "); } TEST_CASE("PromptUtil - Input empty returns default", "[cli][prompt_util][catch2]") { MockStdin input("\n"); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.defaultValue = "default"}) == "default"); } TEST_CASE("PromptUtil - Input empty with no default returns empty", "[cli][prompt_util][catch2]") { MockStdin input("\n"); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.defaultValue = "", .allowEmpty = true}) == ""); } TEST_CASE("PromptUtil - Input EOF returns default", "[cli][prompt_util][catch2]") { MockStdin input(""); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.defaultValue = "fallback"}) == "fallback"); } TEST_CASE("PromptUtil - Input validator accepts valid input", "[cli][prompt_util][catch2]") { MockStdin input("valid\n"); SuppressCout suppress; auto validator = [](const std::string& s) { return s == "valid"; }; CHECK(prompt_input("Enter value:", {.validator = validator}) == "valid"); } TEST_CASE("PromptUtil - Input validator rejects invalid with retry", "[cli][prompt_util][catch2]") { MockStdin input("invalid\nvalid\n"); SuppressCout suppress; auto validator = [](const std::string& s) { return s == "valid"; }; CHECK(prompt_input("Enter value:", {.retryOnInvalid = true, .validator = validator}) == "valid"); } TEST_CASE("PromptUtil - Input validator rejects invalid returns default", "[cli][prompt_util][catch2]") { MockStdin input("invalid\n"); SuppressCout suppress; auto validator = [](const std::string& s) { return s == "valid"; }; CHECK(prompt_input("Enter value:", {.defaultValue = "fallback", .retryOnInvalid = false, .validator = validator}) == "fallback"); } TEST_CASE("PromptUtil - Input trims tabs and newlines", "[cli][prompt_util][catch2]") { MockStdin input("\t\ttabbed\r\n"); SuppressCout suppress; CHECK(prompt_input("Enter value:", {.trimWhitespace = true}) == "tabbed"); } // ============================================================================ // prompt_choice() tests // ============================================================================ TEST_CASE("PromptUtil - Choice returns selected index", "[cli][prompt_util][catch2]") { MockStdin input("2\n"); SuppressCout suppress; std::vector items = {{"first", "First Option", ""}, {"second", "Second Option", ""}, {"third", "Third Option", ""}}; CHECK(prompt_choice("Select:", items) == 1); // 0-indexed } TEST_CASE("PromptUtil - Choice empty returns default", "[cli][prompt_util][catch2]") { MockStdin input("\n"); SuppressCout suppress; std::vector items = {{"a", "Option A", ""}, {"b", "Option B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 1}) == 1); } TEST_CASE("PromptUtil - Choice first item selected", "[cli][prompt_util][catch2]") { MockStdin input("1\n"); SuppressCout suppress; std::vector items = {{"first", "First", ""}, {"second", "Second", ""}}; CHECK(prompt_choice("Select:", items) == 0); } TEST_CASE("PromptUtil - Choice last item selected", "[cli][prompt_util][catch2]") { MockStdin input("3\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}, {"c", "C", ""}}; CHECK(prompt_choice("Select:", items) == 2); } TEST_CASE("PromptUtil - Choice invalid number returns default", "[cli][prompt_util][catch2]") { MockStdin input("99\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 0, .retryOnInvalid = false}) == 0); } TEST_CASE("PromptUtil - Choice non-numeric returns default", "[cli][prompt_util][catch2]") { MockStdin input("abc\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 1, .retryOnInvalid = false}) == 1); } TEST_CASE("PromptUtil - Choice zero is invalid", "[cli][prompt_util][catch2]") { MockStdin input("0\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 0, .retryOnInvalid = false}) == 0); } TEST_CASE("PromptUtil - Choice negative is invalid", "[cli][prompt_util][catch2]") { MockStdin input("-1\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 0, .retryOnInvalid = false}) == 0); } TEST_CASE("PromptUtil - Choice retry eventually accepts", "[cli][prompt_util][catch2]") { MockStdin input("invalid\n99\n2\n"); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}, {"c", "C", ""}}; CHECK(prompt_choice("Select:", items, {.retryOnInvalid = true}) == 1); } TEST_CASE("PromptUtil - Choice EOF returns default", "[cli][prompt_util][catch2]") { MockStdin input(""); SuppressCout suppress; std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; CHECK(prompt_choice("Select:", items, {.defaultIndex = 1}) == 1); } TEST_CASE("PromptUtil - Choice throws on empty items", "[cli][prompt_util][catch2]") { SuppressCout suppress; std::vector items; REQUIRE_THROWS_AS(prompt_choice("Select:", items), std::invalid_argument); } TEST_CASE("PromptUtil - Choice uses value as label when label empty", "[cli][prompt_util][catch2]") { MockStdin input("1\n"); SuppressCout suppress; std::vector items = {{"value_only", "", "description"}}; CHECK(prompt_choice("Select:", items) == 0); } // ============================================================================ // Edge case tests // ============================================================================ TEST_CASE("PromptUtil - YesNo whitespace only treated as empty", "[cli][prompt_util][catch2]") { MockStdin input(" \n"); SuppressCout suppress; CHECK(prompt_yes_no("Continue?", {.defaultYes = true, .retryOnInvalid = false})); } TEST_CASE("PromptUtil - Input only whitespace returns default when trimmed", "[cli][prompt_util][catch2]") { MockStdin input(" \t \n"); SuppressCout suppress; CHECK(prompt_input("Value:", {.defaultValue = "default", .trimWhitespace = true}) == "default"); } TEST_CASE("PromptUtil - Multiple prompts in sequence", "[cli][prompt_util][catch2]") { MockStdin input("y\nhello\n2\n"); SuppressCout suppress; // First prompt: yes/no bool yes = prompt_yes_no("Continue?"); CHECK(yes); // Second prompt: input std::string text = prompt_input("Enter text:"); CHECK(text == "hello"); // Third prompt: choice std::vector items = {{"a", "A", ""}, {"b", "B", ""}}; size_t choice = prompt_choice("Select:", items); CHECK(choice == 1); }