Added Console class

This commit is contained in:
Sebastian Messmer 2015-09-12 20:07:44 +02:00
parent 69a413bf4b
commit 67f0f39b50
8 changed files with 527 additions and 0 deletions

99
io/Console.cpp Normal file
View File

@ -0,0 +1,99 @@
#include "Console.h"
#include <boost/optional.hpp>
#include <boost/algorithm/string/trim.hpp>
using std::string;
using std::vector;
using std::ostream;
using std::istream;
using std::flush;
using std::getline;
using boost::optional;
using boost::none;
using std::function;
using namespace cpputils;
IOStreamConsole::IOStreamConsole(): IOStreamConsole(std::cout, std::cin) {
}
IOStreamConsole::IOStreamConsole(ostream &output, istream &input): _output(output), _input(input) {
}
optional<int> 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;
}
}
function<optional<unsigned int>(const std::string &input)> parseUIntWithMinMax(unsigned int min, unsigned int max) {
return [min, max] (const string &input) {
optional<int> parsed = parseInt(input);
if (parsed == none) {
return optional<unsigned int>(none);
}
unsigned int value = static_cast<unsigned int>(*parsed);
if (value < min || value > max) {
return optional<unsigned int>(none);
}
return optional<unsigned int>(value);
};
}
template<typename Return>
Return IOStreamConsole::_askForChoice(const string &question, function<optional<Return> (const string&)> parse) {
optional<Return> choice = none;
do {
_output << question << flush;
string choiceStr;
getline(_input, choiceStr);
choice = parse(choiceStr);
} while(choice == none);
return *choice;
}
unsigned int IOStreamConsole::ask(const string &question, const vector<string> &options) {
if(options.size() == 0) {
throw std::invalid_argument("options should have at least one entry");
}
_output << "\n" << question << "\n";
for (unsigned int i = 0; i < options.size(); ++i) {
_output << " [" << (i+1) << "] " << options[i] << "\n";
}
int choice = _askForChoice("Your choice [1-" + std::to_string(options.size()) + "]: ", parseUIntWithMinMax(1, options.size()));
return choice-1;
}
function<optional<bool>(const string &input)> parseYesNo() {
return [] (const string &input) {
string trimmed = input;
boost::algorithm::trim(trimmed);
if(trimmed == "Y" || trimmed == "y" || trimmed == "Yes" || trimmed == "yes") {
return optional<bool>(true);
} else if (trimmed == "N" || trimmed == "n" || trimmed == "No" || trimmed == "no") {
return optional<bool>(false);
} else {
return optional<bool>(none);
}
};
}
bool IOStreamConsole::askYesNo(const string &question) {
_output << "\n" << question << "\n";
return _askForChoice("Your choice [y/n]: ", parseYesNo());
}
void IOStreamConsole::print(const std::string &output) {
_output << output << std::flush;
}

37
io/Console.h Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#ifndef MESSMER_CPPUTILS_IO_CONSOLE_H
#define MESSMER_CPPUTILS_IO_CONSOLE_H
#include <string>
#include <vector>
#include <iostream>
#include <boost/optional.hpp>
namespace cpputils {
class Console {
public:
virtual unsigned int ask(const std::string &question, const std::vector<std::string> &options) = 0;
virtual bool askYesNo(const std::string &question) = 0;
virtual void print(const std::string &output) = 0;
};
class IOStreamConsole: public Console {
public:
IOStreamConsole();
IOStreamConsole(std::ostream &output, std::istream &input);
unsigned int ask(const std::string &question, const std::vector<std::string> &options) override;
bool askYesNo(const std::string &question) override;
void print(const std::string &output) override;
private:
template<typename Return>
Return _askForChoice(const std::string &question, std::function<boost::optional<Return> (const std::string&)> parse);
std::ostream &_output;
std::istream &_input;
};
}
#endif

77
test/io/ConsoleTest.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef MESSMER_CPPUTILS_TEST_IO_CONSOLETEST_H
#define MESSMER_CPPUTILS_TEST_IO_CONSOLETEST_H
#include <google/gtest/gtest.h>
#include "../../io/Console.h"
#include <future>
#include <thread>
#include "../../io/pipestream.h"
class ConsoleThread {
public:
ConsoleThread(std::ostream &ostr, std::istream &istr): _console(ostr, istr) {}
std::future<unsigned int> ask(const std::string &question, const std::vector<std::string> &options) {
return std::async(std::launch::async, [this, question, options]() {
return _console.ask(question, options);
});
}
std::future<bool> askYesNo(const std::string &question) {
return std::async(std::launch::async, [this, question]() {
return _console.askYesNo(question);
});
}
void print(const std::string &output) {
_console.print(output);
}
private:
cpputils::IOStreamConsole _console;
};
class ConsoleTest: public ::testing::Test {
public:
ConsoleTest(): _inputStr(), _outputStr(), _input(&_inputStr), _output(&_outputStr), _console(_output, _input) {}
void EXPECT_OUTPUT_LINES(std::initializer_list<std::string> lines) {
for (const std::string &line : lines) {
EXPECT_OUTPUT_LINE(line);
}
}
void EXPECT_OUTPUT_LINE(const std::string &expected, char delimiter = '\n', const std::string &expected_after_delimiter = "") {
std::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 std::string &line) {
_input << line << "\n" << std::flush;
}
std::future<unsigned int> ask(const std::string &question, const std::vector<std::string> &options) {
return _console.ask(question, options);
}
std::future<bool> askYesNo(const std::string &question) {
return _console.askYesNo(question);
}
void print(const std::string &output) {
_console.print(output);
}
private:
cpputils::pipestream _inputStr;
cpputils::pipestream _outputStr;
std::iostream _input;
std::iostream _output;
ConsoleThread _console;
};
#endif

200
test/io/ConsoleTest_Ask.cpp Normal file
View File

@ -0,0 +1,200 @@
#include "ConsoleTest.h"
using std::stringstream;
using std::string;
using std::vector;
using std::istream;
using std::ostream;
using std::future;
using std::initializer_list;
class ConsoleTest_Ask: public ConsoleTest {};
TEST_F(ConsoleTest_Ask, CrashesWithoutOptions) {
EXPECT_THROW(
(ask("My Question?", {}).get()),
std::invalid_argument
);
}
TEST_F(ConsoleTest_Ask, OneOption) {
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_Ask, TwoOptions_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_Ask, TwoOptions_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_Ask, ThreeOptions_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_Ask, ThreeOptions_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_Ask, ThreeOptions_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_Ask, 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_Ask, 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_Ask, 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_Ask, 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_Ask, 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_Ask, 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());
}

View File

@ -0,0 +1,108 @@
#include "ConsoleTest.h"
using std::string;
class ConsoleTest_AskYesNo: public ConsoleTest {
public:
void EXPECT_TRUE_ON_INPUT(const string &input) {
EXPECT_RESULT_ON_INPUT(true, input);
}
void EXPECT_FALSE_ON_INPUT(const string &input) {
EXPECT_RESULT_ON_INPUT(false, input);
}
void EXPECT_RESULT_ON_INPUT(const bool expected, const string &input) {
auto chosen = askYesNo("Are you sure blablub?");
EXPECT_OUTPUT_LINES({"", "Are you sure blablub?"});
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine(input);
EXPECT_EQ(expected, chosen.get());
}
};
TEST_F(ConsoleTest_AskYesNo, Input_Yes) {
EXPECT_TRUE_ON_INPUT("Yes");
}
TEST_F(ConsoleTest_AskYesNo, Input_yes) {
EXPECT_TRUE_ON_INPUT("yes");
}
TEST_F(ConsoleTest_AskYesNo, Input_Y) {
EXPECT_TRUE_ON_INPUT("Y");
}
TEST_F(ConsoleTest_AskYesNo, Input_y) {
EXPECT_TRUE_ON_INPUT("y");
}
TEST_F(ConsoleTest_AskYesNo, Input_No) {
EXPECT_FALSE_ON_INPUT("No");
}
TEST_F(ConsoleTest_AskYesNo, Input_no) {
EXPECT_FALSE_ON_INPUT("no");
}
TEST_F(ConsoleTest_AskYesNo, Input_N) {
EXPECT_FALSE_ON_INPUT("N");
}
TEST_F(ConsoleTest_AskYesNo, Input_n) {
EXPECT_FALSE_ON_INPUT("n");
}
TEST_F(ConsoleTest_AskYesNo, InputWithLeadingSpaces) {
EXPECT_TRUE_ON_INPUT(" y");
}
TEST_F(ConsoleTest_AskYesNo, InputWithFollowingSpaces) {
EXPECT_TRUE_ON_INPUT("y ");
}
TEST_F(ConsoleTest_AskYesNo, InputWithLeadingAndFollowingSpaces) {
EXPECT_TRUE_ON_INPUT(" y ");
}
TEST_F(ConsoleTest_AskYesNo, InputEmptyLine) {
auto chosen = askYesNo("My Question?");
EXPECT_OUTPUT_LINES({"", "My Question?"});
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine(" "); // empty line with space
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("y");
EXPECT_EQ(true, chosen.get());
}
TEST_F(ConsoleTest_AskYesNo, WrongInput) {
auto chosen = askYesNo("My Question?");
EXPECT_OUTPUT_LINES({"", "My Question?"});
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("0");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("1");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("bla");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("Y_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("y_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("N_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("n_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("Yes_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("yes_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("No_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("no_andsomethingelse");
EXPECT_OUTPUT_LINE("Your choice [y/n]", ':', " ");
sendInputLine("y");
EXPECT_EQ(true, chosen.get());
}

View File

@ -0,0 +1,6 @@
#include "ConsoleTest.h"
TEST_F(ConsoleTest, Print) {
print("Bla Blub");
EXPECT_OUTPUT_LINE("Bla Blu", 'b'); // 'b' is the delimiter for reading
}