Improve argument parser tests

This commit is contained in:
Martchus 2017-07-28 19:27:56 +02:00
parent d7cf4312bf
commit f4faf652fd
1 changed files with 218 additions and 178 deletions

View File

@ -21,9 +21,81 @@
using namespace std; using namespace std;
using namespace ApplicationUtilities; using namespace ApplicationUtilities;
using namespace ConversionUtilities; using namespace ConversionUtilities;
using namespace TestUtilities::Literals;
using namespace CPPUNIT_NS; 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<void(const string &output)> &&customCheck);
~StandardOutputCheck();
private:
const function<void(const string &output)> 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<void(const string &)> &&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. * \brief The ArgumentParserTests class tests the ArgumentParser and Argument classes.
*/ */
@ -402,10 +474,14 @@ void ArgumentParserTests::testCallbacks()
parser.parseArgs(4, argv2); parser.parseArgs(4, argv2);
} }
/*!
* \brief Used to check whether the exit() function is called when printing bash completion.
*/
static bool exitCalled = false;
/*! /*!
* \brief Tests bash completion. * \brief Tests bash completion.
* \remarks This tests makes assumptions about the order and the exact output format * \remarks This tests makes assumptions about the order and the exact output format.
* which should be improved.
*/ */
void ArgumentParserTests::testBashCompletion() void ArgumentParserTests::testBashCompletion()
{ {
@ -414,7 +490,7 @@ void ArgumentParserTests::testBashCompletion()
Argument verboseArg("verbose", 'v', "be verbose"); Argument verboseArg("verbose", 'v', "be verbose");
verboseArg.setCombinable(true); verboseArg.setCombinable(true);
Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened"); Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
filesArg.setRequiredValueCount(-1); filesArg.setRequiredValueCount(Argument::varValueCount);
filesArg.setCombinable(true); filesArg.setCombinable(true);
Argument nestedSubArg("nested-sub", '\0', "nested sub arg"); Argument nestedSubArg("nested-sub", '\0', "nested sub arg");
Argument subArg("sub", '\0', "sub arg"); Argument subArg("sub", '\0', "sub arg");
@ -423,11 +499,11 @@ void ArgumentParserTests::testBashCompletion()
displayFileInfoArg.setDenotesOperation(true); displayFileInfoArg.setDenotesOperation(true);
displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg }); displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg });
Argument fieldsArg("fields", '\0', "specifies the fields"); Argument fieldsArg("fields", '\0', "specifies the fields");
fieldsArg.setRequiredValueCount(-1); fieldsArg.setRequiredValueCount(Argument::varValueCount);
fieldsArg.setPreDefinedCompletionValues("title album artist trackpos"); fieldsArg.setPreDefinedCompletionValues("title album artist trackpos");
fieldsArg.setImplicit(true); fieldsArg.setImplicit(true);
Argument valuesArg("values", '\0', "specifies the fields"); Argument valuesArg("values", '\0', "specifies the fields");
valuesArg.setRequiredValueCount(-1); valuesArg.setRequiredValueCount(Argument::varValueCount);
valuesArg.setPreDefinedCompletionValues("title album artist trackpos"); valuesArg.setPreDefinedCompletionValues("title album artist trackpos");
valuesArg.setImplicit(false); valuesArg.setImplicit(false);
valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign); valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
@ -438,192 +514,162 @@ void ArgumentParserTests::testBashCompletion()
parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg }); parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg });
// redirect cout to custom buffer // fail due to operation flags not set
stringstream buffer; const char *const argv1[] = { "se" };
streambuf *regularCoutBuffer = cout.rdbuf(buffer.rdbuf()); ArgumentReader reader(parser, argv1, argv1 + 1, true);
{
try { const StandardOutputCheck c("COMPREPLY=()\n");
// fail due to operation flags not set
const char *const argv1[] = { "se" };
ArgumentReader reader(parser, argv1, argv1 + 1, true);
reader.read(); reader.read();
parser.printBashCompletion(1, argv1, 0, reader); parser.printBashCompletion(1, argv1, 0, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=()\n"s, buffer.str());
// correct operation arg flags // correct operation arg flags
buffer.str(string()); getArg.setDenotesOperation(true);
cout.rdbuf(buffer.rdbuf()); setArg.setDenotesOperation(true);
getArg.setDenotesOperation(true), setArg.setDenotesOperation(true); {
const StandardOutputCheck c("COMPREPLY=('set' )\n");
reader.reset(argv1, argv1 + 1).read(); reader.reset(argv1, argv1 + 1).read();
parser.printBashCompletion(1, argv1, 0, reader); parser.printBashCompletion(1, argv1, 0, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('set' )\n"s, buffer.str());
// argument at current cursor position already specified -> the completion should just return the argument // argument at current cursor position already specified -> the completion should just return the argument
const char *const argv2[] = { "set" }; const char *const argv2[] = { "set" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('set' )\n");
reader.reset(argv2, argv2 + 1).read(); reader.reset(argv2, argv2 + 1).read();
parser.printBashCompletion(1, argv2, 0, reader); parser.printBashCompletion(1, argv2, 0, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('set' )\n"s, buffer.str());
// advance the cursor position -> the completion should propose the next argument // advance the cursor position -> the completion should propose the next argument
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n");
reader.reset(argv2, argv2 + 1).read(); reader.reset(argv2, argv2 + 1).read();
parser.printBashCompletion(1, argv2, 1, reader); parser.printBashCompletion(1, argv2, 1, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('--files' '--values' )\n"s, buffer.str());
// nested operations should be proposed as operations // nested operations should be proposed as operations
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); filesArg.setDenotesOperation(true);
parser.resetArgs(); {
filesArg.setDenotesOperation(true); const StandardOutputCheck c("COMPREPLY=('files' '--values' )\n");
reader.reset(argv2, argv2 + 1).read(); reader.reset(argv2, argv2 + 1).read();
parser.printBashCompletion(1, argv2, 1, reader); parser.printBashCompletion(1, argv2, 1, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('files' '--values' )\n"s, buffer.str());
// specifying no args should propose all main arguments // specifying no args should propose all main arguments
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); filesArg.setDenotesOperation(false);
parser.resetArgs(); {
filesArg.setDenotesOperation(false); const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
reader.reset(nullptr, nullptr).read(); reader.reset(nullptr, nullptr).read();
parser.printBashCompletion(0, nullptr, 0, reader); parser.printBashCompletion(0, nullptr, 0, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"s, buffer.str());
// pre-defined values // pre-defined values
const char *const argv3[] = { "get", "--fields" }; const char *const argv3[] = { "get", "--fields" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n");
reader.reset(argv3, argv3 + 2).read(); reader.reset(argv3, argv3 + 2).read();
parser.printBashCompletion(2, argv3, 2, reader); parser.printBashCompletion(2, argv3, 2, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n"s, buffer.str());
// pre-defined values with equation sign, one letter already present // pre-defined values with equation sign, one letter already present
const char *const argv4[] = { "set", "--values", "a" }; const char *const argv4[] = { "set", "--values", "a" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n");
reader.reset(argv4, argv4 + 3).read(); reader.reset(argv4, argv4 + 3).read();
parser.printBashCompletion(3, argv4, 2, reader); parser.printBashCompletion(3, argv4, 2, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n"s, buffer.str());
// pre-defined values for implicit argument // pre-defined values for implicit argument
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n");
reader.reset(argv3, argv3 + 1).read(); reader.reset(argv3, argv3 + 1).read();
parser.printBashCompletion(1, argv3, 2, reader); parser.printBashCompletion(1, argv3, 2, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n"s, buffer.str());
// file names // file names
string iniFilePath = TestUtilities::testFilePath("test.ini"); string iniFilePath = TestUtilities::testFilePath("test.ini");
iniFilePath.resize(iniFilePath.size() - 4); iniFilePath.resize(iniFilePath.size() - 4);
string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv"); string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv");
mkvFilePath.resize(mkvFilePath.size() - 17); mkvFilePath.resize(mkvFilePath.size() - 17);
TestUtilities::testFilePath("t.aac"); parser.resetArgs();
const char *const argv5[] = { "get", "--files", iniFilePath.c_str() }; const char *const argv5[] = { "get", "--files", iniFilePath.c_str() };
buffer.str(string()); {
cout.rdbuf(buffer.rdbuf()); // order for file names is not specified
parser.resetArgs(); const StandardOutputCheck 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(); reader.reset(argv5, argv5 + 3).read();
parser.printBashCompletion(3, argv5, 2, reader); parser.printBashCompletion(3, argv5, 2, reader);
cout.rdbuf(regularCoutBuffer); }
// order for file names is not specified
const string res(buffer.str());
if (res.find(".mkv") < res.find(".ini")) {
CPPUNIT_ASSERT_EQUAL(
"COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n", buffer.str());
} else {
CPPUNIT_ASSERT_EQUAL(
"COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n", buffer.str());
}
// sub arguments // sub arguments
const char *const argv6[] = { "set", "--" }; const char *const argv6[] = { "set", "--" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n");
reader.reset(argv6, argv6 + 2).read(); reader.reset(argv6, argv6 + 2).read();
parser.printBashCompletion(2, argv6, 1, reader); parser.printBashCompletion(2, argv6, 1, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('--files' '--values' )\n"s, buffer.str());
// nested sub arguments // nested sub arguments
const char *const argv7[] = { "-i", "--sub", "--" }; const char *const argv7[] = { "-i", "--sub", "--" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n");
reader.reset(argv7, argv7 + 3).read(); reader.reset(argv7, argv7 + 3).read();
parser.printBashCompletion(3, argv7, 2, reader); parser.printBashCompletion(3, argv7, 2, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n"s, buffer.str());
// started pre-defined values with equation sign, one letter already present, last value matches // started pre-defined values with equation sign, one letter already present, last value matches
const char *const argv8[] = { "set", "--values", "t" }; const char *const argv8[] = { "set", "--values", "t" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n");
reader.reset(argv8, argv8 + 3).read(); reader.reset(argv8, argv8 + 3).read();
parser.printBashCompletion(3, argv8, 2, reader); parser.printBashCompletion(3, argv8, 2, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n"s, buffer.str());
// combined abbreviations // combined abbreviations
const char *const argv9[] = { "-gf" }; const char *const argv9[] = { "-gf" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('-gf' )\n");
reader.reset(argv9, argv9 + 1).read(); reader.reset(argv9, argv9 + 1).read();
parser.printBashCompletion(1, argv9, 0, reader); parser.printBashCompletion(1, argv9, 0, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('-gf' )\n"s, buffer.str()); parser.resetArgs();
{
buffer.str(string()); const StandardOutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); });
cout.rdbuf(buffer.rdbuf());
parser.resetArgs();
reader.reset(argv9, argv9 + 1).read(); reader.reset(argv9, argv9 + 1).read();
parser.printBashCompletion(1, argv9, 1, reader); parser.printBashCompletion(1, argv9, 1, reader);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT_EQUAL(static_cast<string::size_type>(0), buffer.str().find("COMPREPLY=('--fields' "));
// override exit function to prevent readArgs() from terminating the test run // override exit function to prevent readArgs() from terminating the test run
exitFunction = [](int) {}; exitFunction = [](int) { exitCalled = true; };
// call completion via readArgs() with current word index // call completion via readArgs() with current word index
const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" }; const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
parser.readArgs(3, argv10); parser.readArgs(3, argv10);
cout.rdbuf(regularCoutBuffer); }
CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable())); CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable()));
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"s, buffer.str()); CPPUNIT_ASSERT(exitCalled);
// call completion via readArgs() without current word index // call completion via readArgs() without current word index
const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" }; const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" };
buffer.str(string()); parser.resetArgs();
cout.rdbuf(buffer.rdbuf()); {
parser.resetArgs(); const StandardOutputCheck c("COMPREPLY=('get' )\n");
parser.readArgs(3, argv11); parser.readArgs(3, argv11);
cout.rdbuf(regularCoutBuffer);
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('get' )\n"s, buffer.str());
} catch (...) {
cout.rdbuf(regularCoutBuffer);
throw;
} }
} }
@ -637,10 +683,6 @@ void ArgumentParserTests::testHelp()
indent = indent + 3; indent = indent + 3;
CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(4 + 3), indent.level); CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(4 + 3), indent.level);
// redirect cout to custom buffer
stringstream buffer;
streambuf *regularCoutBuffer = cout.rdbuf(buffer.rdbuf());
// setup parser // setup parser
ArgumentParser parser; ArgumentParser parser;
HelpArgument helpArg(parser); HelpArgument helpArg(parser);
@ -665,38 +707,36 @@ void ArgumentParserTests::testHelp()
parser.addMainArgument(&filesArg); parser.addMainArgument(&filesArg);
parser.addMainArgument(&envArg); parser.addMainArgument(&envArg);
// parse args // parse args and assert output
const char *const argv[] = { "app", "-h" }; const char *const argv[] = { "app", "-h" };
buffer.str(string()); {
cout.rdbuf(buffer.rdbuf()); const StandardOutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n"
parser.parseArgs(2, argv); "\n\e[0m"
cout.rdbuf(regularCoutBuffer); "Available arguments:\n"
CPPUNIT_ASSERT_EQUAL("\e[1m" APP_NAME ", version " APP_VERSION "\n" "\e[1m--help, -h\e[0m\n"
"\n\e[0m" " shows this information\n"
"Available arguments:\n" " particularities: mandatory\n"
"\e[1m--help, -h\e[0m\n" "\n"
" shows this information\n" "\e[1m--verbose, -v\e[0m\n"
" particularities: mandatory\n" " be verbose\n"
"\n" " \n"
"\e[1m--verbose, -v\e[0m\n" "usage: actually not an operation\n"
" be verbose\n" "\n"
" \n" "\e[1m--files, -f\e[0m\n"
"usage: actually not an operation\n" " specifies the path of the file(s) to be opened\n"
"\n" " \e[1m--sub\e[0m\n"
"\e[1m--files, -f\e[0m\n" " sub arg\n"
" specifies the path of the file(s) to be opened\n" " particularities: mandatory if parent argument is present\n"
" \e[1m--sub\e[0m\n" " \e[1m--nested-sub\e[0m [value1] [value2] ...\n"
" sub arg\n" " nested sub arg\n"
" particularities: mandatory if parent argument is present\n" "\n"
" \e[1m--nested-sub\e[0m [value1] [value2] ...\n" "\e[1m--env\e[0m [file] [value 2]\n"
" nested sub arg\n" " env\n"
"\n" " default environment variable: FILES\n"
"\e[1m--env\e[0m [file] [value 2]\n" "\n"
" env\n" "Project website: " APP_URL "\n");
" default environment variable: FILES\n" parser.parseArgs(2, argv);
"\n" }
"Project website: " APP_URL "\n"s,
buffer.str());
} }
/*! /*!