mirror of
https://github.com/aphrody-labs/WSL.git
synced 2026-10-17 12:45:55 +00:00
721 lines
29 KiB
C++
721 lines
29 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
WSLCCLIParserUnitTests.cpp
|
|
|
|
Abstract:
|
|
|
|
This file contains unit tests for WSLC CLI argument parsing and validation.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "windows/Common.h"
|
|
#include "WSLCCLITestHelpers.h"
|
|
|
|
#include "Argument.h"
|
|
#include "ArgumentConvertedTypes.h"
|
|
#include "ArgMap.h"
|
|
#include "ArgumentParser.h"
|
|
#include "Invocation.h"
|
|
#include "ParserTestCases.h"
|
|
|
|
using namespace wsl::windows::wslc;
|
|
using namespace wsl::windows::wslc::argument;
|
|
|
|
using namespace WSLCTestHelpers;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::Common;
|
|
using namespace WEX::TestExecution;
|
|
|
|
namespace WSLCCLIParserUnitTests {
|
|
|
|
using Argument = wsl::windows::wslc::Argument;
|
|
|
|
class WSLCCLIParserUnitTests
|
|
{
|
|
WSLC_TEST_CLASS(WSLCCLIParserUnitTests)
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD(ParserTest_ParserCases)
|
|
{
|
|
std::vector<ParserTestCase> testCases = {
|
|
#define WSLC_PARSER_TEST_CASE(argSetValue, expected, cmdLine) {ArgumentSet::argSetValue, expected, cmdLine},
|
|
WSLC_PARSER_TEST_CASES
|
|
#undef WSLC_PARSER_TEST_CASE
|
|
};
|
|
|
|
for (const auto& testCase : testCases)
|
|
{
|
|
bool succeeded = false;
|
|
const bool optionsOnly = IsOptionsOnlySet(testCase.argumentSet);
|
|
|
|
try
|
|
{
|
|
Log::Comment(String().Format(L"Testing: %ls", testCase.commandLine.c_str()));
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(testCase.commandLine);
|
|
|
|
std::vector<Argument> definedArgs = GetArgumentsForSet(testCase.argumentSet);
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine stateMachine{inv, args, std::move(definedArgs), optionsOnly};
|
|
while (stateMachine.Step())
|
|
{
|
|
stateMachine.ThrowIfError();
|
|
}
|
|
// Step() returns false on stop in optionsOnly mode without surfacing any
|
|
// pending error, so drain once more to convert "missing value at EOF"
|
|
// into a thrown ArgumentException.
|
|
stateMachine.ThrowIfError();
|
|
|
|
const bool helpRequested = args.Contains(ArgType::Help) && args.GetValue<ArgType::Help>();
|
|
if (!helpRequested)
|
|
{
|
|
for (const auto& arg : GetArgumentsForSet(testCase.argumentSet))
|
|
{
|
|
// Required-arg enforcement is a *whole-command-line* concern.
|
|
// In optionsOnly mode the parser only saw part of the input, so
|
|
// missing values would be reported by the second (subcommand)
|
|
// pass, not this one. Skip required checks for those sets.
|
|
if (!optionsOnly && arg.Required() && !args.Contains(arg.Type()))
|
|
{
|
|
throw ArgumentException(std::wstring(L"Required argument missing: ") + arg.Name());
|
|
}
|
|
|
|
if (args.Contains(arg.Type()))
|
|
{
|
|
arg.Validate(args);
|
|
}
|
|
}
|
|
}
|
|
|
|
succeeded = true;
|
|
|
|
if (!helpRequested && testCase.commandLine.find(L"image1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::ImageId));
|
|
auto imageId = args.GetValue<ArgType::ImageId>();
|
|
VERIFY_ARE_EQUAL(L"image1", imageId);
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"cont1") != std::wstring::npos && testCase.argumentSet == ArgumentSet::List)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::ContainerId));
|
|
auto containerId = args.GetValue<ArgType::ContainerId>();
|
|
VERIFY_ARE_EQUAL(L"cont1", containerId);
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"--rm") != std::wstring::npos)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Remove));
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"command") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Command));
|
|
auto command = args.GetValue<ArgType::Command>();
|
|
VERIFY_IS_TRUE(command.find(L"command") != std::wstring::npos);
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"forward") != std::wstring::npos && testCase.argumentSet == ArgumentSet::Run)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::ForwardArgs));
|
|
auto forwardArgs = args.GetValue<ArgType::ForwardArgs>();
|
|
std::wstring forwardArgsConcat = wsl::shared::string::Join(forwardArgs, L' ');
|
|
VERIFY_IS_TRUE(forwardArgsConcat.find(L"hello world") != std::wstring::npos); // Forward args should contain hello world
|
|
VERIFY_IS_TRUE(forwardArgsConcat.find(L"image1") == std::wstring::npos); // Forward args should not contain the imageId
|
|
VERIFY_IS_TRUE(forwardArgsConcat.find(L"command") == std::wstring::npos); // Forward args should not contain the command
|
|
LogComment(L"Forwarded Args: " + forwardArgsConcat);
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"443") != std::wstring::npos)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Publish));
|
|
auto publishArgs = args.GetAllValues<ArgType::Publish>();
|
|
VERIFY_ARE_EQUAL(2, publishArgs.size()); // Should have both publish args
|
|
VERIFY_ARE_NOT_EQUAL(publishArgs[0], publishArgs[1]); // Both publish args should be different
|
|
}
|
|
|
|
// Globals-specific spot checks: the synthetic global flag
|
|
// (--quiet / -q) and value (--session, see GetArgumentsForSet)
|
|
// must have been consumed by the global parser when present,
|
|
// and the stop position must point at the first non-global
|
|
// token (or end()).
|
|
if (testCase.argumentSet == ArgumentSet::Globals)
|
|
{
|
|
if (testCase.commandLine.find(L"--quiet") != std::wstring::npos || testCase.commandLine.find(L"-q") != std::wstring::npos)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Quiet));
|
|
}
|
|
|
|
if (testCase.commandLine.find(L"--session") != std::wstring::npos)
|
|
{
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Session));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"foo"), args.GetValue<ArgType::Session>());
|
|
}
|
|
}
|
|
}
|
|
catch (ArgumentException& ex)
|
|
{
|
|
if (testCase.expectedResult)
|
|
{
|
|
VERIFY_FAIL(String().Format(L"Test case threw unexpected argument exception: %ls", ex.Message().c_str()));
|
|
}
|
|
else
|
|
{
|
|
Log::Comment(String().Format(L"Test case threw expected argument exception: %ls", ex.Message().c_str()));
|
|
}
|
|
}
|
|
catch (std::exception& ex)
|
|
{
|
|
if (testCase.expectedResult)
|
|
{
|
|
VERIFY_FAIL(String().Format(L"Test case threw unexpected exception: %hs", ex.what()));
|
|
}
|
|
else
|
|
{
|
|
Log::Comment(String().Format(L"Test case threw expected exception: %hs", ex.what()));
|
|
}
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(testCase.expectedResult, succeeded, String().Format(L"Command line: %ls", testCase.commandLine.c_str()));
|
|
}
|
|
}
|
|
|
|
// Options-only mode: parser consumes leading options and stops at the first
|
|
// positional without consuming it. inv must advance past consumed options.
|
|
TEST_METHOD(OptionsOnly_StopsAtFirstPositional)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose image1 command");
|
|
|
|
std::vector<Argument> defs = {
|
|
Argument::Create(ArgType::Verbose),
|
|
Argument::Create(ArgType::NoColor),
|
|
};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_IS_FALSE(args.Contains(ArgType::NoColor));
|
|
|
|
// Position points at the first positional ("image1") so the next pass
|
|
// can resume from there by updating the invocation cursor position.
|
|
auto pos = sm.Position();
|
|
VERIFY_IS_TRUE(pos != inv.end());
|
|
VERIFY_ARE_EQUAL(std::wstring(L"image1"), *pos);
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_NoOptionsPresent_StopsImmediately)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc image1");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_OnlyOptions_PositionEqualsEnd)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_IS_TRUE(sm.Position() == inv.end());
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_AdjoinedValue_IsConsumed)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal=9 image1");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
|
|
VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, args.GetValue<ArgType::Signal>());
|
|
VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_SeparatedValue_IsConsumed)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 image1");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Signal));
|
|
VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, args.GetValue<ArgType::Signal>());
|
|
VERIFY_ARE_EQUAL(std::wstring(L"image1"), *sm.Position());
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_UnknownOption_Throws)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --doesnotexist image1");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
bool threw = false;
|
|
try
|
|
{
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
}
|
|
catch (const ArgumentException& exception)
|
|
{
|
|
threw = true;
|
|
VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_InvalidNameError(L"--doesnotexist"), exception.Message());
|
|
}
|
|
|
|
VERIFY_IS_TRUE(threw);
|
|
}
|
|
|
|
TEST_METHOD(OptionsOnly_ValueAtEndOfInput_Throws)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
|
|
|
|
ArgMap args;
|
|
bool threw = false;
|
|
try
|
|
{
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
}
|
|
catch (const ArgumentException& exception)
|
|
{
|
|
threw = true;
|
|
VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_MissingArgumentError(L"--signal"), exception.Message());
|
|
}
|
|
|
|
VERIFY_IS_TRUE(threw);
|
|
}
|
|
|
|
// After options-only stops, the invocation cursor points at the remaining
|
|
// tokens for a second parse pass, matching the production parser flow.
|
|
TEST_METHOD(OptionsOnly_TwoPassParseAcrossPositional)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose image1 --signal 9");
|
|
|
|
// Pass 1: options-only with just Verbose visible.
|
|
std::vector<Argument> globalDefs = {Argument::Create(ArgType::Verbose)};
|
|
ArgMap globals;
|
|
ParseArgumentsStateMachine sm1{inv, globals, std::move(globalDefs), /*optionsOnly*/ true};
|
|
while (sm1.Step())
|
|
{
|
|
sm1.ThrowIfError();
|
|
}
|
|
inv.SetPosition(sm1.Position());
|
|
|
|
VERIFY_IS_TRUE(globals.Contains(ArgType::Verbose));
|
|
VERIFY_IS_FALSE(globals.Contains(ArgType::Signal));
|
|
|
|
// Pass 2: full mode with the subcommand's argument set.
|
|
std::vector<Argument> subDefs = {
|
|
Argument::Create(ArgType::ImageId, {.Required = true}),
|
|
Argument::Create(ArgType::Signal),
|
|
};
|
|
ArgMap subArgs;
|
|
ParseArgumentsStateMachine sm2{inv, subArgs, std::move(subDefs), /*optionsOnly*/ false};
|
|
while (sm2.Step())
|
|
{
|
|
sm2.ThrowIfError();
|
|
}
|
|
|
|
VERIFY_IS_TRUE(subArgs.Contains(ArgType::ImageId));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"image1"), subArgs.GetValue<ArgType::ImageId>());
|
|
VERIFY_IS_TRUE(subArgs.Contains(ArgType::Signal));
|
|
VERIFY_ARE_EQUAL(WSLCSignalSIGKILL, subArgs.GetValue<ArgType::Signal>());
|
|
}
|
|
|
|
// stopOnUnknown: unknown -alias / --name / lone '-' / bare '--' tokens
|
|
// back the iterator up and stop cleanly instead of throwing. Recognized
|
|
// options before the unknown one are still consumed.
|
|
TEST_METHOD(OptionsOnly_StopOnUnknown_LeavesTokenForCaller)
|
|
{
|
|
// Case 1: leading unknown --name. Nothing consumed, position == begin.
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --doesnotexist image1");
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"--doesnotexist"), *sm.Position());
|
|
}
|
|
|
|
// Case 2: leading unknown -alias. Same backing-up behavior.
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc -X image1");
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_IS_FALSE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"-X"), *sm.Position());
|
|
}
|
|
|
|
// Case 3: recognized option consumed, then unknown stops the scan.
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose --doesnotexist image1");
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"--doesnotexist"), *sm.Position());
|
|
}
|
|
|
|
// Case 4: lone '-' with no positionals defined backs up instead of erroring.
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc -");
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs), /*optionsOnly*/ true, /*stopOnUnknown*/ true};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_ARE_EQUAL(std::wstring(L"-"), *sm.Position());
|
|
}
|
|
}
|
|
|
|
// Duplicate flag on the CLI (no env preload) folds to one entry: docker-style.
|
|
TEST_METHOD(DuplicateFlagOnCli_IsIdempotent)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose --verbose");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, defs};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
|
|
VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
|
|
}
|
|
|
|
// Duplicate single-value arg on the CLI (no preload) is last-wins (docker-style):
|
|
// the final value replaces the earlier one instead of accumulating.
|
|
TEST_METHOD(DuplicateValueOnCli_LastWins)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --signal 9 --signal 1");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Signal)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, defs};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Signal));
|
|
VERIFY_ARE_EQUAL(WSLCSignalSIGHUP, args.GetValue<ArgType::Signal>());
|
|
}
|
|
|
|
// Unlimited value args are exempt from last-wins: every CLI occurrence accumulates.
|
|
TEST_METHOD(UnlimitedValueOnCli_Accumulates)
|
|
{
|
|
ArgMap args =
|
|
ParseFlags(L"wslc --publish 80:80 --publish 443:443", {Argument::Create(ArgType::Publish, {.Limit = Limit::Unlimited})});
|
|
|
|
VERIFY_ARE_EQUAL(2u, args.Count(ArgType::Publish));
|
|
}
|
|
|
|
// Boolean flags store their explicit parsed value: present with true or false when the
|
|
// flag is specified, absent when it is not. Consumers read them via ArgMap::GetValue(defaultValue),
|
|
// which returns the stored value if present or a caller-supplied default if absent. The
|
|
// helper parses a single command line against the supplied defs and returns the resulting
|
|
// ArgMap so each case can assert the stored flag value.
|
|
static ArgMap ParseFlags(const std::wstring& commandLine, std::vector<Argument> defs)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(commandLine);
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs)};
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
return args;
|
|
}
|
|
|
|
// "--flag" and every recognized true form store a single true entry. The single-letter
|
|
// "t"/"T" forms are Docker-parity extensions enabled for the CLI flag path.
|
|
TEST_METHOD(Flag_TrueForms_StoreSingleTrueEntry)
|
|
{
|
|
for (const auto* cmd :
|
|
{L"wslc --verbose",
|
|
L"wslc --verbose=true",
|
|
L"wslc --verbose=1",
|
|
L"wslc --verbose=TRUE",
|
|
L"wslc --verbose=t",
|
|
L"wslc --verbose=T"})
|
|
{
|
|
Log::Comment(String().Format(L"Testing: %ls", cmd));
|
|
ArgMap args = ParseFlags(cmd, {Argument::Create(ArgType::Verbose)});
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
|
|
VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
|
|
}
|
|
}
|
|
|
|
// Every recognized false form stores the flag present with value false (a docker-style
|
|
// "--flag=false"), so Contains() is true but GetValue() reports false. The single-letter
|
|
// "f"/"F" forms are Docker-parity extensions enabled for the CLI flag path.
|
|
TEST_METHOD(Flag_FalseForms_StoreSingleFalseEntry)
|
|
{
|
|
for (const auto* cmd :
|
|
{L"wslc --verbose=false", L"wslc --verbose=0", L"wslc --verbose=False", L"wslc --verbose=f", L"wslc --verbose=F"})
|
|
{
|
|
Log::Comment(String().Format(L"Testing: %ls", cmd));
|
|
ArgMap args = ParseFlags(cmd, {Argument::Create(ArgType::Verbose)});
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::Verbose));
|
|
VERIFY_IS_FALSE(args.GetValue<ArgType::Verbose>());
|
|
}
|
|
}
|
|
|
|
// A non-boolean adjoined value is an error rather than being silently ignored.
|
|
TEST_METHOD(Flag_InvalidBoolean_Throws)
|
|
{
|
|
auto inv = WSLCTestHelpers::CreateInvocationFromCommandLine(L"wslc --verbose=maybe");
|
|
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Verbose)};
|
|
|
|
ArgMap args;
|
|
ParseArgumentsStateMachine sm{inv, args, std::move(defs)};
|
|
|
|
bool threw = false;
|
|
try
|
|
{
|
|
while (sm.Step())
|
|
{
|
|
sm.ThrowIfError();
|
|
}
|
|
sm.ThrowIfError();
|
|
}
|
|
catch (const ArgumentException& exception)
|
|
{
|
|
threw = true;
|
|
VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_FlagInvalidBooleanError(L"--verbose=maybe"), exception.Message());
|
|
}
|
|
|
|
VERIFY_IS_TRUE(threw);
|
|
}
|
|
|
|
// Docker parity: a space-separated token after a boolean flag is NOT consumed as the
|
|
// flag's value; the flag is true and the token becomes the next positional.
|
|
TEST_METHOD(Flag_SpaceSeparatedValue_StaysPositional)
|
|
{
|
|
ArgMap args = ParseFlags(
|
|
L"wslc --verbose true",
|
|
{Argument::Create(ArgType::Verbose), Argument::Create(ArgType::ContainerId, {.Limit = Limit::Unlimited})});
|
|
|
|
VERIFY_IS_TRUE(args.Contains(ArgType::Verbose));
|
|
VERIFY_IS_TRUE(args.GetValue<ArgType::Verbose>());
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::ContainerId));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"true"), args.GetValue<ArgType::ContainerId>());
|
|
}
|
|
|
|
// A value argument's alias carries its value exactly like the long name, including the
|
|
// adjoined form. The inspect family depends on this for docker's `-f json`.
|
|
TEST_METHOD(Value_AliasCarriesValue)
|
|
{
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::InspectFormat), Argument::Create(ArgType::ObjectId, {.Limit = Limit::Unlimited})};
|
|
|
|
VERIFY_ARE_EQUAL(wsl::shared::c_jsonCompactIndent, ParseFlags(L"wslc -f json cont1", defs).GetValue<ArgType::InspectFormat>());
|
|
VERIFY_ARE_EQUAL(wsl::shared::c_jsonCompactIndent, ParseFlags(L"wslc -f=json cont1", defs).GetValue<ArgType::InspectFormat>());
|
|
|
|
// The positional still lands where it belongs once the alias has taken its value.
|
|
auto args = ParseFlags(L"wslc -f json cont1", defs);
|
|
VERIFY_ARE_EQUAL(1u, args.Count(ArgType::ObjectId));
|
|
VERIFY_ARE_EQUAL(std::wstring(L"cont1"), args.GetValue<ArgType::ObjectId>());
|
|
}
|
|
|
|
// Alias forms honor adjoined booleans just like the long name.
|
|
TEST_METHOD(Flag_AliasAdjoinedBoolean)
|
|
{
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q=true", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc -q=false", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
}
|
|
|
|
TEST_METHOD(AliasOverride_ControlsCommandAlias)
|
|
{
|
|
const std::vector<Argument> customAlias{Argument::Create(ArgType::Quiet, {.Alias = L"x"})};
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -x", customAlias).GetValue<ArgType::Quiet>());
|
|
VERIFY_THROWS(ParseFlags(L"wslc -q", customAlias), ArgumentException);
|
|
|
|
const std::vector<Argument> noAlias{Argument::Create(ArgType::Quiet, {.Alias = NO_ALIAS})};
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc --quiet", noAlias).GetValue<ArgType::Quiet>());
|
|
VERIFY_THROWS(ParseFlags(L"wslc -q", noAlias), ArgumentException);
|
|
}
|
|
|
|
TEST_METHOD(NameOverride_ControlsCommandName)
|
|
{
|
|
const std::vector<Argument> definitions{Argument::Create(ArgType::Quiet, {.Name = L"silent"})};
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc --silent", definitions).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q", definitions).GetValue<ArgType::Quiet>());
|
|
VERIFY_THROWS(ParseFlags(L"wslc --quiet", definitions), ArgumentException);
|
|
}
|
|
|
|
// Docker-parity single-letter forms ("t"/"T"/"f"/"F") are honored on the alias form too.
|
|
TEST_METHOD(Flag_AliasShortBooleanForms)
|
|
{
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q=t", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q=T", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc -q=f", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc -q=F", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
}
|
|
|
|
// An adjoined boolean value may be wrapped in double quotes (e.g. --flag="true"), just like
|
|
// an adjoined value argument. The quotes are stripped before the boolean is parsed, on both
|
|
// the named and alias forms.
|
|
TEST_METHOD(Flag_QuotedAdjoinedBoolean)
|
|
{
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"true\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"false\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc -q=\"true\"", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc -q=\"false\"", {Argument::Create(ArgType::Quiet)}).GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_TRUE(ParseFlags(L"wslc --verbose=\"t\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
|
|
VERIFY_IS_FALSE(ParseFlags(L"wslc --verbose=\"f\"", {Argument::Create(ArgType::Verbose)}).GetValue<ArgType::Verbose>());
|
|
}
|
|
|
|
// In an alias chain, leading flags are true and a trailing "=false" turns only the
|
|
// last flag off.
|
|
TEST_METHOD(Flag_AliasChain_TrailingFalse)
|
|
{
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Quiet), Argument::Create(ArgType::Interactive)};
|
|
|
|
ArgMap all = ParseFlags(L"wslc -qi", defs);
|
|
VERIFY_IS_TRUE(all.GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_TRUE(all.GetValue<ArgType::Interactive>());
|
|
|
|
ArgMap trailingFalse = ParseFlags(L"wslc -qi=false", defs);
|
|
VERIFY_IS_TRUE(trailingFalse.GetValue<ArgType::Quiet>());
|
|
VERIFY_IS_FALSE(trailingFalse.GetValue<ArgType::Interactive>());
|
|
}
|
|
|
|
// Repeated flags are last-wins (matching docker) and never accumulate multiple entries:
|
|
// "--flag --flag=false" ends up false, the reverse ends up true. The flag is stored either
|
|
// way (a single entry), so GetValue() reports the winning value.
|
|
TEST_METHOD(Flag_Repeated_LastWins)
|
|
{
|
|
ArgMap trueThenFalse = ParseFlags(L"wslc --verbose --verbose=false", {Argument::Create(ArgType::Verbose)});
|
|
VERIFY_IS_FALSE(trueThenFalse.GetValue<ArgType::Verbose>());
|
|
VERIFY_ARE_EQUAL(1u, trueThenFalse.Count(ArgType::Verbose));
|
|
|
|
ArgMap falseThenTrue = ParseFlags(L"wslc --verbose=false --verbose", {Argument::Create(ArgType::Verbose)});
|
|
VERIFY_IS_TRUE(falseThenTrue.GetValue<ArgType::Verbose>());
|
|
VERIFY_ARE_EQUAL(1u, falseThenTrue.Count(ArgType::Verbose));
|
|
|
|
ArgMap duplicateTrue = ParseFlags(L"wslc --verbose --verbose=true", {Argument::Create(ArgType::Verbose)});
|
|
VERIFY_ARE_EQUAL(1u, duplicateTrue.Count(ArgType::Verbose));
|
|
}
|
|
|
|
// A flag whose behavior is on by default is read with GetValue(true): absent yields the
|
|
// default (true), "--flag=false" yields false, and "--flag" yields true. A bare Contains()
|
|
// cannot express this: it reports true for both "--flag" and "--flag=false" and false when
|
|
// the flag is absent, so it distinguishes neither the two stored values nor absent-as-default.
|
|
TEST_METHOD(Flag_GetValueDefaultTrue_DefaultOnFlag)
|
|
{
|
|
std::vector<Argument> defs = {Argument::Create(ArgType::Remove)};
|
|
|
|
ArgMap absent = ParseFlags(L"wslc", defs);
|
|
VERIFY_IS_FALSE(absent.Contains(ArgType::Remove));
|
|
VERIFY_IS_TRUE(absent.GetValue<ArgType::Remove>(true));
|
|
|
|
ArgMap disabled = ParseFlags(L"wslc --rm=false", defs);
|
|
VERIFY_IS_TRUE(disabled.Contains(ArgType::Remove));
|
|
VERIFY_IS_FALSE(disabled.GetValue<ArgType::Remove>(true));
|
|
|
|
ArgMap enabled = ParseFlags(L"wslc --rm", defs);
|
|
VERIFY_IS_TRUE(enabled.GetValue<ArgType::Remove>(true));
|
|
}
|
|
};
|
|
|
|
} // namespace WSLCCLIParserUnitTests
|