#include "./testutils.h" #include "../conversion/stringbuilder.h" #include "../application/argumentparser.h" #include "../application/argumentparserprivate.h" #include "../application/commandlineutils.h" #include "../application/failure.h" #include "../application/fakeqtconfigarguments.h" #include "../io/path.h" #include "resources/config.h" #include #include #include #include using namespace std; using namespace ApplicationUtilities; using namespace ConversionUtilities; 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. */ class ArgumentParserTests : public TestFixture { CPPUNIT_TEST_SUITE(ArgumentParserTests); CPPUNIT_TEST(testArgument); CPPUNIT_TEST(testParsing); CPPUNIT_TEST(testCallbacks); CPPUNIT_TEST(testBashCompletion); CPPUNIT_TEST(testHelp); CPPUNIT_TEST(testSetMainArguments); CPPUNIT_TEST_SUITE_END(); public: void setUp(); void tearDown(); void testArgument(); void testParsing(); void testCallbacks(); void testBashCompletion(); void testHelp(); void testSetMainArguments(); private: void callback(); }; CPPUNIT_TEST_SUITE_REGISTRATION(ArgumentParserTests); void ArgumentParserTests::setUp() { } void ArgumentParserTests::tearDown() { } /*! * \brief Tests the behaviour of the argument class. */ void ArgumentParserTests::testArgument() { Argument argument("test", 't', "some description"); CPPUNIT_ASSERT_EQUAL(argument.isRequired(), false); argument.setConstraints(1, 10); CPPUNIT_ASSERT_EQUAL(argument.isRequired(), true); Argument subArg("sub", 's', "sub arg"); argument.addSubArgument(&subArg); CPPUNIT_ASSERT_EQUAL(subArg.parents().at(0), &argument); CPPUNIT_ASSERT(!subArg.conflictsWithArgument()); CPPUNIT_ASSERT(!argument.firstValue()); argument.setEnvironmentVariable("FOO_ENV_VAR"); setenv("FOO_ENV_VAR", "foo", true); CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "foo")); ArgumentOccurrence occurrence(0, vector(), nullptr); occurrence.values.emplace_back("bar"); argument.m_occurrences.emplace_back(move(occurrence)); CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "bar")); } /*! * \brief Tests parsing command line arguments. */ void ArgumentParserTests::testParsing() { // setup parser with some test argument definitions ArgumentParser parser; SET_APPLICATION_INFO; QT_CONFIG_ARGUMENTS qtConfigArgs; HelpArgument helpArg(parser); Argument verboseArg("verbose", 'v', "be verbose"); verboseArg.setCombinable(true); Argument fileArg("file", 'f', "specifies the path of the file to be opened"); fileArg.setValueNames({ "path" }); fileArg.setRequiredValueCount(1); fileArg.setEnvironmentVariable("PATH"); Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened"); filesArg.setValueNames({ "path 1", "path 2" }); filesArg.setRequiredValueCount(-1); Argument outputFileArg("output-file", 'o', "specifies the path of the output file"); outputFileArg.setValueNames({ "path" }); outputFileArg.setRequiredValueCount(1); outputFileArg.setRequired(true); outputFileArg.setCombinable(true); Argument printFieldNamesArg("print-field-names", '\0', "prints available field names"); Argument displayFileInfoArg("display-file-info", 'i', "displays general file information"); Argument notAlbumArg("album", 'a', "should not be confused with album value"); displayFileInfoArg.setDenotesOperation(true); displayFileInfoArg.setSubArguments({ &fileArg, &verboseArg, ¬AlbumArg }); Argument fieldsArg("fields", '\0', "specifies the fields"); fieldsArg.setRequiredValueCount(-1); fieldsArg.setValueNames({ "title", "album", "artist", "trackpos" }); fieldsArg.setImplicit(true); Argument displayTagInfoArg("get", 'p', "displays the values of all specified tag fields (displays all fields if none specified)"); displayTagInfoArg.setDenotesOperation(true); displayTagInfoArg.setSubArguments({ &fieldsArg, &filesArg, &verboseArg, ¬AlbumArg }); parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &helpArg }); // error about uncombinable arguments const char *argv[] = { "tageditor", "get", "album", "title", "diskpos", "-f", "somefile" }; // try to parse, this should fail try { parser.parseArgs(7, argv); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"files\" can not be combined with \"fields\".")); } // arguments read correctly after successful parse filesArg.setCombinable(true); parser.resetArgs(); parser.parseArgs(7, argv); // check results CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!strcmp(parser.executable(), "tageditor")); CPPUNIT_ASSERT(!verboseArg.isPresent()); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(fieldsArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos")); CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range); // skip empty args const char *argv2[] = { "tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile" }; // reparse the args parser.resetArgs(); parser.parseArgs(9, argv2); // check results again CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!verboseArg.isPresent()); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(fieldsArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(3), "")); CPPUNIT_ASSERT_THROW(fieldsArg.values().at(4), out_of_range); CPPUNIT_ASSERT(filesArg.isPresent()); CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile")); // error about unknown argument: forget get/-p const char *argv3[] = { "tageditor", "album", "title", "diskpos", "--files", "somefile" }; try { parser.resetArgs(); parser.parseArgs(6, argv3); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown."s, string(e.what())); } // warning about unknown argument parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn); // redirect stderr to check whether warnings are printed correctly stringstream buffer; streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf()); parser.resetArgs(); try { parser.parseArgs(6, argv3); } catch (...) { cerr.rdbuf(regularCerrBuffer); throw; } cerr.rdbuf(regularCerrBuffer); CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown and will be ignored.\n"s "The specified argument \"title\" is unknown and will be ignored.\n"s "The specified argument \"diskpos\" is unknown and will be ignored.\n"s "The specified argument \"--files\" is unknown and will be ignored.\n"s "The specified argument \"somefile\" is unknown and will be ignored.\n"s, buffer.str()); // none of the arguments should be present now CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(!fieldsArg.isPresent()); CPPUNIT_ASSERT(!filesArg.isPresent()); // combined abbreviations like "-vf" const char *argv4[] = { "tageditor", "-i", "-vf", "test" }; parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail); parser.resetArgs(); parser.parseArgs(4, argv4); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(verboseArg.isPresent()); CPPUNIT_ASSERT(!displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(!filesArg.isPresent()); CPPUNIT_ASSERT(fileArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fileArg.values().at(0), "test")); CPPUNIT_ASSERT_THROW(fileArg.values().at(1), out_of_range); // constraint checking: no multiple occurrences (not resetting verboseArg on purpose) displayFileInfoArg.reset(), fileArg.reset(); try { parser.parseArgs(4, argv4); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time.")); } // constraint checking: no contraint (not resetting verboseArg on purpose) displayFileInfoArg.reset(), fileArg.reset(); verboseArg.setConstraints(0, -1); parser.parseArgs(4, argv4); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); // constraint checking: mandatory argument verboseArg.setRequired(true); parser.resetArgs(); parser.parseArgs(4, argv4); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); // contraint checking: error about missing mandatory argument const char *argv5[] = { "tageditor", "-i", "-f", "test" }; displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); try { parser.parseArgs(4, argv5); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time.")); } verboseArg.setRequired(false); // combined abbreviation with nesting "-pf" const char *argv10[] = { "tageditor", "-pf", "test" }; parser.resetArgs(); parser.parseArgs(3, argv10); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!fileArg.isPresent()); CPPUNIT_ASSERT(filesArg.isPresent()); CPPUNIT_ASSERT_EQUAL(filesArg.values(0).size(), static_cast::size_type>(1)); CPPUNIT_ASSERT(!strcmp(filesArg.values(0).front(), "test")); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); // constraint checking: no complains about missing -i const char *argv6[] = { "tageditor", "-g" }; parser.resetArgs(); parser.parseArgs(2, argv6); CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent()); // constraint checking: dependend arguments (-f requires -i or -p) const char *argv7[] = { "tageditor", "-f", "test" }; parser.resetArgs(); try { parser.parseArgs(3, argv7); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown."s, string(e.what())); } // equation sign syntax const char *argv11[] = { "tageditor", "-if=test" }; parser.resetArgs(); parser.parseArgs(2, argv11); CPPUNIT_ASSERT(!filesArg.isPresent()); CPPUNIT_ASSERT(fileArg.isPresent()); CPPUNIT_ASSERT_EQUAL(fileArg.values(0).size(), static_cast::size_type>(1)); CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test")); // specifying value directly after abbreviation const char *argv12[] = { "tageditor", "-iftest" }; parser.resetArgs(); parser.parseArgs(2, argv12); CPPUNIT_ASSERT(!filesArg.isPresent()); CPPUNIT_ASSERT(fileArg.isPresent()); CPPUNIT_ASSERT_EQUAL(fileArg.values(0).size(), static_cast::size_type>(1)); CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test")); // default argument const char *argv8[] = { "tageditor" }; parser.resetArgs(); parser.parseArgs(1, argv8); CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!verboseArg.isPresent()); CPPUNIT_ASSERT(!displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(!filesArg.isPresent()); CPPUNIT_ASSERT(!fileArg.isPresent()); if (getenv("PATH")) { CPPUNIT_ASSERT(fileArg.firstValue()); CPPUNIT_ASSERT(!strcmp(fileArg.firstValue(), getenv("PATH"))); } else { CPPUNIT_ASSERT(!fileArg.firstValue()); } // constraint checking: required value count with sufficient number of provided parameters const char *argv13[] = { "tageditor", "get", "--fields", "album=test", "title", "diskpos", "--files", "somefile" }; verboseArg.setRequired(false); parser.resetArgs(); parser.parseArgs(8, argv13); // this should still work without complaints CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); CPPUNIT_ASSERT(!verboseArg.isPresent()); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(fieldsArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos")); CPPUNIT_ASSERT_THROW(fieldsArg.values().at(3), out_of_range); CPPUNIT_ASSERT(filesArg.isPresent()); CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile")); CPPUNIT_ASSERT(!notAlbumArg.isPresent()); // constraint checking: required value count with insufficient number of provided parameters const char *argv9[] = { "tageditor", "-p", "album", "title", "diskpos" }; fieldsArg.setRequiredValueCount(4); parser.resetArgs(); try { parser.parseArgs(5, argv9); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!strcmp(e.what(), "Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos")); } // nested operations const char *argv14[] = { "tageditor", "get", "fields", "album=test", "-f", "somefile" }; parser.resetArgs(); fieldsArg.setRequiredValueCount(-1); fieldsArg.setDenotesOperation(true); parser.parseArgs(6, argv14); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(fieldsArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test")); // implicit flag still works when argument doesn't denote operation parser.resetArgs(); fieldsArg.setDenotesOperation(false); parser.parseArgs(6, argv14); CPPUNIT_ASSERT(displayTagInfoArg.isPresent()); CPPUNIT_ASSERT(fieldsArg.isPresent()); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "fields")); CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "album=test")); } /*! * \brief Tests whether callbacks are called correctly. */ void ArgumentParserTests::testCallbacks() { ArgumentParser parser; Argument callbackArg("with-callback", 't', "callback test"); callbackArg.setRequiredValueCount(2); callbackArg.setCallback([](const ArgumentOccurrence &occurrence) { CPPUNIT_ASSERT_EQUAL(static_cast(0), occurrence.index); CPPUNIT_ASSERT(occurrence.path.empty()); CPPUNIT_ASSERT_EQUAL(static_cast(2), occurrence.values.size()); CPPUNIT_ASSERT(!strcmp(occurrence.values[0], "val1")); CPPUNIT_ASSERT(!strcmp(occurrence.values[1], "val2")); throw 42; }); Argument noCallbackArg("no-callback", 'l', "callback test"); noCallbackArg.setRequiredValueCount(2); parser.setMainArguments({ &callbackArg, &noCallbackArg }); // test whether callback is invoked when argument with callback is specified const char *argv[] = { "test", "-t", "val1", "val2" }; try { parser.parseArgs(4, argv); } catch (int i) { CPPUNIT_ASSERT_EQUAL(i, 42); } // test whether callback is not invoked when argument with callback is not specified callbackArg.reset(); const char *argv2[] = { "test", "-l", "val1", "val2" }; 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. * \remarks This tests makes assumptions about the order and the exact output format. */ void ArgumentParserTests::testBashCompletion() { ArgumentParser parser; HelpArgument helpArg(parser); Argument verboseArg("verbose", 'v', "be verbose"); verboseArg.setCombinable(true); Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened"); filesArg.setRequiredValueCount(Argument::varValueCount); filesArg.setCombinable(true); Argument nestedSubArg("nested-sub", '\0', "nested sub arg"); Argument subArg("sub", '\0', "sub arg"); subArg.setSubArguments({ &nestedSubArg }); Argument displayFileInfoArg("display-file-info", 'i', "displays general file information"); displayFileInfoArg.setDenotesOperation(true); displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg }); Argument fieldsArg("fields", '\0', "specifies the fields"); fieldsArg.setRequiredValueCount(Argument::varValueCount); fieldsArg.setPreDefinedCompletionValues("title album artist trackpos"); fieldsArg.setImplicit(true); Argument valuesArg("values", '\0', "specifies the fields"); valuesArg.setRequiredValueCount(Argument::varValueCount); valuesArg.setPreDefinedCompletionValues("title album artist trackpos"); valuesArg.setImplicit(false); valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign); Argument getArg("get", 'g', "gets tag values"); getArg.setSubArguments({ &fieldsArg, &filesArg }); Argument setArg("set", 's', "sets tag values"); setArg.setSubArguments({ &valuesArg, &filesArg }); parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg }); // fail due to operation flags not set const char *const argv1[] = { "se" }; ArgumentReader reader(parser, argv1, argv1 + 1, true); { const StandardOutputCheck c("COMPREPLY=()\n"); reader.read(); parser.printBashCompletion(1, argv1, 0, reader); } // correct operation arg flags getArg.setDenotesOperation(true); setArg.setDenotesOperation(true); { const StandardOutputCheck c("COMPREPLY=('set' )\n"); reader.reset(argv1, argv1 + 1).read(); parser.printBashCompletion(1, argv1, 0, reader); } // argument at current cursor position already specified -> the completion should just return the argument const char *const argv2[] = { "set" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('set' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 0, reader); } // advance the cursor position -> the completion should propose the next argument parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 1, reader); } // nested operations should be proposed as operations parser.resetArgs(); filesArg.setDenotesOperation(true); { const StandardOutputCheck c("COMPREPLY=('files' '--values' )\n"); reader.reset(argv2, argv2 + 1).read(); parser.printBashCompletion(1, argv2, 1, reader); } // specifying no args should propose all main arguments parser.resetArgs(); filesArg.setDenotesOperation(false); { const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); reader.reset(nullptr, nullptr).read(); parser.printBashCompletion(0, nullptr, 0, reader); } // pre-defined values const char *const argv3[] = { "get", "--fields" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n"); reader.reset(argv3, argv3 + 2).read(); parser.printBashCompletion(2, argv3, 2, reader); } // pre-defined values with equation sign, one letter already present const char *const argv4[] = { "set", "--values", "a" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n"); reader.reset(argv4, argv4 + 3).read(); parser.printBashCompletion(3, argv4, 2, reader); } // pre-defined values for implicit argument parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n"); reader.reset(argv3, argv3 + 1).read(); parser.printBashCompletion(1, argv3, 2, reader); } // file names string iniFilePath = TestUtilities::testFilePath("test.ini"); iniFilePath.resize(iniFilePath.size() - 4); string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv"); mkvFilePath.resize(mkvFilePath.size() - 17); parser.resetArgs(); 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", "COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n"); reader.reset(argv5, argv5 + 3).read(); parser.printBashCompletion(3, argv5, 2, reader); } // sub arguments const char *const argv6[] = { "set", "--" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('--files' '--values' )\n"); reader.reset(argv6, argv6 + 2).read(); parser.printBashCompletion(2, argv6, 1, reader); } // nested sub arguments const char *const argv7[] = { "-i", "--sub", "--" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n"); reader.reset(argv7, argv7 + 3).read(); parser.printBashCompletion(3, argv7, 2, reader); } // started pre-defined values with equation sign, one letter already present, last value matches const char *const argv8[] = { "set", "--values", "t" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n"); reader.reset(argv8, argv8 + 3).read(); parser.printBashCompletion(3, argv8, 2, reader); } // combined abbreviations const char *const argv9[] = { "-gf" }; parser.resetArgs(); { const StandardOutputCheck 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' ")); }); reader.reset(argv9, argv9 + 1).read(); parser.printBashCompletion(1, argv9, 1, reader); } // override exit function to prevent readArgs() from terminating the test run exitFunction = [](int) { exitCalled = true; }; // call completion via readArgs() with current word index const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n"); parser.readArgs(3, argv10); } CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable())); CPPUNIT_ASSERT(exitCalled); // call completion via readArgs() without current word index const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" }; parser.resetArgs(); { const StandardOutputCheck c("COMPREPLY=('get' )\n"); parser.readArgs(3, argv11); } } /*! * \brief Tests --help output. */ void ArgumentParserTests::testHelp() { // identation Indentation indent; indent = indent + 3; CPPUNIT_ASSERT_EQUAL(static_cast(4 + 3), indent.level); // setup parser ArgumentParser parser; HelpArgument helpArg(parser); helpArg.setRequired(true); OperationArgument verboseArg("verbose", 'v', "be verbose", "actually not an operation"); verboseArg.setCombinable(true); ConfigValueArgument nestedSubArg("nested-sub", '\0', "nested sub arg", { "value1", "value2" }); nestedSubArg.setRequiredValueCount(-1); Argument subArg("sub", '\0', "sub arg"); subArg.setRequired(true); subArg.addSubArgument(&nestedSubArg); Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened"); filesArg.setCombinable(true); filesArg.addSubArgument(&subArg); filesArg.setSubArguments({ &subArg }); // test re-assignment btw Argument envArg("env", '\0', "env"); envArg.setEnvironmentVariable("FILES"); envArg.setRequiredValueCount(2); envArg.appendValueName("file"); parser.addMainArgument(&helpArg); parser.addMainArgument(&verboseArg); parser.addMainArgument(&filesArg); parser.addMainArgument(&envArg); // parse args and assert output const char *const argv[] = { "app", "-h" }; { const StandardOutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n" "\n\e[0m" "Available arguments:\n" "\e[1m--help, -h\e[0m\n" " shows this information\n" " particularities: mandatory\n" "\n" "\e[1m--verbose, -v\e[0m\n" " be verbose\n" " \n" "usage: 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); } } /*! * \brief Tests some corner cases in setMainArguments() which are not already checked in the other tests. */ void ArgumentParserTests::testSetMainArguments() { ArgumentParser parser; HelpArgument helpArg(parser); Argument subArg("sub-arg", 's', "mandatory sub arg"); subArg.setRequired(true); helpArg.addSubArgument(&subArg); parser.addMainArgument(&helpArg); parser.setMainArguments({}); CPPUNIT_ASSERT_MESSAGE("clear main args", parser.mainArguments().empty()); parser.setMainArguments({ &helpArg }); CPPUNIT_ASSERT_MESSAGE("no default due to required sub arg", !parser.defaultArgument()); subArg.setConstraints(0, 20); parser.setMainArguments({ &helpArg }); CPPUNIT_ASSERT_MESSAGE("default if no required sub arg", &helpArg == parser.defaultArgument()); }