diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp index 4150d0af5..f911f6c60 100644 --- a/src/windows/wslc/commands/ContainerCreateCommand.cpp +++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp @@ -53,6 +53,7 @@ std::vector ContainerCreateCommand::GetArguments() const Argument::Create(ArgType::User), Argument::Create(ArgType::Volume, false, NO_LIMIT), // Argument::Create(ArgType::Virtual), + Argument::Create(ArgType::WorkDir), }; // clang-format on } diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp index e2810cd5f..548c2d049 100644 --- a/src/windows/wslc/commands/ContainerRunCommand.cpp +++ b/src/windows/wslc/commands/ContainerRunCommand.cpp @@ -54,6 +54,7 @@ std::vector ContainerRunCommand::GetArguments() const Argument::Create(ArgType::User), Argument::Create(ArgType::Volume, false, NO_LIMIT), // Argument::Create(ArgType::Virtual), + Argument::Create(ArgType::WorkDir), }; // clang-format on } diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index b6da3b35a..217796df0 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -101,6 +101,11 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& sessio containerLauncher.SetUser(std::move(user)); } + if (!options.WorkingDirectory.empty()) + { + containerLauncher.SetWorkingDirectory(std::string(options.WorkingDirectory)); + } + for (const auto& tmpfsSpec : options.Tmpfs) { auto tmpfsMount = TmpfsMount::Parse(tmpfsSpec); diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h index fa3c8cb4d..ca482ae14 100644 --- a/test/windows/wslc/CommandLineTestCases.h +++ b/test/windows/wslc/CommandLineTestCases.h @@ -83,6 +83,16 @@ COMMAND_LINE_TEST_CASE(L"container start --attach cont", L"start", true) COMMAND_LINE_TEST_CASE(L"container start -a cont", L"start", true) COMMAND_LINE_TEST_CASE(L"create ubuntu:latest", L"create", true) COMMAND_LINE_TEST_CASE(L"container create --name foo ubuntu", L"create", true) +COMMAND_LINE_TEST_CASE(L"create --workdir /app ubuntu", L"create", true) +COMMAND_LINE_TEST_CASE(L"create -w /app ubuntu", L"create", true) +COMMAND_LINE_TEST_CASE(L"container create --workdir /app ubuntu sh", L"create", true) +COMMAND_LINE_TEST_CASE(L"create --workdir", L"create", false) // Missing value for --workdir +COMMAND_LINE_TEST_CASE(L"create --workdir \"\" ubuntu", L"create", false) // Empty working directory +COMMAND_LINE_TEST_CASE(L"run --workdir /app ubuntu echo hello", L"run", true) +COMMAND_LINE_TEST_CASE(L"run -w /app ubuntu echo hello", L"run", true) +COMMAND_LINE_TEST_CASE(L"container run --workdir /app ubuntu sh", L"run", true) +COMMAND_LINE_TEST_CASE(L"run --workdir", L"run", false) // Missing value for --workdir +COMMAND_LINE_TEST_CASE(L"run --workdir \"\" ubuntu echo hello", L"run", false) // Empty working directory COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true) COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp index 83d2d5a32..aaa6adf09 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -188,6 +188,96 @@ class WSLCCLIExecutionUnitTests 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: 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. diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 9497f9c91..9216c46b9 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -532,6 +532,16 @@ class WSLCE2EContainerCreateTests result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1}); } + WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir) + { + auto result = + RunWslc(std::format(L"container create --name {} --workdir /tmp {} pwd", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"container start -a {}", WslcContainerName)); + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0}); + } + private: // Test container name const std::wstring WslcContainerName = L"wslc-test-container"; @@ -603,6 +613,7 @@ class WSLCE2EContainerCreateTests << L" -t,--tty Open a TTY with the container process.\r\n" << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n" << L" -v,--volume Bind mount a volume to the container\r\n" + << L" -w,--workdir Working directory inside the container\r\n" << L" -h,--help Shows help about the selected command\r\n" << L"\r\n"; return options.str(); diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 563c2f862..d99d29b2b 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -553,6 +553,12 @@ class WSLCE2EContainerRunTests result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1}); } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir) + { + auto result = RunWslc(std::format(L"container run --rm --workdir /tmp {} pwd", DebianImage.NameAndTag())); + result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0}); + } + private: // Test container name const std::wstring WslcContainerName = L"wslc-test-container"; @@ -627,6 +633,7 @@ class WSLCE2EContainerRunTests << L" -t,--tty Open a TTY with the container process.\r\n" << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n" << L" -v,--volume Bind mount a volume to the container\r\n" + << L" -w,--workdir Working directory inside the container\r\n" << L" -h,--help Shows help about the selected command\r\n" << L"\r\n"; return options.str();