diff --git a/CMakeLists.txt b/CMakeLists.txt index cb3b43a..d0fe9ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set(HEADER_FILES misc/traits.h tests/testutils.h tests/cppunit.h + tests/outputcheck.h ) set(SRC_FILES application/argumentparser.cpp diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index 015bf25..94e330c 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -1,3 +1,4 @@ +#include "./outputcheck.h" #include "./testutils.h" #include "../conversion/stringbuilder.h" @@ -21,81 +22,11 @@ using namespace std; using namespace ApplicationUtilities; using namespace ConversionUtilities; +using namespace TestUtilities; using namespace TestUtilities::Literals; using namespace CPPUNIT_NS; -/*! - * \brief The StandardOutputCheck class asserts whether the standard output written in the enclosing code block matches the expected output. - * \remarks Only works for output printed via std::cout. - * \todo Could be generalized and moved to testutils.h. - */ -class StandardOutputCheck { -public: - StandardOutputCheck(const string &expectedOutput); - StandardOutputCheck(string &&expectedOutput, string &&alternativeOutput); - StandardOutputCheck(function &&customCheck); - ~StandardOutputCheck(); - -private: - const function m_customCheck; - const string m_expectedOutput; - const string m_alternativeOutput; - stringstream m_buffer; - streambuf *const m_regularCoutBuffer; -}; - -/*! - * \brief Redirects standard output to an internal buffer. - */ -StandardOutputCheck::StandardOutputCheck(const string &expectedOutput) - : m_expectedOutput(expectedOutput) - , m_buffer() - , m_regularCoutBuffer(cout.rdbuf(m_buffer.rdbuf())) -{ -} - -/*! - * \brief Redirects standard output to an internal buffer. - */ -StandardOutputCheck::StandardOutputCheck(string &&expectedOutput, string &&alternativeOutput) - : m_expectedOutput(expectedOutput) - , m_alternativeOutput(alternativeOutput) - , m_buffer() - , m_regularCoutBuffer(cout.rdbuf(m_buffer.rdbuf())) -{ -} - -/*! - * \brief Redirects standard output to an internal buffer. - */ -StandardOutputCheck::StandardOutputCheck(function &&customCheck) - : m_customCheck(customCheck) - , m_buffer() - , m_regularCoutBuffer(cout.rdbuf(m_buffer.rdbuf())) -{ -} - -/*! - * \brief Asserts the buffered standard output and restores the regular behaviour of std::cout. - */ -StandardOutputCheck::~StandardOutputCheck() -{ - cout.rdbuf(m_regularCoutBuffer); - if (m_customCheck) { - m_customCheck(m_buffer.str()); - return; - } - if (m_alternativeOutput.empty()) { - CPPUNIT_ASSERT_EQUAL(m_expectedOutput, m_buffer.str()); - return; - } - const string actualOutput(m_buffer.str()); - if (m_expectedOutput != actualOutput && m_alternativeOutput != actualOutput) { - CPPUNIT_FAIL("Output is not either \"" % m_expectedOutput % "\" or \"" % m_alternativeOutput % "\". Got instead:\n" + actualOutput); - } -} - /*! * \brief The ArgumentParserTests class tests the ArgumentParser and Argument classes. */ @@ -518,7 +449,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv1[] = { "se" }; ArgumentReader reader(parser, argv1, argv1 + 1, true); { - const StandardOutputCheck c("COMPREPLY=()\n"); + const OutputCheck c("COMPREPLY=()\n"); reader.read(); parser.printBashCompletion(1, argv1, 0, reader); } @@ -527,7 +458,7 @@ void ArgumentParserTests::testBashCompletion() getArg.setDenotesOperation(true); setArg.setDenotesOperation(true); { - const StandardOutputCheck c("COMPREPLY=('set' )\n"); + const OutputCheck c("COMPREPLY=('set' )\n"); reader.reset(argv1, argv1 + 1).read(); parser.printBashCompletion(1, argv1, 0, reader); } @@ -536,7 +467,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv2[] = { "set" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('set' )\n"); + const OutputCheck c("COMPREPLY=('set' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 0, reader); } @@ -544,7 +475,7 @@ void ArgumentParserTests::testBashCompletion() // advance the cursor position -> the completion should propose the next argument parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n"); + const OutputCheck c("COMPREPLY=('--files' '--values' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 1, reader); } @@ -553,7 +484,7 @@ void ArgumentParserTests::testBashCompletion() parser.resetArgs(); filesArg.setDenotesOperation(true); { - const StandardOutputCheck c("COMPREPLY=('files' '--values' )\n"); + const OutputCheck c("COMPREPLY=('files' '--values' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 1, reader); } @@ -562,7 +493,7 @@ void ArgumentParserTests::testBashCompletion() parser.resetArgs(); filesArg.setDenotesOperation(false); { - const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); + const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); reader.reset(nullptr, nullptr).read(); parser.printBashCompletion(0, nullptr, 0, reader); } @@ -571,7 +502,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv3[] = { "get", "--fields" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n"); + const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n"); reader.reset(argv3, argv3 + 2).read(); parser.printBashCompletion(2, argv3, 2, reader); } @@ -580,7 +511,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv4[] = { "set", "--values", "a" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n"); + const OutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n"); reader.reset(argv4, argv4 + 3).read(); parser.printBashCompletion(3, argv4, 2, reader); } @@ -588,7 +519,7 @@ void ArgumentParserTests::testBashCompletion() // pre-defined values for implicit argument parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n"); + const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n"); reader.reset(argv3, argv3 + 1).read(); parser.printBashCompletion(1, argv3, 2, reader); } @@ -602,8 +533,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv5[] = { "get", "--files", iniFilePath.c_str() }; { // order for file names is not specified - const StandardOutputCheck c( - "COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n", + const OutputCheck c("COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n", "COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n"); reader.reset(argv5, argv5 + 3).read(); parser.printBashCompletion(3, argv5, 2, reader); @@ -613,7 +543,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv6[] = { "set", "--" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n"); + const OutputCheck c("COMPREPLY=('--files' '--values' )\n"); reader.reset(argv6, argv6 + 2).read(); parser.printBashCompletion(2, argv6, 1, reader); } @@ -622,7 +552,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv7[] = { "-i", "--sub", "--" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n"); + const OutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n"); reader.reset(argv7, argv7 + 3).read(); parser.printBashCompletion(3, argv7, 2, reader); } @@ -631,7 +561,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv8[] = { "set", "--values", "t" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n"); + const OutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n"); reader.reset(argv8, argv8 + 3).read(); parser.printBashCompletion(3, argv8, 2, reader); } @@ -640,13 +570,13 @@ void ArgumentParserTests::testBashCompletion() const char *const argv9[] = { "-gf" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('-gf' )\n"); + const OutputCheck c("COMPREPLY=('-gf' )\n"); reader.reset(argv9, argv9 + 1).read(); parser.printBashCompletion(1, argv9, 0, reader); } parser.resetArgs(); { - const StandardOutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); }); + const OutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); }); reader.reset(argv9, argv9 + 1).read(); parser.printBashCompletion(1, argv9, 1, reader); } @@ -658,7 +588,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); + const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); parser.readArgs(3, argv10); } CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable())); @@ -668,7 +598,7 @@ void ArgumentParserTests::testBashCompletion() const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" }; parser.resetArgs(); { - const StandardOutputCheck c("COMPREPLY=('get' )\n"); + const OutputCheck c("COMPREPLY=('get' )\n"); parser.readArgs(3, argv11); } } @@ -711,31 +641,31 @@ void ArgumentParserTests::testHelp() // parse args and assert output const char *const argv[] = { "app", "-h" }; { - const StandardOutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n" - "\e[0mLinked against: somelib, some other lib\n" - "\n\e[0m" - "Available arguments:\n" - "\e[1m--help, -h\e[0m\n" - " shows this information\n" - " particularities: mandatory\n" - "\n" - "\e[1mverbose, -v\e[0m\n" - " be verbose\n" - " example: actually not an operation\n" - "\n" - "\e[1m--files, -f\e[0m\n" - " specifies the path of the file(s) to be opened\n" - " \e[1m--sub\e[0m\n" - " sub arg\n" - " particularities: mandatory if parent argument is present\n" - " \e[1m--nested-sub\e[0m [value1] [value2] ...\n" - " nested sub arg\n" - "\n" - "\e[1m--env\e[0m [file] [value 2]\n" - " env\n" - " default environment variable: FILES\n" - "\n" - "Project website: " APP_URL "\n"); + const OutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n" + "\e[0mLinked against: somelib, some other lib\n" + "\n\e[0m" + "Available arguments:\n" + "\e[1m--help, -h\e[0m\n" + " shows this information\n" + " particularities: mandatory\n" + "\n" + "\e[1mverbose, -v\e[0m\n" + " be verbose\n" + " example: actually not an operation\n" + "\n" + "\e[1m--files, -f\e[0m\n" + " specifies the path of the file(s) to be opened\n" + " \e[1m--sub\e[0m\n" + " sub arg\n" + " particularities: mandatory if parent argument is present\n" + " \e[1m--nested-sub\e[0m [value1] [value2] ...\n" + " nested sub arg\n" + "\n" + "\e[1m--env\e[0m [file] [value 2]\n" + " env\n" + " default environment variable: FILES\n" + "\n" + "Project website: " APP_URL "\n"); parser.parseArgs(2, argv); } } diff --git a/tests/outputcheck.h b/tests/outputcheck.h new file mode 100644 index 0000000..1118dc9 --- /dev/null +++ b/tests/outputcheck.h @@ -0,0 +1,92 @@ +#ifndef TESTUTILS_OUTPUTCHECK_H +#define TESTUTILS_OUTPUTCHECK_H + +#include "../conversion/stringbuilder.h" + +#include + +#include +#include +#include +#include + +namespace TestUtilities { + +/*! + * \brief The StandardOutputCheck class asserts whether the (standard) output written in the enclosing code block + * matches the expected output. + */ +class OutputCheck { +public: + OutputCheck(const std::string &expectedOutput, std::ostream &os = std::cout); + OutputCheck(std::string &&expectedOutput, std::string &&alternativeOutput, std::ostream &os = std::cout); + OutputCheck(std::function &&customCheck, std::ostream &os = std::cout); + ~OutputCheck(); + +private: + std::ostream &m_os; + const std::function m_customCheck; + const std::string m_expectedOutput; + const std::string m_alternativeOutput; + std::stringstream m_buffer; + std::streambuf *const m_regularOutputBuffer; +}; + +/*! + * \brief Redirects standard output to an internal buffer. + */ +inline OutputCheck::OutputCheck(const std::string &expectedOutput, std::ostream &os) + : m_os(os) + , m_expectedOutput(expectedOutput) + , m_buffer() + , m_regularOutputBuffer(os.rdbuf(m_buffer.rdbuf())) +{ +} + +/*! + * \brief Redirects standard output to an internal buffer. + */ +inline OutputCheck::OutputCheck(std::string &&expectedOutput, std::string &&alternativeOutput, std::ostream &os) + : m_os(os) + , m_expectedOutput(expectedOutput) + , m_alternativeOutput(alternativeOutput) + , m_buffer() + , m_regularOutputBuffer(os.rdbuf(m_buffer.rdbuf())) +{ +} + +/*! + * \brief Redirects standard output to an internal buffer. + */ +inline OutputCheck::OutputCheck(std::function &&customCheck, std::ostream &os) + : m_os(os) + , m_customCheck(customCheck) + , m_buffer() + , m_regularOutputBuffer(os.rdbuf(m_buffer.rdbuf())) +{ +} + +/*! + * \brief Asserts the buffered standard output and restores the regular behaviour of std::cout. + */ +inline OutputCheck::~OutputCheck() +{ + m_os.rdbuf(m_regularOutputBuffer); + if (m_customCheck) { + m_customCheck(m_buffer.str()); + return; + } + if (m_alternativeOutput.empty()) { + CPPUNIT_ASSERT_EQUAL(m_expectedOutput, m_buffer.str()); + return; + } + const std::string actualOutput(m_buffer.str()); + if (m_expectedOutput != actualOutput && m_alternativeOutput != actualOutput) { + using namespace ConversionUtilities; + CPPUNIT_FAIL("Output is not either \"" % m_expectedOutput % "\" or \"" % m_alternativeOutput % "\". Got instead:\n" + actualOutput); + } +} + +} // namespace TestUtilities + +#endif // TESTUTILS_OUTPUTCHECK_H