diff --git a/src/utils/Console.cpp b/src/utils/Console.cpp new file mode 100644 index 00000000..3753296a --- /dev/null +++ b/src/utils/Console.cpp @@ -0,0 +1,53 @@ +#include "Console.h" + +#include +#include + +using std::string; +using std::vector; +using std::ostream; +using std::istream; +using std::flush; +using std::getline; +using boost::optional; +using boost::none; + +Console::Console(): Console(std::cout, std::cin) { +} + +Console::Console(ostream &output, istream &input): _output(output), _input(input) { +} + +optional parseInt(const string &str) { + try { + string trimmed = str; + boost::algorithm::trim(trimmed); + int parsed = std::stoi(str); + if (std::to_string(parsed) != trimmed) { + return none; + } + return parsed; + } catch (const std::invalid_argument &e) { + return none; + } catch (const std::out_of_range &e) { + return none; + } +} + +unsigned int Console::ask(const string &question, const vector &options) { + if(options.size() == 0) { + throw std::invalid_argument("options should have at least one entry"); + } + _output << question << "\n"; + for (unsigned int i = 0; i < options.size(); ++i) { + _output << " [" << (i+1) << "] " << options[i] << "\n"; + } + optional choice; + do { + _output << "Your choice [1-" << options.size() << "]: " << flush; + string choiceStr; + getline(_input, choiceStr); + choice = parseInt(choiceStr); + } while(choice == none || *choice < 1 || static_cast(*choice) > options.size()); + return *choice-1; +} diff --git a/src/utils/Console.h b/src/utils/Console.h new file mode 100644 index 00000000..6d56710b --- /dev/null +++ b/src/utils/Console.h @@ -0,0 +1,22 @@ +#pragma once +#ifndef CRYFS_CONSOLE_H +#define CRYFS_CONSOLE_H + +#include +#include +#include + +//TODO Add test cases + +class Console { +public: + Console(); + Console(std::ostream &output, std::istream &input); + unsigned int ask(const std::string &question, const std::vector &options); +private: + std::ostream &_output; + std::istream &_input; +}; + + +#endif diff --git a/test/utils/ConsoleTest.cpp b/test/utils/ConsoleTest.cpp new file mode 100644 index 00000000..0369ea77 --- /dev/null +++ b/test/utils/ConsoleTest.cpp @@ -0,0 +1,241 @@ +#include + +#include "../../src/utils/Console.h" + +#include +#include +#include + +using std::stringstream; +using std::string; +using std::vector; +using std::istream; +using std::ostream; +using std::future; +using std::initializer_list; + +class ConsoleThread { +public: + ConsoleThread(ostream &ostr, istream &istr): _console(ostr, istr) {} + future ask(const string &question, const vector &options) { + return std::async(std::launch::async, [this, question, options]() { + return _console.ask(question, options); + }); + } +private: + Console _console; +}; + +class ConsoleTest: public ::testing::Test { +public: + ConsoleTest(): _inputStr(), _outputStr(), _input(&_inputStr), _output(&_outputStr), _console(_output, _input) {} + + void EXPECT_OUTPUT_LINES(initializer_list lines) { + for (const string &line : lines) { + EXPECT_OUTPUT_LINE(line); + } + } + + void EXPECT_OUTPUT_LINE(const string &expected, char delimiter = '\n', const string &expected_after_delimiter = "") { + string actual; + std::getline(_output, actual, delimiter); + EXPECT_EQ(expected, actual); + for (char expected_char : expected_after_delimiter) { + char actual_char; + _output.get(actual_char); + EXPECT_EQ(expected_char, actual_char); + } + } + + void sendInputLine(const string &line) { + _input << line << "\n" << std::flush; + } + + future ask(const string &question, const vector &options) { + return _console.ask(question, options); + } + +private: + cpputils::pipestream _inputStr; + cpputils::pipestream _outputStr; + std::iostream _input; + std::iostream _output; + ConsoleThread _console; +}; + +TEST_F(ConsoleTest, AskCrashesWithoutOptions) { + EXPECT_THROW( + (ask("My Question?", {}).get()), + std::invalid_argument + ); +} + +TEST_F(ConsoleTest, AskOneOption) { + auto chosen = ask("My Question?", {"First Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-1]", ':', " "); + sendInputLine("1"); + EXPECT_EQ(0, chosen.get()); +} + +TEST_F(ConsoleTest, AskTwoOptions_ChooseFirst) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("1"); + EXPECT_EQ(0, chosen.get()); +} + +TEST_F(ConsoleTest, AskTwoOptions_ChooseSecond) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("2"); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, AskThreeOptions_ChooseFirst) { + auto chosen = ask("My Other Question?", {"1st Option", "2nd Option", "3rd Option"}); + EXPECT_OUTPUT_LINES({ + "My Other Question?", + " [1] 1st Option", + " [2] 2nd Option", + " [3] 3rd Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-3]", ':', " "); + sendInputLine("1"); + EXPECT_EQ(0, chosen.get()); +} + +TEST_F(ConsoleTest, AskThreeOptions_ChooseSecond) { + auto chosen = ask("My Question?", {"1st Option", "2nd Option", "3rd Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] 1st Option", + " [2] 2nd Option", + " [3] 3rd Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-3]", ':', " "); + sendInputLine("2"); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, AskThreeOptions_ChooseThird) { + auto chosen = ask("My Question?", {"1st Option", "2nd Option", "3rd Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] 1st Option", + " [2] 2nd Option", + " [3] 3rd Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-3]", ':', " "); + sendInputLine("3"); + EXPECT_EQ(2, chosen.get()); +} + +TEST_F(ConsoleTest, InputWithLeadingSpaces) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine(" 2"); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, InputWithFollowingSpaces) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("2 "); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, InputWithLeadingAndFollowingSpaces) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine(" 2 "); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, InputEmptyLine) { + auto chosen = ask("My Question?", {"First Option", "Second Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] First Option", + " [2] Second Option" + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine(""); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine(" "); // empty line with space + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("2"); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, InputWrongNumbers) { + auto chosen = ask("My Question?", {"1st Option", "2nd Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] 1st Option", + " [2] 2nd Option", + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("0"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("-1"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("3"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("1.5"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("1,5"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("2"); + EXPECT_EQ(1, chosen.get()); +} + +TEST_F(ConsoleTest, InputNonNumbers) { + auto chosen = ask("My Question?", {"1st Option", "2nd Option"}); + EXPECT_OUTPUT_LINES({ + "My Question?", + " [1] 1st Option", + " [2] 2nd Option", + }); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("abc"); + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("3a"); // Wrong number and string attached + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("1a"); // Right number but string attached + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("a3"); // Wrong number and string attached + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("a1"); // Right number but string attached + EXPECT_OUTPUT_LINE("Your choice [1-2]", ':', " "); + sendInputLine("2"); + EXPECT_EQ(1, chosen.get()); +} \ No newline at end of file