#include "./testutils.h" #include "../application/argumentparser.h" #include "../application/failure.h" #include "../application/fakeqtconfigarguments.h" #include "../io/path.h" #include "resources/config.h" #include #include #include using namespace std; using namespace ApplicationUtilities; using namespace CPPUNIT_NS; /*! * \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_SUITE_END(); public: void setUp(); void tearDown(); void testArgument(); void testParsing(); void testCallbacks(); void testBashCompletion(); 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()); } /*! * \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); 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"); displayFileInfoArg.setDenotesOperation(true); displayFileInfoArg.setSubArguments({&fileArg, &verboseArg}); 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}); parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &helpArg}); // define some argument values 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\".")); } // try to parse again, but adjust the configuration for a successful parse displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); filesArg.setCombinable(true); 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); // define the same arguments in a different way const char *argv2[] = {"tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile"}; // reparse the args displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); 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")); // forget "get"/"-p" const char *argv3[] = {"tageditor", "album", "title", "diskpos", "--files", "somefile"}; displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); // a parsing error should occur because the argument "album" is not defined try { parser.parseArgs(6, argv3); CPPUNIT_FAIL("Exception expected."); } catch(const Failure &e) { CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"album\" is unknown and will be ignored.")); } // repeat the test, but this time just ignore the undefined argument printing a warning displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn); // redirect stderr to check whether warnings are printed correctly stringstream buffer; streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf()); try { parser.parseArgs(6, argv3); cerr.rdbuf(regularCerrBuffer); } catch(...) { cerr.rdbuf(regularCerrBuffer); throw; } CPPUNIT_ASSERT(!strcmp(buffer.str().data(), "The specified argument \"album\" is unknown and will be ignored.\n" "The specified argument \"title\" is unknown and will be ignored.\n" "The specified argument \"diskpos\" is unknown and will be ignored.\n" "The specified argument \"--files\" is unknown and will be ignored.\n" "The specified argument \"somefile\" is unknown and will be ignored.\n")); // 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()); // test abbreviations like "-vf" const char *argv4[] = {"tageditor", "-i", "-vf", "test"}; displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail); 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); // don't reset verbose argument to test constraint checking 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.")); } // relax constraint displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); verboseArg.setConstraints(0, -1); parser.parseArgs(4, argv4); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); // make verbose mandatory verboseArg.setRequired(true); displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); parser.parseArgs(4, argv4); CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); // make it complain about missing 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.")); } // it should not complain if -i is not present const char *argv6[] = {"tageditor", "-g"}; displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); parser.parseArgs(2, argv6); CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent()); // it should not be possible to specify -f without -i or -p const char *argv7[] = {"tageditor", "-f", "test"}; displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(), qtConfigArgs.qtWidgetsGuiArg().reset(); try { parser.parseArgs(3, argv7); CPPUNIT_FAIL("Exception expected."); } catch(const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"-f\" is unknown and will be ignored.")); } // test default argument const char *argv8[] = {"tageditor"}; displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); 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()); // test required value count constraint with sufficient number of provided parameters qtConfigArgs.qtWidgetsGuiArg().reset(); fieldsArg.setRequiredValueCount(3); verboseArg.setRequired(false); parser.parseArgs(9, argv2); // 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")); 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")); // test required value count constraint with insufficient number of provided parameters displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); fieldsArg.setRequiredValueCount(4); const char *argv9[] = {"tageditor", "-p", "album", "title", "diskpos"}; 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")); } } /*! * \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 vector &values) { CPPUNIT_ASSERT(values.size() == 2); CPPUNIT_ASSERT(!strcmp(values[0], "val1")); CPPUNIT_ASSERT(!strcmp(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 Tests bash completion. * \remarks This tests makes assumptions about the order and the exact output format * which should be improved. */ 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(-1); 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(-1); fieldsArg.setPreDefinedCompletionValues("title album artist trackpos"); fieldsArg.setImplicit(true); Argument valuesArg("values", '\0', "specifies the fields"); valuesArg.setRequiredValueCount(-1); valuesArg.setPreDefinedCompletionValues("title album artist trackpos"); valuesArg.setImplicit(true); 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}); size_t index = 0; Argument *lastDetectedArg = nullptr; // redirect cout to custom buffer stringstream buffer; streambuf *regularCoutBuffer = cout.rdbuf(buffer.rdbuf()); try { // should fail because operation flags are not set const char *const argv1[] = {"se"}; const char *const *argv = argv1; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv1 + 1, lastDetectedArg, true); parser.printBashCompletion(1, argv1, 0, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=()\n"), buffer.str()); // with correct operation arg flags index = 0, lastDetectedArg = nullptr, buffer.str(string()); cout.rdbuf(buffer.rdbuf()); getArg.setDenotesOperation(true), setArg.setDenotesOperation(true); argv = argv1; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv1 + 1, lastDetectedArg, true); parser.printBashCompletion(1, argv1, 0, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(set )\n"), buffer.str()); // argument is already specified const char *const argv2[] = {"set"}; index = 0, lastDetectedArg = nullptr, buffer.str(string()); cout.rdbuf(buffer.rdbuf()); argv = argv2; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv2 + 1, lastDetectedArg, true); parser.printBashCompletion(1, argv2, 0, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(set )\n"), buffer.str()); // advance the cursor position -> the completion should propose the next argument index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv2; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv2 + 1, lastDetectedArg, true); parser.printBashCompletion(1, argv2, 1, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --values )\n"), buffer.str()); // specifying no args should propose all main arguments index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = nullptr; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, nullptr, lastDetectedArg, true); parser.printBashCompletion(0, nullptr, 0, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(display-file-info get set --help )\n"), buffer.str()); // values const char *const argv3[] = {"get", "--fields"}; index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv3; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv3 + 2, lastDetectedArg, true); parser.printBashCompletion(2, argv3, 2, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(title album artist trackpos --files )\n"), buffer.str()); // values with equation sign, one letter already present const char *const argv4[] = {"set", "--values", "a"}; index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv4; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv4 + 3, lastDetectedArg, true); parser.printBashCompletion(3, argv4, 2, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(album= artist= ); compopt -o nospace\n"), buffer.str()); // file names string iniFilePath = TestUtilities::testFilePath("test.ini"); iniFilePath.resize(iniFilePath.size() - 3); const char *const argv5[] = {"get", "--files", iniFilePath.c_str()}; index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv5; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv5 + 3, lastDetectedArg, true); parser.printBashCompletion(3, argv5, 2, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL("COMPREPLY=('" + iniFilePath + "ini' ); compopt -o filenames\n", buffer.str()); // sub arguments const char *const argv6[] = {"set", "--"}; index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset(), valuesArg.reset(), filesArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv6; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv6 + 2, lastDetectedArg, true); parser.printBashCompletion(2, argv6, 1, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --values )\n"), buffer.str()); // nested sub arguments const char *const argv7[] = {"-i", "--sub", "--"}; index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset(), valuesArg.reset(), filesArg.reset(); cout.rdbuf(buffer.rdbuf()); argv = argv7; parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv7 + 3, lastDetectedArg, true); parser.printBashCompletion(3, argv7, 2, lastDetectedArg); cout.rdbuf(regularCoutBuffer); CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --nested-sub --verbose )\n"), buffer.str()); } catch(...) { cout.rdbuf(regularCoutBuffer); throw; } }