/*++ Copyright (c) Microsoft. All rights reserved. Module Name: WSLCCLIExecutionUnitTests.cpp Abstract: This file contains unit tests for WSLC CLI command execution. --*/ #include "precomp.h" #include "windows/Common.h" #include "WSLCCLITestHelpers.h" #include "SessionModel.h" #include "AsyncExecution.h" #include "Command.h" #include "RootCommand.h" #include "ContainerCommand.h" #include "ContainerTasks.h" using namespace wsl::windows::wslc; using namespace WSLCTestHelpers; using namespace WEX::Logging; using namespace WEX::Common; using namespace WEX::TestExecution; namespace WSLCCLIExecutionUnitTests { // Helper structure to hold test data struct CommandLineTestCase { std::wstring commandLine; std::wstring expectedCommand; bool shouldSucceed; }; class WSLCCLIExecutionUnitTests { WSLC_TEST_CLASS(WSLCCLIExecutionUnitTests) TEST_CLASS_SETUP(TestClassSetup) { return true; } TEST_CLASS_CLEANUP(TestClassCleanup) { return true; } // Test: Verify EnumVariantMap on DataMap for Context Data TEST_METHOD(EnumVariantMap_DataMapValidation) { // DataMap is an EnumVariantMap, but for command execution context data instead of arguments. // It does not have rigid typing like the Args map, so this will verify every Data enum value // can be added and retrieved successfully. The arguments unit tests have more complex tests // for the EnumVariantMap behavior. This one ensures Data enum values are correct. wsl::windows::wslc::execution::DataMap dataMap; // Verify all data enum values defined. auto allDataTypes = std::vector{}; for (int i = 0; i < static_cast(Data::Max); ++i) { Data dataType = static_cast(i); // Add the data to the DataMap with a test value based on its type. // Each data type needs to be added here as each enum may have its own value. VERIFY_IS_FALSE(dataMap.Contains(dataType)); bool handled = false; if (dataType == Data::Session) { // Create a null session for testing - Session requires a COM pointer wil::com_ptr nullSession; // Creates null COM pointer wsl::windows::wslc::models::Session session{nullSession}; dataMap.Add(std::move(session)); handled = true; } else if (dataType == Data::Containers) { std::vector containers; dataMap.Add(std::move(containers)); handled = true; } else if (dataType == Data::ContainerOptions) { wsl::windows::wslc::models::ContainerOptions options; dataMap.Add(std::move(options)); handled = true; } else if (dataType == Data::Images) { std::vector images; dataMap.Add(std::move(images)); handled = true; } else if (dataType == Data::Volumes) { std::vector volumes; dataMap.Add(std::move(volumes)); handled = true; } if (!handled) { VERIFY_FAIL(L"Unhandled Data type in test"); } allDataTypes.push_back(dataType); VERIFY_IS_TRUE(dataMap.Contains(dataType)); } // Verify basic retrieval. auto& session = dataMap.Get(); VERIFY_IS_NULL(session.Get()); // A null ptr was added. auto& containers = dataMap.Get(); VERIFY_ARE_EQUAL(0u, containers.size()); // Other more complex EnumVariantMap tests are in the Args unit tests. // This one will just verify all the data types in the Data Map work as expected. } // Test: SetContainerOptionsFromArgs sets WorkingDirectory when --workdir is provided TEST_METHOD(SetContainerOptionsFromArgs_WithWorkDir_SetsWorkingDirectory) { CLIExecutionContext context; context.Args.Add(std::wstring{L"/app"}); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory); } // Test: SetContainerOptionsFromArgs leaves WorkingDirectory empty when --workdir is not provided TEST_METHOD(SetContainerOptionsFromArgs_WithoutWorkDir_WorkingDirectoryIsEmpty) { CLIExecutionContext context; wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_IS_TRUE(options.WorkingDirectory.empty()); } // Test: Full parse of 'exec --workdir "" cont1 cmd' rejects empty working directory TEST_METHOD(ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException) { // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts. auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" cont1 sh"); ContainerExecCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); VERIFY_THROWS_SPECIFIC( command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } // Test: Full parse of 'exec --workdir /path cont1 cmd' sets WorkingDirectory TEST_METHOD(ExecCommand_ParseWorkDirLongOption_SetsWorkingDirectory) { // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts. auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir cont1 sh"); ContainerExecCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory); } // Test: Full parse of 'exec -w /path cont1 cmd' (short alias) sets WorkingDirectory TEST_METHOD(ExecCommand_ParseWorkDirShortOption_SetsWorkingDirectory) { auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app cont1 sh"); ContainerExecCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory); } // Test: Full parse of 'run --workdir "" image cmd' rejects empty working directory TEST_METHOD(RunCommand_ParseWorkDirEmptyValue_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh"); ContainerRunCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); VERIFY_THROWS_SPECIFIC( command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } // Test: Full parse of 'run --workdir /path image cmd' sets WorkingDirectory TEST_METHOD(RunCommand_ParseWorkDirLongOption_SetsWorkingDirectory) { auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh"); ContainerRunCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory); } // Test: Full parse of 'run -w /path image cmd' (short alias) sets WorkingDirectory TEST_METHOD(RunCommand_ParseWorkDirShortOption_SetsWorkingDirectory) { auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh"); ContainerRunCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory); } // Test: Full parse of 'create --workdir "" image cmd' rejects empty working directory TEST_METHOD(CreateCommand_ParseWorkDirEmptyValue_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh"); ContainerCreateCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); VERIFY_THROWS_SPECIFIC( command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } // Test: Full parse of 'create --workdir /path image cmd' sets WorkingDirectory TEST_METHOD(CreateCommand_ParseWorkDirLongOption_SetsWorkingDirectory) { auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh"); ContainerCreateCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory); } // Test: Full parse of 'create -w /path image cmd' (short alias) sets WorkingDirectory TEST_METHOD(CreateCommand_ParseWorkDirShortOption_SetsWorkingDirectory) { auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh"); ContainerCreateCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory); } TEST_METHOD(RunCommand_ParseGpusAll_SetsGpuOption) { auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh"); ContainerRunCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_IS_TRUE(options.Gpu); } TEST_METHOD(RunCommand_ParseGpusInvalid_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus invalid ubuntu sh"); ContainerRunCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); VERIFY_THROWS_SPECIFIC( command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } TEST_METHOD(CreateCommand_ParseGpusAll_SetsGpuOption) { auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh"); ContainerCreateCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); command.ValidateArguments(context.Args); wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); const auto& options = context.Data.Get(); VERIFY_IS_TRUE(options.Gpu); } TEST_METHOD(CreateCommand_ParseGpusInvalid_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus none ubuntu sh"); ContainerCreateCommand command{L""}; CLIExecutionContext context; command.ParseArguments(invocation, context.Args); VERIFY_THROWS_SPECIFIC( command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } // Test: Command Line test parsing all cases defined in CommandLineTestCases.h // This test verifies the command line parsing logic used by the CLI and executes the same // code as the CLI up to the point of command execution, including parsing and argument validtion. // It does not actually verify the execution of the command, just that the correct command is // found and the provided command line parsed correctly according to the command's defined arguments, // and the argument validation rules are correctly applied. The test cases are defined in // CommandLineTestCases.h and cover various valid and invalid command lines. TEST_METHOD(CommandLineParsing_AllCases) { std::vector testCases = { #define COMMAND_LINE_TEST_CASE(cmdLine, expectedCmd, shouldPass) {cmdLine, expectedCmd, shouldPass}, #include "CommandLineTestCases.h" #undef COMMAND_LINE_TEST_CASE }; // Run all test cases for (const auto& testCase : testCases) { LogComment(L"Testing: " + testCase.commandLine); // Pre-pend executable name, which will get stripped off by CommandLineToArgvW auto fullCommandLine = L"wslc " + testCase.commandLine; // Process the command line as Windows does. int argc = 0; auto argv = CommandLineToArgvW(fullCommandLine.c_str(), &argc); std::vector args; for (int i = 1; i < argc; ++i) { args.emplace_back(argv[i]); } // And now process the command line like WSLC does. bool succeeded = true; try { Invocation invocation{std::move(args)}; std::unique_ptr command = std::make_unique(); std::unique_ptr subCommand = command->FindSubCommand(invocation); while (subCommand) { command = std::move(subCommand); subCommand = command->FindSubCommand(invocation); } // Ensure we found the expected command VERIFY_ARE_EQUAL(testCase.expectedCommand, command->Name()); CLIExecutionContext context; // Parse and validate and compare to expected results. command->ParseArguments(invocation, context.Args); command->ValidateArguments(context.Args); } catch (const CommandException& ce) { LogComment(L"Command line parsing threw an exception: " + ce.Message()); succeeded = false; } catch (...) { LogComment(L"Command line parsing threw an unexpected exception."); succeeded = false; } VERIFY_ARE_EQUAL(testCase.shouldSucceed, succeeded); } } }; class ForEachAsyncUnitTests { WSLC_TEST_CLASS(ForEachAsyncUnitTests) TEST_METHOD(ForEachAsync_SuccessCallbackInvokedForAllItems) { const std::vector items = {1, 2, 3, 4, 5}; std::vector results; ForEachAsync( items, [](int item) { return item * 2; }, [&](int result) { results.push_back(result); }, [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); }); VERIFY_ARE_EQUAL(items.size(), results.size()); for (int item : items) { VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item * 2) != results.end()); } } TEST_METHOD(ForEachAsync_ErrorCallbackInvokedOnFailure) { const std::vector items = {1, 2, 3}; std::vector failedItems; std::vector succeededItems; ForEachAsync( items, [](int item) -> int { if (item == 2) { THROW_HR(E_FAIL); } return item; }, [&](int result) { succeededItems.push_back(result); }, [&](int item, wil::ResultException /*error*/) { failedItems.push_back(item); }); VERIFY_ARE_EQUAL(1u, failedItems.size()); VERIFY_ARE_EQUAL(2, failedItems[0]); VERIFY_ARE_EQUAL(2u, succeededItems.size()); } TEST_METHOD(ForEachAsync_EmptyInputProducesNoCallbacks) { const std::vector items; bool successCalled = false; bool errorCalled = false; ForEachAsync( items, [](int item) { return item; }, [&](int /*result*/) { successCalled = true; }, [&](int /*item*/, wil::ResultException /*error*/) { errorCalled = true; }); VERIFY_IS_FALSE(successCalled); VERIFY_IS_FALSE(errorCalled); } TEST_METHOD(ForEachAsync_BatchSizeOfOneProcessesAllItems) { const std::vector items = {10, 20, 30, 40, 50}; std::vector results; ForEachAsync( items, [](int item) { return item; }, [&](int result) { results.push_back(result); }, [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); }, /*batchSize=*/1); VERIFY_ARE_EQUAL(items.size(), results.size()); for (int item : items) { VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item) != results.end()); } } TEST_METHOD(ForEachAsync_ErrorInOnErrorPropagatesThrow) { const std::vector items = {1}; VERIFY_THROWS_SPECIFIC( ForEachAsync( items, [](int /*item*/) -> int { THROW_HR(E_ACCESSDENIED); }, [](int /*result*/) {}, [](int /*item*/, wil::ResultException error) { throw error; }), wil::ResultException, [](const wil::ResultException& ex) { return ex.GetErrorCode() == E_ACCESSDENIED; }); } }; } // namespace WSLCCLIExecutionUnitTests