winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

Command.cpp (25740B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "TestCommon.h"
      5 #include <Command.h>
      6 #include <AppInstallerStrings.h>
      7 #include <Commands/RootCommand.h>
      8 
      9 using namespace std::string_literals;
     10 using namespace std::string_view_literals;
     11 using namespace TestCommon;
     12 using namespace AppInstaller;
     13 using namespace AppInstaller::CLI;
     14 using namespace AppInstaller::CLI::Execution;
     15 
     16 std::string GetCommandName(const std::unique_ptr<Command>& command)
     17 {
     18     return std::string{ command->FullName() };
     19 }
     20 
     21 std::vector<std::string_view> GetCommandAliases(const std::unique_ptr<Command>& command)
     22 {
     23     return std::vector<std::string_view> { command->Aliases() };
     24 }
     25 
     26 std::string GetArgumentName(const Argument& arg)
     27 {
     28     return std::string{ arg.Name() };
     29 }
     30 
     31 std::string GetArgumentAlternateName(const Argument& arg)
     32 {
     33     if (arg.AlternateName() == Argument::NoAlternateName)
     34     {
     35         return {};
     36     }
     37     else
     38     {
     39         return std::string{ arg.AlternateName() };
     40     }
     41 }
     42 
     43 std::string GetArgumentAlias(const Argument& arg)
     44 {
     45     if (arg.Alias() == ArgumentCommon::NoAlias)
     46     {
     47         return {};
     48     }
     49     else
     50     {
     51         return std::string(1, arg.Alias());
     52     }
     53 }
     54 
     55 bool StringIsLowercase(const std::string& s)
     56 {
     57     return Utility::ToLower(s) == s;
     58 }
     59 
     60 template <typename Enumerable, typename Op>
     61 void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, std::unordered_set<std::string>& values, bool requireLower = true)
     62 {
     63     INFO(info);
     64 
     65     for (const auto& val : e)
     66     {
     67         std::string valString = op(val);
     68         if (valString.empty())
     69         {
     70             continue;
     71         }
     72         INFO(valString);
     73 
     74         if (requireLower)
     75         {
     76             REQUIRE(StringIsLowercase(valString));
     77         }
     78 
     79         REQUIRE(values.find(valString) == values.end());
     80 
     81         values.emplace(std::move(valString));
     82     }
     83 }
     84 
     85 template <typename Enumerable, typename Op>
     86 void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, bool requireLower = true)
     87 {
     88     std::unordered_set<std::string> values;
     89     EnsureStringsAreLowercaseAndNoCollisions(info, e, op, values, requireLower);
     90 }
     91 
     92 template <typename Enumerable, typename Op>
     93 void EnsureVectorStringViewsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, std::unordered_set<std::string>& values, bool requireLower = true)
     94 {
     95     INFO(info);
     96 
     97     for (const auto& val : e)
     98     { 
     99         std::vector<std::string_view> aliasVector = op(val);
    100         std::vector<std::string> valVector(aliasVector.begin(), aliasVector.end());
    101         if (valVector.empty())
    102         {
    103             continue;
    104         }
    105         // When op returns a vector, we need to ensure every value in the vector does not cause a collision
    106         for (auto& valString : valVector)
    107         {
    108             INFO(valString);
    109 
    110             if (requireLower)
    111             {
    112                 REQUIRE(StringIsLowercase(valString));
    113             }
    114 
    115             REQUIRE(values.find(valString) == values.end());
    116             values.emplace(std::move(valString));
    117         }
    118     }
    119 }
    120 
    121 void EnsureCommandConsistency(const Command& command)
    122 {
    123     // Command names and aliases exist in the same space, so both need to be checked as a set
    124     // However, collisions do not occur between levels, so the full name must be used to check for collision
    125     std::unordered_set<std::string> allCommandAliasNames; 
    126     EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " commands", command.GetCommands(), GetCommandName, allCommandAliasNames);
    127     EnsureVectorStringViewsAreLowercaseAndNoCollisions(command.FullName() + " aliases", command.GetCommands(), GetCommandAliases, allCommandAliasNames);
    128 
    129     auto args = command.GetArguments();
    130     Argument::GetCommon(args);
    131 
    132     // Argument names and alternate names exist in the same space, so both need to be checked as a set
    133     std::unordered_set<std::string> allArgumentNames;
    134     EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " argument names", args, GetArgumentName, allArgumentNames);
    135     EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " argument alternate name", args, GetArgumentAlternateName, allArgumentNames);
    136     // Argument aliases use a different space than the names and can be checked separately
    137     EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " argument alias", args, GetArgumentAlias, false);
    138 
    139     // No : allowed in commands
    140     for (const auto& comm : command.GetCommands())
    141     {
    142         INFO(command.FullName());
    143         INFO(comm->Name());
    144 
    145         REQUIRE(comm->Name().find_first_of(Command::ParentSplitChar) == std::string_view::npos);
    146     }
    147 
    148     // No = allowed in arguments
    149     // All positional args should be listed first
    150     bool foundNonPositional = false;
    151     bool queryArgPresent = false;
    152     bool multiQueryArgPresent = false;
    153     for (const auto& arg : command.GetArguments())
    154     {
    155         INFO(command.FullName());
    156         INFO(arg.Name());
    157 
    158         REQUIRE(arg.Name().find_first_of(APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR) == std::string_view::npos);
    159 
    160         if (arg.Type() == ArgumentType::Positional)
    161         {
    162             REQUIRE(!foundNonPositional);
    163         }
    164         else
    165         {
    166             foundNonPositional = true;
    167         }
    168 
    169         if (arg.ExecArgType() == Execution::Args::Type::Query)
    170         {
    171             queryArgPresent = true;
    172         }
    173 
    174         if (arg.ExecArgType() == Execution::Args::Type::MultiQuery)
    175         {
    176             multiQueryArgPresent = true;
    177         }
    178     }
    179 
    180     REQUIRE((!queryArgPresent || !multiQueryArgPresent));
    181 
    182     // Recurse for all subcommands
    183     for (const auto& sub : command.GetCommands())
    184     {
    185         EnsureCommandConsistency(*sub.get());
    186     }
    187 }
    188 
    189 // This test ensure that the command tree we expose does not have any inconsistencies.
    190 //  1. No command name collisions
    191 //  2. All command names are lower cased
    192 //  3. No argument name collisions
    193 //  4. All arguments are lower cased
    194 //  5. No argument alias collisions
    195 //  6. All argument alias are lower cased
    196 //  7. No argument names contain '='
    197 //  8. All positional arguments are first in the list
    198 //  9. No command includes both Query and MultiQuery arguments
    199 TEST_CASE("EnsureCommandTreeConsistency", "[command]")
    200 {
    201     RootCommand root;
    202     EnsureCommandConsistency(root);
    203 }
    204 
    205 struct TestCommand : public Command
    206 {
    207     TestCommand(std::vector<Argument> args) : Command("test", ""), m_args(std::move(args)) {}
    208 
    209     std::vector<Argument> GetArguments() const override
    210     {
    211         return m_args;
    212     }
    213 
    214     CLI::Resource::LocString ShortDescription() const override { return {}; }
    215     CLI::Resource::LocString LongDescription() const override { return {}; }
    216 
    217     std::vector<Argument> m_args;
    218 };
    219 
    220 // Matcher that lets us verify CommandExceptions.
    221 struct CommandExceptionMatcher : public Catch::Matchers::MatcherBase<CommandException>
    222 {
    223     CommandExceptionMatcher(CLI::Resource::LocString message) : m_expectedMessage(std::move(message)) {}
    224 
    225     bool match(const CommandException& ce) const override
    226     {
    227         return ce.Message() == m_expectedMessage;
    228     }
    229 
    230     std::string describe() const override
    231     {
    232         std::ostringstream result;
    233         result << "has message == " << m_expectedMessage;
    234         return result.str();
    235     }
    236 
    237 private:
    238     CLI::Resource::LocString m_expectedMessage;
    239 };
    240 
    241 namespace Catch {
    242     template<>
    243     struct StringMaker<CommandException> {
    244         static std::string convert(CommandException const& ce) {
    245             return Utility::Format("CommandException{ '{0}' }", ce.Message().get());
    246         }
    247     };
    248 }
    249 
    250 #define REQUIRE_COMMAND_EXCEPTION(_expr_, _arg_)     REQUIRE_THROWS_MATCHES(_expr_, CommandException, CommandExceptionMatcher(_arg_))
    251 
    252 void RequireValueParsedToArg(const std::string& value, const Argument& arg, const Args& args)
    253 {
    254     REQUIRE(args.Contains(arg.ExecArgType()));
    255     REQUIRE(value == args.GetArg(arg.ExecArgType()));
    256 }
    257 
    258 void RequireValuesParsedToArg(const std::vector<std::string>& values, Args::Type execArgType, const Args& args)
    259 {
    260     REQUIRE(args.Contains(execArgType));
    261     REQUIRE(args.GetCount(execArgType) == values.size());
    262 
    263     auto argValues = args.GetArgs(execArgType);
    264     for (size_t i = 0; i < values.size(); ++i)
    265     {
    266         REQUIRE(argValues->at(i) == values[i]);
    267     }
    268 }
    269 
    270 void RequireValuesParsedToArg(const std::vector<std::string>& values, const Argument& arg, const Args& args)
    271 {
    272     RequireValuesParsedToArg(values, arg.ExecArgType(), args);
    273 }
    274 
    275 // Description used for tests; doesn't need to be anything in particular.
    276 static constexpr CLI::Resource::StringId DefaultDesc{ L""sv };
    277 
    278 TEST_CASE("ParseArguments_MultiplePositional", "[command]")
    279 {
    280     Args args;
    281     TestCommand command({
    282             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    283             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    284             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    285         });
    286 
    287     std::vector<std::string> values{ "val1", "val2" };
    288     Invocation inv{ std::vector<std::string>(values) };
    289 
    290     command.ParseArguments(inv, args);
    291 
    292     RequireValueParsedToArg(values[0], command.m_args[0], args);
    293     RequireValueParsedToArg(values[1], command.m_args[2], args);
    294 }
    295 
    296 TEST_CASE("ParseArguments_ForcePositional", "[command]")
    297 {
    298     Args args;
    299     TestCommand command({
    300             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    301             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    302             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    303         });
    304 
    305     std::vector<std::string> values{ "val1", "--", "-std1" };
    306     Invocation inv{ std::vector<std::string>(values) };
    307 
    308     command.ParseArguments(inv, args);
    309 
    310     RequireValueParsedToArg(values[0], command.m_args[0], args);
    311     RequireValueParsedToArg(values[2], command.m_args[2], args);
    312 }
    313 
    314 TEST_CASE("ParseArguments_TooManyPositional", "[command]")
    315 {
    316     Args args;
    317     TestCommand command({
    318             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    319             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    320         });
    321 
    322     std::vector<std::string> values{ "val1", "--", "-std1" };
    323     Invocation inv{ std::vector<std::string>(values) };
    324 
    325     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::ExtraPositionalError(Utility::LocIndView{ values[2] }));
    326 }
    327 
    328 TEST_CASE("ParseArguments_InvalidChar", "[command]")
    329 {
    330     Args args;
    331     TestCommand command({
    332             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    333             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    334             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    335         });
    336 
    337     std::vector<std::string> values{ "val1", "-", "-std1" };
    338     Invocation inv{ std::vector<std::string>(values) };
    339 
    340     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidArgumentSpecifierError(Utility::LocIndView{ values[1] }));
    341 }
    342 
    343 TEST_CASE("ParseArguments_InvalidAlias", "[command]")
    344 {
    345     Args args;
    346     TestCommand command({
    347             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    348             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    349             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    350         });
    351 
    352     std::vector<std::string> values{ "val1", "-b", "-std1" };
    353     Invocation inv{ std::vector<std::string>(values) };
    354 
    355     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidAliasError(Utility::LocIndView{ values[1] }));
    356 }
    357 
    358 TEST_CASE("ParseArguments_MultiFlag", "[command]")
    359 {
    360     Args args;
    361     TestCommand command({
    362             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    363             Argument{ "flag1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Flag },
    364             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    365             Argument{ "flag2", 't', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    366         });
    367 
    368     std::vector<std::string> values{ "val1", "-st", "val2" };
    369     Invocation inv{ std::vector<std::string>(values) };
    370 
    371     command.ParseArguments(inv, args);
    372 
    373     REQUIRE(args.Contains(command.m_args[1].ExecArgType()));
    374     REQUIRE(args.Contains(command.m_args[3].ExecArgType()));
    375 }
    376 
    377 TEST_CASE("ParseArguments_FlagThenUnknown", "[command]")
    378 {
    379     Args args;
    380     TestCommand command({
    381             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    382             Argument{ "flag1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Flag },
    383             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    384             Argument{ "flag2", 't', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    385         });
    386 
    387     std::vector<std::string> values{ "val1", "-sr", "val2" };
    388     Invocation inv{ std::vector<std::string>(values) };
    389 
    390     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFoundError(Utility::LocIndView{ values[1] }));
    391 }
    392 
    393 TEST_CASE("ParseArguments_FlagThenNonFlag", "[command]")
    394 {
    395     Args args;
    396     TestCommand command({
    397             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    398             Argument{ "flag1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Flag },
    399             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    400             Argument{ "flag2", 't', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    401         });
    402 
    403     std::vector<std::string> values{ "val1", "-sp", "val2" };
    404     Invocation inv{ std::vector<std::string>(values) };
    405 
    406     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFlagError(Utility::LocIndView{ values[1] }));
    407 }
    408 
    409 TEST_CASE("ParseArguments_NameUsingAliasSpecifier", "[command]")
    410 {
    411     Args args;
    412     TestCommand command({
    413             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    414             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    415             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    416             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    417         });
    418 
    419     std::vector<std::string> values{ "another", "-flag1" };
    420     Invocation inv{ std::vector<std::string>(values) };
    421 
    422     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFoundError(Utility::LocIndView{ values[1] }));
    423 }
    424 
    425 TEST_CASE("ParseArguments_AliasWithAdjoinedValue", "[command]")
    426 {
    427     Args args;
    428     TestCommand command({
    429             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    430             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    431             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    432         });
    433 
    434     std::vector<std::string> values{ "-s=Val1" };
    435     Invocation inv{ std::vector<std::string>(values) };
    436 
    437     command.ParseArguments(inv, args);
    438 
    439     RequireValueParsedToArg(values[0].substr(3), command.m_args[1], args);
    440 }
    441 
    442 TEST_CASE("ParseArguments_AliasWithSeparatedValue", "[command]")
    443 {
    444     Args args;
    445     TestCommand command({
    446             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    447             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    448             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    449         });
    450 
    451     std::vector<std::string> values{ "-s", "Val1" };
    452     Invocation inv{ std::vector<std::string>(values) };
    453 
    454     command.ParseArguments(inv, args);
    455 
    456     RequireValueParsedToArg(values[1], command.m_args[1], args);
    457 }
    458 
    459 TEST_CASE("ParseArguments_AliasWithSeparatedValueMissing", "[command]")
    460 {
    461     Args args;
    462     TestCommand command({
    463             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    464             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    465             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    466         });
    467 
    468     std::vector<std::string> values{ "-s" };
    469     Invocation inv{ std::vector<std::string>(values) };
    470 
    471     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::MissingArgumentError(Utility::LocIndView{ values[0] }));
    472 }
    473 
    474 TEST_CASE("ParseArguments_NameWithAdjoinedValue", "[command]")
    475 {
    476     Args args;
    477     TestCommand command({
    478             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    479             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    480             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    481         });
    482 
    483     std::vector<std::string> values{ "--pos1=Val1" };
    484     Invocation inv{ std::vector<std::string>(values) };
    485 
    486     command.ParseArguments(inv, args);
    487 
    488     RequireValueParsedToArg(values[0].substr(7), command.m_args[0], args);
    489 }
    490 
    491 TEST_CASE("ParseArguments_AlternateNameWithAdjoinedValue", "[command]")
    492 {
    493     Args args;
    494     TestCommand command({
    495             Argument{ "pos1", 'p', "p1", Args::Type::Channel, DefaultDesc, ArgumentType::Positional},
    496             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    497             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    498         });
    499 
    500     std::vector<std::string> values{ "--p1=Val1" };
    501     Invocation inv{ std::vector<std::string>(values) };
    502 
    503     command.ParseArguments(inv, args);
    504 
    505     RequireValueParsedToArg(values[0].substr(5), command.m_args[0], args);
    506 }
    507 
    508 TEST_CASE("ParseArguments_NameFlag", "[command]")
    509 {
    510     Args args;
    511     TestCommand command({
    512             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    513             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    514             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    515             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    516         });
    517 
    518     std::vector<std::string> values{ "--flag1", "arbitrary" };
    519     Invocation inv{ std::vector<std::string>(values) };
    520 
    521     command.ParseArguments(inv, args);
    522 
    523     RequireValueParsedToArg(values[1], command.m_args[0], args);
    524     REQUIRE(args.Contains(command.m_args[3].ExecArgType()));
    525 }
    526 
    527 TEST_CASE("ParseArguments_NameFlagWithAdjoinedValue", "[command]")
    528 {
    529     Args args;
    530     TestCommand command({
    531             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    532             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    533             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    534             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    535         });
    536 
    537     std::vector<std::string> values{ "another", "--flag1=arbitrary" };
    538     Invocation inv{ std::vector<std::string>(values) };
    539 
    540     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::FlagContainAdjoinedError(Utility::LocIndView{ values[1] }));
    541 }
    542 
    543 TEST_CASE("ParseArguments_NameWithSeparatedValue", "[command]")
    544 {
    545     Args args;
    546     TestCommand command({
    547             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    548             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    549             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    550             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    551         });
    552 
    553     std::vector<std::string> values{ "--pos2", "arbitrary" };
    554     Invocation inv{ std::vector<std::string>(values) };
    555 
    556     command.ParseArguments(inv, args);
    557 
    558     RequireValueParsedToArg(values[1], command.m_args[2], args);
    559 }
    560 
    561 TEST_CASE("ParseArguments_NameWithSeparatedValueMissing", "[command]")
    562 {
    563     Args args;
    564     TestCommand command({
    565             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    566             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    567             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    568             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    569         });
    570 
    571     std::vector<std::string> values{ "--pos2" };
    572     Invocation inv{ std::vector<std::string>(values) };
    573 
    574     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::MissingArgumentError(Utility::LocIndView{ values[0] }));
    575 }
    576 
    577 TEST_CASE("ParseArguments_UnknownName", "[command]")
    578 {
    579     Args args;
    580     TestCommand command({
    581             Argument{ "pos1", 'p', Args::Type::Channel, DefaultDesc, ArgumentType::Positional },
    582             Argument{ "std1", 's', Args::Type::Command, DefaultDesc, ArgumentType::Standard },
    583             Argument{ "pos2", 'q', Args::Type::Count, DefaultDesc, ArgumentType::Positional },
    584             Argument{ "flag1", 'f', Args::Type::Exact, DefaultDesc, ArgumentType::Flag },
    585         });
    586 
    587     std::vector<std::string> values{ "another", "--nope" };
    588     Invocation inv{ std::vector<std::string>(values) };
    589 
    590     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidNameError(Utility::LocIndView{ values[1] }));
    591 }
    592 
    593 TEST_CASE("ParseArguments_PositionalWithMultipleValues", "[command]")
    594 {
    595     Args args;
    596     TestCommand command({
    597             Argument{ "multi", 'm', Args::Type::MultiQuery, DefaultDesc, ArgumentType::Positional }.SetCountLimit(5),
    598         });
    599 
    600     std::vector<std::string> values{ "value1" "value2", "value3" };
    601     Invocation inv{ std::vector<std::string>(values) };
    602 
    603     command.ParseArguments(inv, args);
    604     RequireValuesParsedToArg(values, command.m_args[0], args);
    605 }
    606 
    607 TEST_CASE("ParseArguments_PositionalWithMultipleValuesAndOtherArgs", "[command]")
    608 {
    609     Args args;
    610     TestCommand command({
    611             Argument{ "pos1", 'p', Args::Type::Source, DefaultDesc, ArgumentType::Positional },
    612             Argument{ "pos2", 'q', Args::Type::All, DefaultDesc, ArgumentType::Positional },
    613             Argument{ "multi", 'm', Args::Type::MultiQuery, DefaultDesc, ArgumentType::Positional }.SetCountLimit(5),
    614             Argument{ "flag", 'f', Args::Type::BlockingPin, DefaultDesc, ArgumentType::Flag },
    615         });
    616 
    617     std::vector<std::string> values{ "positional", "-q", "anotherPos", "multiValue1", "multiValue2", "-f" };
    618     Invocation inv{ std::vector<std::string>(values) };
    619 
    620     command.ParseArguments(inv, args);
    621     RequireValueParsedToArg(values[0], command.m_args[0], args);
    622     RequireValueParsedToArg(values[2], command.m_args[1], args);
    623     RequireValuesParsedToArg({ values[3], values[4] }, command.m_args[2], args);
    624     REQUIRE(args.Contains(command.m_args[3].ExecArgType()));
    625 }
    626 
    627 TEST_CASE("ParseArguments_PositionalWithMultipleValuesAndName", "[command]")
    628 {
    629     Args args;
    630     TestCommand command({
    631             Argument{ "multi", 'm', Args::Type::MultiQuery, DefaultDesc, ArgumentType::Positional }.SetCountLimit(5),
    632         });
    633 
    634     std::vector<std::string> values{ "--multi", "one", "two", "three" };
    635     Invocation inv{ std::vector<std::string>(values) };
    636 
    637     command.ParseArguments(inv, args);
    638     RequireValuesParsedToArg({ values[1], values[2], values[3] }, command.m_args[0], args);
    639 }
    640 
    641 TEST_CASE("ParseArguments_MultiQueryConvertedToSingleQuery", "[command]")
    642 {
    643     Args args;
    644     TestCommand command({
    645             Argument{ "multi", 'm', Args::Type::MultiQuery, DefaultDesc, ArgumentType::Positional }.SetCountLimit(5),
    646         });
    647 
    648     std::vector<std::string> values{ "singleValue" };
    649     Invocation inv{ std::vector<std::string>(values) };
    650 
    651     // ParseArguments converts MultiQuery args with a single value into Query args
    652     command.ParseArguments(inv, args);
    653     RequireValuesParsedToArg({ values[0] }, Args::Type::Query, args);
    654 }
    655 
    656 TEST_CASE("ParseArguments_PositionalWithTooManyValues", "[command]")
    657 {
    658     Args args;
    659     TestCommand command({
    660             Argument{ "multi", 'm', Args::Type::MultiQuery, DefaultDesc, ArgumentType::Positional }.SetCountLimit(5),
    661         });
    662 
    663     std::vector<std::string> values{ "1", "2", "3", "4", "5", "tooMany" };
    664     Invocation inv{ std::vector<std::string>(values) };
    665 
    666     REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::ExtraPositionalError(Utility::LocIndView{ values.back() }));
    667 }