Common C++ classes and routines used by my applications such as argument parser, IO and conversion utilities
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

981 lines
43 KiB

#include "./outputcheck.h"
#include "./testutils.h"
#include "../conversion/stringbuilder.h"
#include "../conversion/stringconversion.h"
#include "../application/argumentparser.h"
#include "../application/argumentparserprivate.h"
#include "../application/commandlineutils.h"
#include "../application/fakeqtconfigarguments.h"
#include "../io/ansiescapecodes.h"
#include "../io/path.h"
#include "../misc/parseerror.h"
#include "resources/config.h"
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cstdlib>
#include <cstring>
#ifdef PLATFORM_WINDOWS
#include <windows.h>
#endif
using namespace std;
using namespace CppUtilities;
using namespace CppUtilities::Literals;
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(testSetMainArguments);
CPPUNIT_TEST(testValueConversion);
#ifndef PLATFORM_WINDOWS
CPPUNIT_TEST(testBashCompletion);
CPPUNIT_TEST(testHelp);
CPPUNIT_TEST(testNoColorArgument);
#endif
CPPUNIT_TEST_SUITE_END();
public:
void setUp() override;
void tearDown() override;
void testArgument();
void testParsing();
void testCallbacks();
void testSetMainArguments();
void testValueConversion();
#ifndef PLATFORM_WINDOWS
void testBashCompletion();
void testHelp();
void testNoColorArgument();
#endif
private:
void callback();
[[noreturn]] void failOnExit(int code);
};
CPPUNIT_TEST_SUITE_REGISTRATION(ArgumentParserTests);
void ArgumentParserTests::setUp()
{
#ifndef PLATFORM_WINDOWS
setenv("ENABLE_ESCAPE_CODES", "0", 1);
#endif
EscapeCodes::enabled = false;
}
void ArgumentParserTests::tearDown()
{
}
[[noreturn]] void ArgumentParserTests::failOnExit(int code)
{
CPPUNIT_FAIL(argsToString("Exited unexpectedly with code ", code));
}
/*!
* \brief Tests the behaviour of the argument class.
*/
void ArgumentParserTests::testArgument()
{
Argument argument("test", 't', "some description");
CPPUNIT_ASSERT_EQUAL(false, argument.isRequired());
argument.setConstraints(1, 10);
CPPUNIT_ASSERT_EQUAL(true, argument.isRequired());
Argument subArg("sub", 's', "sub arg");
argument.addSubArgument(&subArg);
CPPUNIT_ASSERT_EQUAL(&argument, subArg.parents().at(0));
CPPUNIT_ASSERT(!subArg.conflictsWithArgument());
CPPUNIT_ASSERT(!argument.firstValue());
argument.setEnvironmentVariable("FOO_ENV_VAR");
#ifndef PLATFORM_WINDOWS // disabled under Windows for same reason as testNoColorArgument()
setenv("FOO_ENV_VAR", "foo", 1);
CPPUNIT_ASSERT_EQUAL("foo"s, string(argument.firstValue()));
#endif
ArgumentOccurrence occurrence(0, vector<Argument *>(), nullptr);
occurrence.values.emplace_back("bar");
argument.m_occurrences.emplace_back(move(occurrence));
CPPUNIT_ASSERT_EQUAL("bar"s, string(argument.firstValue()));
}
/*!
* \brief Tests parsing command line arguments.
*/
void ArgumentParserTests::testParsing()
{
// setup parser with some test argument definitions
ArgumentParser parser;
parser.setExitFunction(std::bind(&ArgumentParserTests::failOnExit, this, std::placeholders::_1));
SET_APPLICATION_INFO;
QT_CONFIG_ARGUMENTS qtConfigArgs;
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(Argument::varValueCount);
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, &notAlbumArg });
Argument fieldsArg("fields", '\0', "specifies the fields");
fieldsArg.setRequiredValueCount(Argument::varValueCount);
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, &notAlbumArg });
parser.setMainArguments(
{ &qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &parser.noColorArg(), &parser.helpArg() });
// no args present
parser.parseArgs(0, nullptr, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!parser.executable());
CPPUNIT_ASSERT(!parser.specifiedOperation());
CPPUNIT_ASSERT_EQUAL(0u, parser.actualArgumentCount());
// 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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT_EQUAL("The argument \"files\" can not be combined with \"fields\"."s, string(e.what()));
// test printing btw
stringstream ss;
ss << e;
CPPUNIT_ASSERT_EQUAL(
"Error: Unable to parse arguments: The argument \"files\" can not be combined with \"fields\".\nSee --help for available commands.\n"s,
ss.str());
}
CPPUNIT_ASSERT(parser.isUncombinableMainArgPresent());
// arguments read correctly after successful parse
filesArg.setCombinable(true);
parser.resetArgs();
parser.parseArgs(7, argv, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
// 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);
CPPUNIT_ASSERT_EQUAL(&displayTagInfoArg, parser.specifiedOperation());
// skip empty args
const char *argv2[] = { "tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile" };
// reparse the args
parser.resetArgs();
parser.parseArgs(9, argv2, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
// 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"));
6 years ago
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown.\nDid you mean get or --help?"s, string(e.what()));
}
// error about unknown argument: mistake in final argument
const char *argv18[] = { "tageditor", "get", "album", "title", "diskpos", "--verbose", "--fi" };
try {
parser.resetArgs();
parser.parseArgs(7, argv18, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT_EQUAL("The specified argument \"--fi\" is unknown.\nDid you mean --files or --no-color?"s, string(e.what()));
}
// warning about unknown argument
parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
{
#ifndef PLATFORM_WINDOWS
const OutputCheck outputCheck("Warning: The specified argument \"album\" is unknown and will be ignored.\n"s
"Warning: The specified argument \"title\" is unknown and will be ignored.\n"s
"Warning: The specified argument \"diskpos\" is unknown and will be ignored.\n"s
"Warning: The specified argument \"--files\" is unknown and will be ignored.\n"s
"Warning: The specified argument \"somefile\" is unknown and will be ignored.\n"s,
cerr);
#endif
parser.resetArgs();
EscapeCodes::enabled = false;
parser.parseArgs(6, argv3, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
// 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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &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, Argument::varValueCount);
parser.parseArgs(4, argv4, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
// constraint checking: mandatory argument
verboseArg.setRequired(true);
parser.resetArgs();
parser.parseArgs(4, argv4, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!fileArg.isPresent());
CPPUNIT_ASSERT(filesArg.isPresent());
CPPUNIT_ASSERT_EQUAL(1_st, filesArg.values(0).size());
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown.\nDid you mean get or --help?"s, string(e.what()));
}
// equation sign syntax
const char *argv11[] = { "tageditor", "-if=test-v" };
parser.resetArgs();
parser.parseArgs(2, argv11, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!filesArg.isPresent());
CPPUNIT_ASSERT(fileArg.isPresent());
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
const char *argv15[] = { "tageditor", "-i", "--file=test", "-v" };
parser.resetArgs();
parser.parseArgs(4, argv15, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!filesArg.isPresent());
CPPUNIT_ASSERT(fileArg.isPresent());
CPPUNIT_ASSERT(verboseArg.isPresent());
CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
CPPUNIT_ASSERT_EQUAL("test"s, string(fileArg.values(0).front()));
// specifying value directly after abbreviation
const char *argv12[] = { "tageditor", "-iftest" };
parser.resetArgs();
parser.parseArgs(2, argv12, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!filesArg.isPresent());
CPPUNIT_ASSERT(fileArg.isPresent());
CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test"));
// specifying top-level argument after abbreviation
const char *argv17[] = { "tageditor", "-if=test-v", "--no-color" };
parser.resetArgs();
parser.parseArgs(3, argv17, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_ASSERT(!filesArg.isPresent());
CPPUNIT_ASSERT(fileArg.isPresent());
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT(parser.noColorArg().isPresent());
CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
// default argument
const char *argv8[] = { "tageditor" };
parser.resetArgs();
parser.parseArgs(1, argv8, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
// 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"));
6 years ago
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT_EQUAL(
"Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos"s,
string(e.what()));
}
// constraint checking: truncated argument not wrongly detected
const char *argv16[] = { "tageditor", "--hel", "-p", "album", "title", "diskpos" };
fieldsArg.setRequiredValueCount(Argument::varValueCount);
parser.resetArgs();
try {
parser.parseArgs(6, argv16, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
CPPUNIT_FAIL("Exception expected.");
} catch (const ParseError &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown.\nDid you mean --help or get?"s, string(e.what()));
}
// nested operations
const char *argv14[] = { "tageditor", "get", "fields", "album=test", "-f", "somefile" };
parser.resetArgs();
fieldsArg.setDenotesOperation(true);
parser.parseArgs(6, argv14, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
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;
parser.setExitFunction(std::bind(&ArgumentParserTests::failOnExit, this, std::placeholders::_1));
Argument callbackArg("with-callback", 't', "callback test");
callbackArg.setRequiredValueCount(2);
callbackArg.setCallback([](const ArgumentOccurrence &occurrence) {
CPPUNIT_ASSERT_EQUAL(0_st, occurrence.index);
CPPUNIT_ASSERT(occurrence.path.empty());
CPPUNIT_ASSERT_EQUAL(2_st, 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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
} 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, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::InvokeCallbacks);
}
#ifndef PLATFORM_WINDOWS
/*!
* \brief Used to check whether the exit() function is called when printing bash completion.
*/
static bool exitCalled = false;
/*!
* \brief Tests bash completion.
* \remarks
* - Disabled under Windows because OutputCheck isn't working.
* - This tests makes assumptions about the order and the exact output format.
*/
void ArgumentParserTests::testBashCompletion()
{
ArgumentParser parser;
parser.setExitFunction(std::bind(&ArgumentParserTests::failOnExit, this, std::placeholders::_1));
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 selectorsArg("selectors", '\0', "has some more pre-defined values");
selectorsArg.setRequiredValueCount(Argument::varValueCount);
selectorsArg.setPreDefinedCompletionValues("tag=id3v1 tag=id3v2 tag=matroska target=file target=track");
selectorsArg.setImplicit(false);
selectorsArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues);
selectorsArg.setCallback(
[&selectorsArg](const ArgumentOccurrence &) { selectorsArg.setPreDefinedCompletionValues("tag=matroska tag=mp4 tag=vorbis"); });
Argument getArg("get", 'g', "gets tag values");
getArg.setSubArguments({ &fieldsArg, &filesArg });
Argument setArg("set", 's', "sets tag values");
setArg.setSubArguments({ &valuesArg, &filesArg, &selectorsArg });
parser.setMainArguments({ &displayFileInfoArg, &getArg, &setArg, &parser.noColorArg(), &parser.helpArg() });
// fail due to operation flags not set
const char *const argv1[] = { "se" };
ArgumentReader reader(parser, argv1, argv1 + 1, true);
{
const OutputCheck c("COMPREPLY=()\n");
CPPUNIT_ASSERT(reader.read());
parser.printBashCompletion(1, argv1, 0, reader);
}
// correct operation arg flags
getArg.setDenotesOperation(true);
setArg.setDenotesOperation(true);
{
const OutputCheck 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 OutputCheck 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 OutputCheck c("COMPREPLY=('--files' '--no-color' '--selectors' '--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 OutputCheck c("COMPREPLY=('files' '--no-color' '--selectors' '--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 OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' '--no-color' )\n");
reader.reset(nullptr, nullptr).read();
parser.printBashCompletion(0, nullptr, 0, reader);
}
// pre-defined values
const char *const argv3[] = { "get", "--fields" };
parser.resetArgs();
{
const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' '--no-color' )\n");
reader.reset(argv3, argv3 + 2).read();