master
cpp 512 lines 24.3 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EContainerExecTests.cpp
8
9 Abstract:
10
11 This file contains end-to-end tests for WSLC container exec command.
12 --*/
13
14 #include "precomp.h"
15 #include "windows/Common.h"
16 #include "WSLCExecutor.h"
17 #include "WSLCE2EHelpers.h"
18 #include "TestImageRegistry.h"
19
20 namespace WSLCE2ETests {
21
22 class WSLCE2EContainerExecTests
23 {
24 WSLC_TEST_CLASS(WSLCE2EContainerExecTests)
25
26 TEST_CLASS_SETUP(ClassSetup)
27 {
28 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
29 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
30 VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
31
32 TestImageRegistry::Instance().EnsureLoaded(DebianImage);
33 return true;
34 }
35
36 TEST_CLASS_CLEANUP(ClassCleanup)
37 {
38 EnsureContainerDoesNotExist(WslcContainerName);
39
40 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
41 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
42 VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
43 return true;
44 }
45
46 TEST_METHOD_SETUP(TestMethodSetup)
47 {
48 EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
49 EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
50 EnsureContainerDoesNotExist(WslcContainerName);
51 return true;
52 }
53
54 TEST_METHOD_CLEANUP(TestMethodCleanup)
55 {
56 DeleteFileW(EnvTestFile1.c_str());
57 DeleteFileW(EnvTestFile2.c_str());
58 return true;
59 }
60
61 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_HelpCommand)
62 {
63 auto result = RunWslc(L"container exec --help");
64 result.Verify({.Stderr = L"", .ExitCode = 0});
65 VERIFY_IS_FALSE(result.Stdout.value().empty());
66 }
67
68 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingContainerId)
69 {
70 auto result = RunWslc(L"container exec");
71 result.Verify({.Stdout = L"", .ExitCode = 1});
72 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
73 }
74
75 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingCommand)
76 {
77 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
78 result.Verify({.Stderr = L"", .ExitCode = 0});
79
80 result = RunWslc(std::format(L"container exec {}", WslcContainerName));
81 result.Verify({.Stdout = L"", .ExitCode = 1});
82 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'command'"));
83 }
84
85 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_ContainerNotFound)
86 {
87 auto result = RunWslc(std::format(L"container exec {} echo hello", WslcContainerName));
88 result.Verify(
89 {.Stderr =
90 FormatErrorMessage(std::format(L"Container '{}' not found.", WslcContainerName), L"WSLC_E_CONTAINER_NOT_FOUND"),
91 .ExitCode = 1});
92 }
93
94 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_SimpleCommand)
95 {
96 // Run a container in background
97 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
98 result.Verify({.Stderr = L"", .ExitCode = 0});
99
100 // Execute a command
101 result = RunWslc(std::format(L"container exec {} echo hello", WslcContainerName));
102 result.Verify({.Stdout = L"hello\n", .Stderr = L"", .ExitCode = 0});
103 }
104
105 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveTTY)
106 {
107 VerifyContainerIsNotListed(WslcContainerName);
108
109 const auto& prompt = ">";
110 auto result =
111 RunWslc(std::format(L"container run -itd -e PS1={} --name {} {}", prompt, WslcContainerName, DebianImage.NameAndTag()));
112 result.Verify({.Stderr = L"", .ExitCode = 0});
113 auto containerId = result.GetStdoutOneLine();
114
115 const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
116
117 auto session = RunWslcInteractive(std::format(L"container exec -it {} /bin/bash --norc", containerId));
118 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
119
120 session.ExpectStdout(expectedPrompt);
121
122 session.WriteLine("echo hello");
123 session.ExpectCommandEcho("echo hello");
124 session.ExpectStdout("hello\r\n");
125 session.ExpectStdout(expectedPrompt);
126
127 session.WriteLine("whoami");
128 session.ExpectCommandEcho("whoami");
129 session.ExpectStdout("root\r\n");
130 session.ExpectStdout(expectedPrompt);
131
132 session.ExitAndVerifyNoErrors();
133 auto exitCode = session.Wait();
134 VERIFY_ARE_EQUAL(0, exitCode);
135 }
136
137 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveNoTTY)
138 {
139 VerifyContainerIsNotListed(WslcContainerName);
140 auto result = RunWslc(std::format(L"container run -id --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
141 result.Verify({.Stderr = L"", .ExitCode = 0});
142 auto containerId = result.GetStdoutOneLine();
143
144 auto session = RunWslcInteractive(std::format(L"container exec -i {} cat", containerId));
145 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
146
147 session.WriteLine("test line 1");
148 session.ExpectStdout("test line 1\n");
149 session.WriteLine("test line 2");
150 session.ExpectStdout("test line 2\n");
151
152 // Close stdin to signal EOF to cat
153 session.CloseStdin();
154
155 // Wait for cat to exit with code 0
156 auto exitCode = session.Wait(10000);
157 VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
158 session.VerifyNoErrors();
159 }
160
161 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveNoTTY_SelfExitingCommand)
162 {
163 // Regression test for a stdin deadlock. When stdin is a synchronous (non-overlapped) anonymous pipe, the client
164 // relays it on a worker thread parked in a blocking ReadFile() that the exit event cannot interrupt. With
165 // `echo hello` (which exits without reading stdin), teardown's join() on that worker blocks until stdin closes,
166 // so wslc hangs. The test exercises this by running `exec -i echo hello` and requiring it to exit while the
167 // client keeps stdin open.
168 //
169 // If this regresses, look at the client-side stdin relay teardown: InterruptAndJoinInputThread
170 // (the CancelSynchronousIo retry loop that unblocks the worker) and relay.cpp's InterruptableRead (which maps
171 // the resulting ERROR_OPERATION_ABORTED to EOF).
172 VerifyContainerIsNotListed(WslcContainerName);
173 auto result = RunWslc(std::format(L"container run -id --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
174 result.Verify({.Stderr = L"", .ExitCode = 0});
175 auto containerId = result.GetStdoutOneLine();
176
177 // RunWslcInteractive wires wslc's stdin to the read end of a synchronous (non-overlapped) anonymous pipe, which
178 // is what triggers the blocking-ReadFile relay path under test.
179 auto session = RunWslcInteractive(std::format(L"container exec -i {} echo hello", containerId));
180
181 // The command's output must arrive without the client closing stdin first.
182 session.ExpectStdout("hello\n");
183
184 // Long timeout: this only bounds the failure (hang) path, so it is generous to avoid false positives under CI load.
185 auto exitCode = session.Wait(120000);
186 VERIFY_ARE_EQUAL(0, exitCode, L"echo should exit with code 0 without the client closing stdin");
187
188 // Closing stdin after the process has already exited must remain a clean no-op with no errors emitted.
189 session.CloseStdin();
190 session.VerifyNoErrors();
191 }
192
193 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_InteractiveTTY_SelfExitingCommand)
194 {
195 // TTY counterpart to WSLCE2E_Container_Exec_InteractiveNoTTY_SelfExitingCommand (see that test for the full
196 // explanation of the deadlock). The -t flag routes wslc through ConsoleService::RelayInteractiveTty, whose
197 // stdin worker teardown is a separate scope-exit from the non-TTY path, so a regression could be introduced
198 // in one path and not the other. This test guards the TTY call site.
199 VerifyContainerIsNotListed(WslcContainerName);
200 auto result = RunWslc(std::format(L"container run -id --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
201 result.Verify({.Stderr = L"", .ExitCode = 0});
202 auto containerId = result.GetStdoutOneLine();
203
204 // -t sets the TTY flag; the harness still wires stdin as a synchronous pipe, so wslc takes the vulnerable
205 // RelayInteractiveTty else-branch (its input handle is a pipe, not a console).
206 auto session = RunWslcInteractive(std::format(L"container exec -it {} echo hello", containerId));
207
208 // The TTY translates the trailing LF to CRLF, so the exact output is "hello\r\n".
209 session.ExpectStdout("hello\r\n");
210
211 auto exitCode = session.Wait(120000);
212 VERIFY_ARE_EQUAL(0, exitCode, L"echo should exit with code 0 without the client closing stdin");
213
214 session.CloseStdin();
215 session.VerifyNoErrors();
216 }
217
218 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_PseudoConsole_TerminalSize)
219 {
220 VerifyContainerIsNotListed(WslcContainerName);
221
222 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
223 result.Verify({.Stderr = L"", .ExitCode = 0});
224
225 constexpr SHORT columns = 42;
226 constexpr SHORT rows = 43;
227 const auto commandLine =
228 std::format(L"container exec -it {} /bin/sh -c -- \"while true; do stty size; sleep 1; done\"", WslcContainerName);
229
230 auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows});
231 VerifyPseudoConsoleTtySize(session, columns, rows);
232 }
233
234 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption)
235 {
236 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
237 result.Verify({.Stderr = L"", .ExitCode = 0});
238
239 result = RunWslc(std::format(L"container exec -e {}=A {} env", HostEnvVariableName, WslcContainerName));
240 result.Verify({.Stderr = L"", .ExitCode = 0});
241
242 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
243 }
244
245 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_KeyOnly_UsesHostValue)
246 {
247 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
248 result.Verify({.Stderr = L"", .ExitCode = 0});
249
250 result = RunWslc(std::format(L"container exec -e {} {} env", HostEnvVariableName, WslcContainerName));
251 result.Verify({.Stderr = L"", .ExitCode = 0});
252
253 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
254 }
255
256 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile)
257 {
258 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a", "WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b"});
259
260 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
261 result.Verify({.Stderr = L"", .ExitCode = 0});
262
263 result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
264 result.Verify({.Stderr = L"", .ExitCode = 0});
265
266 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a"));
267 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b"));
268 }
269
270 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_MultipleValues)
271 {
272 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
273 result.Verify({.Stderr = L"", .ExitCode = 0});
274
275 result = RunWslc(std::format(
276 L"container exec -e {}=value-a -e {}=value-b {} env", HostEnvVariableName, HostEnvVariableName2, WslcContainerName));
277 result.Verify({.Stderr = L"", .ExitCode = 0});
278
279 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=value-a", HostEnvVariableName)));
280 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=value-b", HostEnvVariableName2)));
281 }
282
283 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_KeyOnly_MultipleValues_UsesHostValues)
284 {
285 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
286 result.Verify({.Stderr = L"", .ExitCode = 0});
287
288 result = RunWslc(std::format(L"container exec -e {} -e {} {} env", HostEnvVariableName, HostEnvVariableName2, WslcContainerName));
289 result.Verify({.Stderr = L"", .ExitCode = 0});
290
291 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
292 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName2, HostEnvVariableValue2)));
293 }
294
295 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_EmptyValue)
296 {
297 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
298 result.Verify({.Stderr = L"", .ExitCode = 0});
299
300 // Pass an explicit empty value and verify it is present as KEY=
301 result = RunWslc(std::format(L"container exec -e {}= {} env", HostEnvVariableName, WslcContainerName));
302 result.Verify({.Stderr = L"", .ExitCode = 0});
303
304 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=", HostEnvVariableName)));
305 }
306
307 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_MixedWithEnvFile)
308 {
309 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b"});
310
311 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
312 result.Verify({.Stderr = L"", .ExitCode = 0});
313
314 result = RunWslc(std::format(
315 L"container exec -e WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
316 result.Verify({.Stderr = L"", .ExitCode = 0});
317
318 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a"));
319 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b"));
320 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli"));
321 }
322
323 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MissingFile)
324 {
325 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
326 result.Verify({.Stderr = L"", .ExitCode = 0});
327
328 result = RunWslc(std::format(L"container exec --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName));
329 result.Verify({.Stdout = L"", .ExitCode = 1});
330 VERIFY_IS_TRUE(result.StderrContainsSubstring(
331 L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
332 }
333
334 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MultipleFiles)
335 {
336 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_FILE_MULTI_A=file1-a", "WSLC_TEST_EXEC_ENV_FILE_MULTI_B=file1-b"});
337 WriteTestFile(EnvTestFile2, {"WSLC_TEST_EXEC_ENV_FILE_MULTI_C=file2-c", "WSLC_TEST_EXEC_ENV_FILE_MULTI_D=file2-d"});
338
339 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
340 result.Verify({.Stderr = L"", .ExitCode = 0});
341
342 result = RunWslc(std::format(
343 L"container exec --env-file {} --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), EscapePath(EnvTestFile2.wstring()), WslcContainerName));
344 result.Verify({.Stderr = L"", .ExitCode = 0});
345
346 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_A=file1-a"));
347 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_B=file1-b"));
348 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_C=file2-c"));
349 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_FILE_MULTI_D=file2-d"));
350 }
351
352 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_InvalidContent)
353 {
354 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_VALID=ok", "BAD KEY=value"});
355
356 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
357 result.Verify({.Stderr = L"", .ExitCode = 0});
358
359 result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
360 result.Verify({.Stdout = L"", .ExitCode = 1});
361 VERIFY_IS_TRUE(result.StderrContainsSubstring(
362 L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
363 }
364
365 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_DuplicateKeys_Precedence)
366 {
367 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_DUP=from-file-1"});
368 WriteTestFile(EnvTestFile2, {"WSLC_TEST_EXEC_ENV_DUP=from-file-2"});
369
370 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
371 result.Verify({.Stderr = L"", .ExitCode = 0});
372
373 // Later --env-file wins
374 result = RunWslc(std::format(
375 L"container exec --env-file {} --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), EscapePath(EnvTestFile2.wstring()), WslcContainerName));
376 result.Verify({.Stderr = L"", .ExitCode = 0});
377
378 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_DUP=from-file-2"));
379
380 // Explicit -e wins over env-file
381 result = RunWslc(std::format(
382 L"container exec -e WSLC_TEST_EXEC_ENV_DUP=from-cli --env-file {} --env-file {} {} env",
383 EscapePath(EnvTestFile1.wstring()),
384 EscapePath(EnvTestFile2.wstring()),
385 WslcContainerName));
386 result.Verify({.Stderr = L"", .ExitCode = 0});
387
388 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_DUP=from-cli"));
389 }
390
391 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_ValueContainsEquals)
392 {
393 WriteTestFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_EQUALS=value=with=equals"});
394
395 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
396 result.Verify({.Stderr = L"", .ExitCode = 0});
397
398 result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
399 result.Verify({.Stderr = L"", .ExitCode = 0});
400
401 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_EXEC_ENV_EQUALS=value=with=equals"));
402 }
403
404 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_ExitCode_Propagates)
405 {
406 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
407 result.Verify({.Stderr = L"", .ExitCode = 0});
408
409 result = RunWslc(std::format(L"container exec {} sh -c \"exit 42\"", WslcContainerName));
410 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 42});
411 }
412
413 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_Stderr_Propagates)
414 {
415 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
416 result.Verify({.Stderr = L"", .ExitCode = 0});
417
418 result = RunWslc(std::format(L"container exec {} sh -c \"echo exec-error 1>&2\"", WslcContainerName));
419 result.Verify({.Stdout = L"", .Stderr = L"exec-error\n", .ExitCode = 0});
420 }
421
422 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_StoppedContainer)
423 {
424 auto result = RunWslc(std::format(L"container run --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
425 result.Verify({.Stderr = L"", .ExitCode = 0});
426
427 auto inspect = InspectContainer(WslcContainerName);
428 result = RunWslc(std::format(L"container exec {} echo should-fail", WslcContainerName));
429 auto errorMessage =
430 FormatErrorMessage(std::format(L"Container '{}' is not running.", inspect.Id), L"WSLC_E_CONTAINER_NOT_RUNNING");
431 result.Verify({.Stderr = errorMessage, .ExitCode = 1});
432 }
433
434 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_UidRoot)
435 {
436 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
437 result.Verify({.Stderr = L"", .ExitCode = 0});
438
439 result = RunWslc(std::format(L"container exec -u 0 {} sh -c \"id -u; id -g\"", WslcContainerName));
440 result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
441 }
442
443 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_NameGroupRoot)
444 {
445 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
446 result.Verify({.Stderr = L"", .ExitCode = 0});
447
448 result = RunWslc(std::format(L"container exec -u root:root {} sh -c \"id -un; id -u; id -g\"", WslcContainerName));
449 result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
450 }
451
452 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_UserOption_InvalidGroup_Fails)
453 {
454 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
455 result.Verify({.Stderr = L"", .ExitCode = 0});
456
457 result = RunWslc(std::format(L"container exec -u root:badgid {} id -u", WslcContainerName));
458 result.Verify({.Stdout = L"unable to find group badgid: no matching entries in group file\r\n", .ExitCode = 126});
459 }
460
461 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_WorkDir)
462 {
463 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
464 result.Verify({.Stderr = L"", .ExitCode = 0});
465
466 result = RunWslc(std::format(L"container exec --workdir /tmp {} pwd", WslcContainerName));
467 result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
468 }
469
470 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_WorkDir_ShortAlias)
471 {
472 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
473 result.Verify({.Stderr = L"", .ExitCode = 0});
474
475 result = RunWslc(std::format(L"container exec -w /tmp {} pwd", WslcContainerName));
476 result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
477 }
478
479 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_Detach)
480 {
481 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
482 result.Verify({.Stderr = L"", .ExitCode = 0});
483
484 constexpr auto markerPath = L"/tmp/wslc-exec-detach-marker";
485 result = RunWslc(std::format(L"container exec -d {} sh -c \"sleep 1 && echo wslc-detach-ok > {}\"", WslcContainerName, markerPath));
486 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
487
488 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
489 [&]() {
490 auto readResult = RunWslc(std::format(L"container exec {} cat {}", WslcContainerName, markerPath));
491 readResult.Verify({.Stdout = L"wslc-detach-ok\n", .Stderr = L"", .ExitCode = 0});
492 },
493 std::chrono::milliseconds(200),
494 std::chrono::seconds(10)));
495 }
496
497 private:
498 const std::wstring WslcContainerName = L"wslc-test-container";
499 const TestImage& DebianImage = DebianTestImage();
500
501 // Test environment variables
502 const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
503 const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
504 const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
505 const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
506 const std::wstring MissingHostEnvVariableName = L"WSLC_TEST_MISSING_HOST_ENV";
507
508 // Test environment variable files
509 std::filesystem::path EnvTestFile1;
510 std::filesystem::path EnvTestFile2;
511 };
512 } // namespace WSLCE2ETests