llvm-project/llvm/unittests/Option/OptionParsingTest.cpp
gbreynoo e28cd75a50 [OptTable] Reapply Improve error message output for grouped short options
This reapplies 71d7fed3bc2ad6c22729d446526a59fcfd99bd03 which was
reverted by 3e2bd82f02c6cbbfb0544897c7645867f04b3a7e. This change
includes the fix for breaking the sanitizer bots.

As seen in https://bugs.llvm.org/show_bug.cgi?id=48880 the current
implementation for parsing grouped short options can return unclear
error messages. This change fixes the example given in the ticket in
which a flag is incorrectly given an argument. Also when parsing a
group we now keep reading past the first incorrect option and output
errors for all incorrect options in the group.

Differential Revision: https://reviews.llvm.org/D108770
2021-09-03 11:13:52 +01:00

405 lines
12 KiB
C++

//===- unittest/Support/OptionParsingTest.cpp - OptTable tests ------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/ADT/STLExtras.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/Option.h"
#include "gtest/gtest.h"
using namespace llvm;
using namespace llvm::opt;
enum ID {
OPT_INVALID = 0, // This is not an option ID.
#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \
HELPTEXT, METAVAR, VALUES) \
OPT_##ID,
#include "Opts.inc"
LastOption
#undef OPTION
};
#define PREFIX(NAME, VALUE) const char *const NAME[] = VALUE;
#include "Opts.inc"
#undef PREFIX
enum OptionFlags {
OptFlag1 = (1 << 4),
OptFlag2 = (1 << 5),
OptFlag3 = (1 << 6)
};
static const OptTable::Info InfoTable[] = {
#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \
HELPTEXT, METAVAR, VALUES) \
{PREFIX, NAME, HELPTEXT, METAVAR, OPT_##ID, Option::KIND##Class, \
PARAM, FLAGS, OPT_##GROUP, OPT_##ALIAS, ALIASARGS, VALUES},
#include "Opts.inc"
#undef OPTION
};
namespace {
class TestOptTable : public OptTable {
public:
TestOptTable(bool IgnoreCase = false)
: OptTable(InfoTable, IgnoreCase) {}
};
}
const char *Args[] = {
"-A",
"-Bhi",
"--C=desu",
"-C", "bye",
"-D,adena",
"-E", "apple", "bloom",
"-Fblarg",
"-F", "42",
"-Gchuu", "2"
};
TEST(Option, OptionParsing) {
TestOptTable T;
unsigned MAI, MAC;
InputArgList AL = T.ParseArgs(Args, MAI, MAC);
// Check they all exist.
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_B));
EXPECT_TRUE(AL.hasArg(OPT_C));
EXPECT_TRUE(AL.hasArg(OPT_D));
EXPECT_TRUE(AL.hasArg(OPT_E));
EXPECT_TRUE(AL.hasArg(OPT_F));
EXPECT_TRUE(AL.hasArg(OPT_G));
// Check the values.
EXPECT_EQ("hi", AL.getLastArgValue(OPT_B));
EXPECT_EQ("bye", AL.getLastArgValue(OPT_C));
EXPECT_EQ("adena", AL.getLastArgValue(OPT_D));
std::vector<std::string> Es = AL.getAllArgValues(OPT_E);
EXPECT_EQ("apple", Es[0]);
EXPECT_EQ("bloom", Es[1]);
EXPECT_EQ("42", AL.getLastArgValue(OPT_F));
std::vector<std::string> Gs = AL.getAllArgValues(OPT_G);
EXPECT_EQ("chuu", Gs[0]);
EXPECT_EQ("2", Gs[1]);
// Check the help text.
std::string Help;
raw_string_ostream RSO(Help);
T.printHelp(RSO, "test", "title!");
EXPECT_NE(std::string::npos, Help.find("-A"));
// Check usage line.
T.printHelp(RSO, "name [options] file...", "title!");
EXPECT_NE(std::string::npos, Help.find("USAGE: name [options] file...\n"));
// Test aliases.
auto Cs = AL.filtered(OPT_C);
ASSERT_NE(Cs.begin(), Cs.end());
EXPECT_EQ("desu", StringRef((*Cs.begin())->getValue()));
ArgStringList ASL;
(*Cs.begin())->render(AL, ASL);
ASSERT_EQ(2u, ASL.size());
EXPECT_EQ("-C", StringRef(ASL[0]));
EXPECT_EQ("desu", StringRef(ASL[1]));
}
TEST(Option, ParseWithFlagExclusions) {
TestOptTable T;
unsigned MAI, MAC;
// Exclude flag3 to avoid parsing as OPT_SLASH_C.
InputArgList AL = T.ParseArgs(Args, MAI, MAC,
/*FlagsToInclude=*/0,
/*FlagsToExclude=*/OptFlag3);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_C));
EXPECT_FALSE(AL.hasArg(OPT_SLASH_C));
// Exclude flag1 to avoid parsing as OPT_C.
AL = T.ParseArgs(Args, MAI, MAC,
/*FlagsToInclude=*/0,
/*FlagsToExclude=*/OptFlag1);
EXPECT_TRUE(AL.hasArg(OPT_B));
EXPECT_FALSE(AL.hasArg(OPT_C));
EXPECT_TRUE(AL.hasArg(OPT_SLASH_C));
const char *NewArgs[] = { "/C", "foo", "--C=bar" };
AL = T.ParseArgs(NewArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_SLASH_C));
EXPECT_TRUE(AL.hasArg(OPT_C));
EXPECT_EQ("foo", AL.getLastArgValue(OPT_SLASH_C));
EXPECT_EQ("bar", AL.getLastArgValue(OPT_C));
}
TEST(Option, ParseAliasInGroup) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-I" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_H));
}
TEST(Option, AliasArgs) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-J", "-Joo" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_B));
EXPECT_EQ("foo", AL.getAllArgValues(OPT_B)[0]);
EXPECT_EQ("bar", AL.getAllArgValues(OPT_B)[1]);
}
TEST(Option, IgnoreCase) {
TestOptTable T(true);
unsigned MAI, MAC;
const char *MyArgs[] = { "-a", "-joo" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_B));
}
TEST(Option, DoNotIgnoreCase) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-a", "-joo" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_FALSE(AL.hasArg(OPT_A));
EXPECT_FALSE(AL.hasArg(OPT_B));
}
TEST(Option, SlurpEmpty) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurp" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_Slurp));
EXPECT_EQ(0U, AL.getAllArgValues(OPT_Slurp).size());
}
TEST(Option, Slurp) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurp", "-B", "--", "foo" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_EQ(AL.size(), 2U);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_FALSE(AL.hasArg(OPT_B));
EXPECT_TRUE(AL.hasArg(OPT_Slurp));
EXPECT_EQ(3U, AL.getAllArgValues(OPT_Slurp).size());
EXPECT_EQ("-B", AL.getAllArgValues(OPT_Slurp)[0]);
EXPECT_EQ("--", AL.getAllArgValues(OPT_Slurp)[1]);
EXPECT_EQ("foo", AL.getAllArgValues(OPT_Slurp)[2]);
}
TEST(Option, SlurpJoinedEmpty) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurpjoined" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_SlurpJoined));
EXPECT_EQ(AL.getAllArgValues(OPT_SlurpJoined).size(), 0U);
}
TEST(Option, SlurpJoinedOneJoined) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurpjoinedfoo" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_SlurpJoined));
EXPECT_EQ(AL.getAllArgValues(OPT_SlurpJoined).size(), 1U);
EXPECT_EQ(AL.getAllArgValues(OPT_SlurpJoined)[0], "foo");
}
TEST(Option, SlurpJoinedAndSeparate) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurpjoinedfoo", "bar", "baz" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_SlurpJoined));
EXPECT_EQ(3U, AL.getAllArgValues(OPT_SlurpJoined).size());
EXPECT_EQ("foo", AL.getAllArgValues(OPT_SlurpJoined)[0]);
EXPECT_EQ("bar", AL.getAllArgValues(OPT_SlurpJoined)[1]);
EXPECT_EQ("baz", AL.getAllArgValues(OPT_SlurpJoined)[2]);
}
TEST(Option, SlurpJoinedButSeparate) {
TestOptTable T;
unsigned MAI, MAC;
const char *MyArgs[] = { "-A", "-slurpjoined", "foo", "bar", "baz" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_SlurpJoined));
EXPECT_EQ(3U, AL.getAllArgValues(OPT_SlurpJoined).size());
EXPECT_EQ("foo", AL.getAllArgValues(OPT_SlurpJoined)[0]);
EXPECT_EQ("bar", AL.getAllArgValues(OPT_SlurpJoined)[1]);
EXPECT_EQ("baz", AL.getAllArgValues(OPT_SlurpJoined)[2]);
}
TEST(Option, FlagAliasToJoined) {
TestOptTable T;
unsigned MAI, MAC;
// Check that a flag alias provides an empty argument to a joined option.
const char *MyArgs[] = { "-K" };
InputArgList AL = T.ParseArgs(MyArgs, MAI, MAC);
EXPECT_EQ(AL.size(), 1U);
EXPECT_TRUE(AL.hasArg(OPT_B));
EXPECT_EQ(1U, AL.getAllArgValues(OPT_B).size());
EXPECT_EQ("", AL.getAllArgValues(OPT_B)[0]);
}
TEST(Option, FindNearest) {
TestOptTable T;
std::string Nearest;
// Options that are too short should not be considered
// "near" other short options.
EXPECT_GT(T.findNearest("-A", Nearest), 4U);
EXPECT_GT(T.findNearest("/C", Nearest), 4U);
EXPECT_GT(T.findNearest("--C=foo", Nearest), 4U);
// The nearest candidate should mirror the amount of prefix
// characters used in the original string.
EXPECT_EQ(1U, T.findNearest("-blorb", Nearest));
EXPECT_EQ(Nearest, "-blorp");
EXPECT_EQ(1U, T.findNearest("--blorm", Nearest));
EXPECT_EQ(Nearest, "--blorp");
EXPECT_EQ(1U, T.findNearest("-blarg", Nearest));
EXPECT_EQ(Nearest, "-blarn");
EXPECT_EQ(1U, T.findNearest("--blarm", Nearest));
EXPECT_EQ(Nearest, "--blarn");
EXPECT_EQ(1U, T.findNearest("-fjormp", Nearest));
EXPECT_EQ(Nearest, "--fjormp");
// The nearest candidate respects the prefix and value delimiter
// of the original string.
EXPECT_EQ(1U, T.findNearest("/framb:foo", Nearest));
EXPECT_EQ(Nearest, "/cramb:foo");
// `--glormp` should have an editing distance > 0 from `--glormp=`.
EXPECT_GT(T.findNearest("--glorrmp", Nearest), 0U);
EXPECT_EQ(Nearest, "--glorrmp=");
EXPECT_EQ(0U, T.findNearest("--glorrmp=foo", Nearest));
// `--blurmps` should correct to `--blurmp`, not `--blurmp=`, even though
// both naively have an editing distance of 1.
EXPECT_EQ(1U, T.findNearest("--blurmps", Nearest));
EXPECT_EQ(Nearest, "--blurmp");
// ...but `--blurmps=foo` should correct to `--blurmp=foo`.
EXPECT_EQ(1U, T.findNearest("--blurmps=foo", Nearest));
EXPECT_EQ(Nearest, "--blurmp=foo");
// Flags should be included and excluded as specified.
EXPECT_EQ(1U, T.findNearest("-doopf", Nearest, /*FlagsToInclude=*/OptFlag2));
EXPECT_EQ(Nearest, "-doopf2");
EXPECT_EQ(1U, T.findNearest("-doopf", Nearest,
/*FlagsToInclude=*/0,
/*FlagsToExclude=*/OptFlag2));
EXPECT_EQ(Nearest, "-doopf1");
}
TEST(DISABLED_Option, FindNearestFIXME) {
TestOptTable T;
std::string Nearest;
// FIXME: Options with joined values should not have those values considered
// when calculating distance. The test below would fail if run, but it should
// succeed.
EXPECT_EQ(1U, T.findNearest("--erbghFoo", Nearest));
EXPECT_EQ(Nearest, "--ermghFoo");
}
TEST(Option, ParseGroupedShortOptions) {
TestOptTable T;
T.setGroupedShortOptions(true);
unsigned MAI, MAC;
// Grouped short options can be followed by a long Flag (-Joo), or a non-Flag
// option (-C=1).
const char *Args1[] = {"-AIJ", "-AIJoo", "-AC=1"};
InputArgList AL = T.ParseArgs(Args1, MAI, MAC);
EXPECT_TRUE(AL.hasArg(OPT_A));
EXPECT_TRUE(AL.hasArg(OPT_H));
ASSERT_EQ((size_t)2, AL.getAllArgValues(OPT_B).size());
EXPECT_EQ("foo", AL.getAllArgValues(OPT_B)[0]);
EXPECT_EQ("bar", AL.getAllArgValues(OPT_B)[1]);
ASSERT_TRUE(AL.hasArg(OPT_C));
EXPECT_EQ("1", AL.getAllArgValues(OPT_C)[0]);
// Prefer a long option to a short option.
const char *Args2[] = {"-AB"};
InputArgList AL2 = T.ParseArgs(Args2, MAI, MAC);
EXPECT_TRUE(!AL2.hasArg(OPT_A));
EXPECT_TRUE(AL2.hasArg(OPT_AB));
// Short options followed by a long option. We probably should disallow this.
const char *Args3[] = {"-AIblorp"};
InputArgList AL3 = T.ParseArgs(Args3, MAI, MAC);
EXPECT_TRUE(AL3.hasArg(OPT_A));
EXPECT_TRUE(AL3.hasArg(OPT_Blorp));
}
TEST(Option, UnknownOptions) {
TestOptTable T;
unsigned MAI, MAC;
const char *Args[] = {"-u", "--long", "0"};
for (int I = 0; I < 2; ++I) {
T.setGroupedShortOptions(I != 0);
InputArgList AL = T.ParseArgs(Args, MAI, MAC);
const std::vector<std::string> Unknown = AL.getAllArgValues(OPT_UNKNOWN);
ASSERT_EQ((size_t)2, Unknown.size());
EXPECT_EQ("-u", Unknown[0]);
EXPECT_EQ("--long", Unknown[1]);
}
}
TEST(Option, FlagsWithoutValues) {
TestOptTable T;
T.setGroupedShortOptions(true);
unsigned MAI, MAC;
const char *Args[] = {"-A=1", "-A="};
InputArgList AL = T.ParseArgs(Args, MAI, MAC);
const std::vector<std::string> Unknown = AL.getAllArgValues(OPT_UNKNOWN);
ASSERT_EQ((size_t)2, Unknown.size());
EXPECT_EQ("-A=1", Unknown[0]);
EXPECT_EQ("-A=", Unknown[1]);
}
TEST(Option, UnknownGroupedShortOptions) {
TestOptTable T;
T.setGroupedShortOptions(true);
unsigned MAI, MAC;
const char *Args[] = {"-AuzK", "-AuzK"};
InputArgList AL = T.ParseArgs(Args, MAI, MAC);
const std::vector<std::string> Unknown = AL.getAllArgValues(OPT_UNKNOWN);
ASSERT_EQ((size_t)4, Unknown.size());
EXPECT_EQ("-u", Unknown[0]);
EXPECT_EQ("-z", Unknown[1]);
EXPECT_EQ("-u", Unknown[2]);
EXPECT_EQ("-z", Unknown[3]);
}