master
cpp 892 lines 35.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLIExecutionUnitTests.cpp
8
9 Abstract:
10
11 This file contains unit tests for WSLC CLI command execution.
12
13 --*/
14
15 #include "precomp.h"
16 #include "windows/Common.h"
17 #include "WSLCCLITestHelpers.h"
18
19 #include "SessionModel.h"
20
21 #include "AsyncExecution.h"
22 #include "Command.h"
23 #include "RootCommand.h"
24 #include "ArgumentValidation.h"
25 #include "ContainerCommand.h"
26 #include "ContainerTasks.h"
27
28 using namespace wsl::windows::wslc;
29 using namespace WSLCTestHelpers;
30 using namespace WEX::Logging;
31 using namespace WEX::Common;
32 using namespace WEX::TestExecution;
33
34 namespace WSLCCLIExecutionUnitTests {
35 // Helper structure to hold test data
36 struct CommandLineTestCase
37 {
38 std::wstring commandLine;
39 std::wstring expectedCommand;
40 bool shouldSucceed;
41 };
42
43 class WSLCCLIExecutionUnitTests
44 {
45 WSLC_TEST_CLASS(WSLCCLIExecutionUnitTests)
46
47 TEST_CLASS_SETUP(TestClassSetup)
48 {
49 return true;
50 }
51
52 TEST_CLASS_CLEANUP(TestClassCleanup)
53 {
54 return true;
55 }
56
57 TEST_METHOD(ValidateArguments_RequiredOptionUsesLongName)
58 {
59 RootCommand command;
60 ArgMap args;
61 const std::vector<Argument> definitions{Argument::Create(ArgType::Password, {.Required = true})};
62
63 try
64 {
65 command.ValidateArguments(args, definitions, false);
66 VERIFY_FAIL(L"Expected ArgumentException");
67 }
68 catch (const ArgumentException& exception)
69 {
70 VERIFY_ARE_EQUAL(wsl::shared::Localization::WSLCCLI_RequiredArgumentOptionError(L"--password"), exception.Message());
71 }
72 }
73
74 TEST_METHOD(GlobalEnvironmentOptions_NoColorIsAppliedAndFrozen)
75 {
76 {
77 CLIExecutionContext context;
78
79 context.ApplyGlobalEnvironmentOptions();
80 VERIFY_IS_FALSE(context.Terminal.IsNoColor());
81
82 VERIFY_THROWS_SPECIFIC(context.GlobalArgs.Add<ArgType::NoColor>(true), wil::ResultException, [](const wil::ResultException& e) {
83 return e.GetErrorCode() == E_ILLEGAL_METHOD_CALL;
84 });
85 }
86
87 {
88 CLIExecutionContext present;
89 present.GlobalArgs.Add<ArgType::NoColor>(true);
90 present.ApplyGlobalEnvironmentOptions();
91 VERIFY_IS_TRUE(present.Terminal.IsNoColor());
92
93 VERIFY_NO_THROW(Argument::Create(ArgType::NoColor).Validate(present.GlobalArgs));
94 VERIFY_THROWS_SPECIFIC(present.GlobalArgs.Remove(ArgType::NoColor), wil::ResultException, [](const wil::ResultException& e) {
95 return e.GetErrorCode() == E_ILLEGAL_METHOD_CALL;
96 });
97 }
98 }
99
100 // Test: Verify EnumVariantMap on DataMap for Context Data
101 TEST_METHOD(EnumVariantMap_DataMapValidation)
102 {
103 // DataMap is an EnumVariantMap, but for command execution context data instead of arguments.
104 // It does not have rigid typing like the Args map, so this will verify every Data enum value
105 // can be added and retrieved successfully. The arguments unit tests have more complex tests
106 // for the EnumVariantMap behavior. This one ensures Data enum values are correct.
107 wsl::windows::wslc::execution::DataMap dataMap;
108
109 // Verify all data enum values defined.
110 auto allDataTypes = std::vector<Data>{};
111 for (int i = 0; i < static_cast<int>(Data::Max); ++i)
112 {
113 Data dataType = static_cast<Data>(i);
114
115 // Add the data to the DataMap with a test value based on its type.
116 // Each data type needs to be added here as each enum may have its own value.
117 VERIFY_IS_FALSE(dataMap.Contains(dataType));
118 bool handled = false;
119 if (dataType == Data::Session)
120 {
121 // Create a null session for testing - Session requires a COM pointer
122 wil::com_ptr<IWSLCSession> nullSession; // Creates null COM pointer
123 wsl::windows::wslc::models::Session session{nullSession};
124 dataMap.Add<Data::Session>(std::move(session));
125 handled = true;
126 }
127 else if (dataType == Data::Containers)
128 {
129 std::vector<wsl::windows::wslc::models::ContainerInformation> containers;
130 dataMap.Add<Data::Containers>(std::move(containers));
131 handled = true;
132 }
133 else if (dataType == Data::ContainerOptions)
134 {
135 wsl::windows::wslc::models::ContainerOptions options;
136 dataMap.Add<Data::ContainerOptions>(std::move(options));
137 handled = true;
138 }
139 else if (dataType == Data::Images)
140 {
141 std::vector<wsl::windows::wslc::models::ImageInformation> images;
142 dataMap.Add<Data::Images>(std::move(images));
143 handled = true;
144 }
145 else if (dataType == Data::Volumes)
146 {
147 std::vector<wsl::windows::common::wslc_schema::VolumeListEntry> volumes;
148 dataMap.Add<Data::Volumes>(std::move(volumes));
149 handled = true;
150 }
151 else if (dataType == Data::Networks)
152 {
153 std::vector<wsl::windows::common::wslc_schema::NetworkListEntry> networks;
154 dataMap.Add<Data::Networks>(std::move(networks));
155 handled = true;
156 }
157 else if (dataType == Data::NetworkEndpointOptions)
158 {
159 wsl::windows::wslc::models::NetworkEndpointOptions endpointOptions;
160 dataMap.Add<Data::NetworkEndpointOptions>(std::move(endpointOptions));
161 handled = true;
162 }
163
164 if (!handled)
165 {
166 VERIFY_FAIL(L"Unhandled Data type in test");
167 }
168
169 allDataTypes.push_back(dataType);
170 VERIFY_IS_TRUE(dataMap.Contains(dataType));
171 }
172
173 // Verify basic retrieval.
174 auto& session = dataMap.Get<Data::Session>();
175 VERIFY_IS_NULL(session.Get()); // A null ptr was added.
176
177 auto& containers = dataMap.Get<Data::Containers>();
178 VERIFY_ARE_EQUAL(0u, containers.size());
179
180 // Other more complex EnumVariantMap tests are in the Args unit tests.
181 // This one will just verify all the data types in the Data Map work as expected.
182 }
183
184 // Test: SetContainerOptionsFromArgs sets WorkingDirectory when --workdir is provided
185 TEST_METHOD(SetContainerOptionsFromArgs_WithWorkDir_SetsWorkingDirectory)
186 {
187 CLIExecutionContext context;
188 context.Args.Add<ArgType::WorkDir>(std::wstring{L"/app"});
189
190 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
191
192 const auto& options = context.Data.Get<Data::ContainerOptions>();
193 VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
194 }
195
196 // Test: SetContainerOptionsFromArgs leaves WorkingDirectory empty when --workdir is not provided
197 TEST_METHOD(SetContainerOptionsFromArgs_WithoutWorkDir_WorkingDirectoryIsEmpty)
198 {
199 CLIExecutionContext context;
200
201 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
202
203 const auto& options = context.Data.Get<Data::ContainerOptions>();
204 VERIFY_IS_TRUE(options.WorkingDirectory.empty());
205 }
206
207 // Test: Full parse of 'exec --workdir "" cont1 cmd' rejects empty working directory
208 TEST_METHOD(ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
209 {
210 // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
211 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" cont1 sh");
212
213 ContainerExecCommand command{L""};
214 CLIExecutionContext context;
215 command.ParseArguments(invocation, context.Args);
216
217 VERIFY_THROWS_SPECIFIC(
218 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
219 }
220
221 // Test: Full parse of 'exec --workdir /path cont1 cmd' sets WorkingDirectory
222 TEST_METHOD(ExecCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
223 {
224 // Invoke ContainerExecCommand parsing directly with the subcommand arguments it accepts.
225 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir cont1 sh");
226
227 ContainerExecCommand command{L""};
228 CLIExecutionContext context;
229 command.ParseArguments(invocation, context.Args);
230 command.ValidateArguments(context.Args);
231
232 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
233
234 const auto& options = context.Data.Get<Data::ContainerOptions>();
235 VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
236 }
237
238 // Test: Full parse of 'exec -w /path cont1 cmd' (short alias) sets WorkingDirectory
239 TEST_METHOD(ExecCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
240 {
241 auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app cont1 sh");
242
243 ContainerExecCommand command{L""};
244 CLIExecutionContext context;
245 command.ParseArguments(invocation, context.Args);
246 command.ValidateArguments(context.Args);
247
248 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
249
250 const auto& options = context.Data.Get<Data::ContainerOptions>();
251 VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
252 }
253
254 // Test: Full parse of 'run --workdir "" image cmd' rejects empty working directory
255 TEST_METHOD(RunCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
256 {
257 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh");
258
259 ContainerRunCommand command{L""};
260 CLIExecutionContext context;
261 command.ParseArguments(invocation, context.Args);
262
263 VERIFY_THROWS_SPECIFIC(
264 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
265 }
266
267 // Test: Full parse of 'run --workdir /path image cmd' sets WorkingDirectory
268 TEST_METHOD(RunCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
269 {
270 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh");
271
272 ContainerRunCommand command{L""};
273 CLIExecutionContext context;
274 command.ParseArguments(invocation, context.Args);
275 command.ValidateArguments(context.Args);
276
277 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
278
279 const auto& options = context.Data.Get<Data::ContainerOptions>();
280 VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
281 }
282
283 // Test: Full parse of 'run -w /path image cmd' (short alias) sets WorkingDirectory
284 TEST_METHOD(RunCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
285 {
286 auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh");
287
288 ContainerRunCommand command{L""};
289 CLIExecutionContext context;
290 command.ParseArguments(invocation, context.Args);
291 command.ValidateArguments(context.Args);
292
293 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
294
295 const auto& options = context.Data.Get<Data::ContainerOptions>();
296 VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
297 }
298
299 // Test: Full parse of 'create --workdir "" image cmd' rejects empty working directory
300 TEST_METHOD(CreateCommand_ParseWorkDirEmptyValue_ThrowsArgumentException)
301 {
302 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir \"\" ubuntu sh");
303
304 ContainerCreateCommand command{L""};
305 CLIExecutionContext context;
306 command.ParseArguments(invocation, context.Args);
307
308 VERIFY_THROWS_SPECIFIC(
309 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
310 }
311
312 // Test: Full parse of 'create --workdir /path image cmd' sets WorkingDirectory
313 TEST_METHOD(CreateCommand_ParseWorkDirLongOption_SetsWorkingDirectory)
314 {
315 auto invocation = CreateInvocationFromCommandLine(L"wslc --workdir /tmp/mydir ubuntu sh");
316
317 ContainerCreateCommand command{L""};
318 CLIExecutionContext context;
319 command.ParseArguments(invocation, context.Args);
320 command.ValidateArguments(context.Args);
321
322 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
323
324 const auto& options = context.Data.Get<Data::ContainerOptions>();
325 VERIFY_ARE_EQUAL(std::string("/tmp/mydir"), options.WorkingDirectory);
326 }
327
328 // Test: Full parse of 'create -w /path image cmd' (short alias) sets WorkingDirectory
329 TEST_METHOD(CreateCommand_ParseWorkDirShortOption_SetsWorkingDirectory)
330 {
331 auto invocation = CreateInvocationFromCommandLine(L"wslc -w /app ubuntu sh");
332
333 ContainerCreateCommand command{L""};
334 CLIExecutionContext context;
335 command.ParseArguments(invocation, context.Args);
336 command.ValidateArguments(context.Args);
337
338 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
339
340 const auto& options = context.Data.Get<Data::ContainerOptions>();
341 VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
342 }
343
344 TEST_METHOD(RunCommand_ParseGpusAll_SetsGpuOption)
345 {
346 auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh");
347
348 ContainerRunCommand command{L""};
349 CLIExecutionContext context;
350 command.ParseArguments(invocation, context.Args);
351 command.ValidateArguments(context.Args);
352
353 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
354
355 const auto& options = context.Data.Get<Data::ContainerOptions>();
356 VERIFY_IS_TRUE(options.Gpu);
357 }
358
359 TEST_METHOD(RunCommand_ParseGpusInvalid_ThrowsArgumentException)
360 {
361 auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus invalid ubuntu sh");
362
363 ContainerRunCommand command{L""};
364 CLIExecutionContext context;
365 command.ParseArguments(invocation, context.Args);
366
367 VERIFY_THROWS_SPECIFIC(
368 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
369 }
370
371 TEST_METHOD(CreateCommand_ParseGpusAll_SetsGpuOption)
372 {
373 auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh");
374
375 ContainerCreateCommand command{L""};
376 CLIExecutionContext context;
377 command.ParseArguments(invocation, context.Args);
378 command.ValidateArguments(context.Args);
379
380 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
381
382 const auto& options = context.Data.Get<Data::ContainerOptions>();
383 VERIFY_IS_TRUE(options.Gpu);
384 }
385
386 TEST_METHOD(CreateCommand_ParseGpusInvalid_ThrowsArgumentException)
387 {
388 auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus none ubuntu sh");
389
390 ContainerCreateCommand command{L""};
391 CLIExecutionContext context;
392 command.ParseArguments(invocation, context.Args);
393
394 VERIFY_THROWS_SPECIFIC(
395 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
396 }
397
398 TEST_METHOD(SetContainerOptionsFromArgs_WithoutNetwork_NetworksIsEmpty)
399 {
400 CLIExecutionContext context;
401
402 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
403
404 const auto& options = context.Data.Get<Data::ContainerOptions>();
405 VERIFY_IS_TRUE(options.Networks.empty());
406 }
407
408 TEST_METHOD(RunCommand_ParseNetworkSingleValue_SetsNetwork)
409 {
410 auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 ubuntu sh");
411
412 ContainerRunCommand command{L""};
413 CLIExecutionContext context;
414 command.ParseArguments(invocation, context.Args);
415 command.ValidateArguments(context.Args);
416
417 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
418
419 const auto& options = context.Data.Get<Data::ContainerOptions>();
420 VERIFY_ARE_EQUAL(1u, options.Networks.size());
421 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
422 }
423
424 TEST_METHOD(RunCommand_ParseNetworkMultipleValues_PreservesOrder)
425 {
426 auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");
427
428 ContainerRunCommand command{L""};
429 CLIExecutionContext context;
430 command.ParseArguments(invocation, context.Args);
431 command.ValidateArguments(context.Args);
432
433 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
434
435 const auto& options = context.Data.Get<Data::ContainerOptions>();
436 VERIFY_ARE_EQUAL(2u, options.Networks.size());
437 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
438 VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
439 }
440
441 TEST_METHOD(RunCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases)
442 {
443 auto invocation =
444 CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh");
445
446 ContainerRunCommand command{L""};
447 CLIExecutionContext context;
448 command.ParseArguments(invocation, context.Args);
449 command.ValidateArguments(context.Args);
450
451 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
452
453 const auto& options = context.Data.Get<Data::ContainerOptions>();
454 VERIFY_ARE_EQUAL(2u, options.Networks.size());
455 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
456 VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
457 VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
458 VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
459 VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
460 VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size());
461 VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]);
462 }
463
464 TEST_METHOD(RunCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases)
465 {
466 auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh");
467
468 ContainerRunCommand command{L""};
469 CLIExecutionContext context;
470 command.ParseArguments(invocation, context.Args);
471 command.ValidateArguments(context.Args);
472
473 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
474
475 const auto& options = context.Data.Get<Data::ContainerOptions>();
476 VERIFY_ARE_EQUAL(1u, options.Networks.size());
477 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
478 VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
479 VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
480 VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
481 }
482
483 TEST_METHOD(RunCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
484 {
485 auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");
486
487 ContainerRunCommand command{L""};
488 CLIExecutionContext context;
489 command.ParseArguments(invocation, context.Args);
490
491 VERIFY_THROWS_SPECIFIC(
492 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
493 }
494
495 TEST_METHOD(CreateCommand_ParseNetworkSingleValue_SetsNetwork)
496 {
497 auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 ubuntu sh");
498
499 ContainerCreateCommand command{L""};
500 CLIExecutionContext context;
501 command.ParseArguments(invocation, context.Args);
502 command.ValidateArguments(context.Args);
503
504 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
505
506 const auto& options = context.Data.Get<Data::ContainerOptions>();
507 VERIFY_ARE_EQUAL(1u, options.Networks.size());
508 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
509 }
510
511 TEST_METHOD(CreateCommand_ParseNetworkMultipleValues_PreservesOrder)
512 {
513 auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");
514
515 ContainerCreateCommand command{L""};
516 CLIExecutionContext context;
517 command.ParseArguments(invocation, context.Args);
518 command.ValidateArguments(context.Args);
519
520 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
521
522 const auto& options = context.Data.Get<Data::ContainerOptions>();
523 VERIFY_ARE_EQUAL(2u, options.Networks.size());
524 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
525 VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
526 }
527
528 TEST_METHOD(CreateCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases)
529 {
530 auto invocation =
531 CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh");
532
533 ContainerCreateCommand command{L""};
534 CLIExecutionContext context;
535 command.ParseArguments(invocation, context.Args);
536 command.ValidateArguments(context.Args);
537
538 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
539
540 const auto& options = context.Data.Get<Data::ContainerOptions>();
541 VERIFY_ARE_EQUAL(2u, options.Networks.size());
542 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
543 VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
544 VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
545 VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
546 VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
547 VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size());
548 VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]);
549 }
550
551 TEST_METHOD(CreateCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases)
552 {
553 auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh");
554
555 ContainerCreateCommand command{L""};
556 CLIExecutionContext context;
557 command.ParseArguments(invocation, context.Args);
558 command.ValidateArguments(context.Args);
559
560 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
561
562 const auto& options = context.Data.Get<Data::ContainerOptions>();
563 VERIFY_ARE_EQUAL(1u, options.Networks.size());
564 VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
565 VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
566 VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
567 VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
568 }
569
570 TEST_METHOD(CreateCommand_ParseNetworkDuplicateNameOption_ThrowsArgumentException)
571 {
572 auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh");
573
574 ContainerCreateCommand command{L""};
575 CLIExecutionContext context;
576 command.ParseArguments(invocation, context.Args);
577
578 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
579 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network");
580 return exception.Message() == expectedMessage;
581 });
582 }
583
584 TEST_METHOD(CreateCommand_ParseNetworkUnsupportedOption_ThrowsArgumentException)
585 {
586 auto invocation = CreateInvocationFromCommandLine(
587 L"wslc --network name=net1,driver-opt=com.docker.network.endpoint.sysctls="
588 L"net.ipv4.conf.IFNAME.log_martians=1 ubuntu sh");
589
590 ContainerCreateCommand command{L""};
591 CLIExecutionContext context;
592 command.ParseArguments(invocation, context.Args);
593
594 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
595 const auto expectedMessage =
596 wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"driver-opt");
597 return exception.Message() == expectedMessage;
598 });
599 }
600
601 TEST_METHOD(CreateCommand_ParseNetworkUnknownOption_ThrowsArgumentException)
602 {
603 auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,aliases=a ubuntu sh");
604
605 ContainerCreateCommand command{L""};
606 CLIExecutionContext context;
607 command.ParseArguments(invocation, context.Args);
608
609 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
610 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"aliases");
611 return exception.Message() == expectedMessage;
612 });
613 }
614
615 TEST_METHOD(CreateCommand_ParseNetworkBackendAliasesOption_ThrowsArgumentException)
616 {
617 auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,Aliases=a ubuntu sh");
618
619 ContainerCreateCommand command{L""};
620 CLIExecutionContext context;
621 command.ParseArguments(invocation, context.Args);
622
623 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
624 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"Aliases");
625 return exception.Message() == expectedMessage;
626 });
627 }
628
629 TEST_METHOD(CreateCommand_ParseNetworkAliasWithoutName_ThrowsArgumentException)
630 {
631 auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a ubuntu sh");
632
633 ContainerCreateCommand command{L""};
634 CLIExecutionContext context;
635 command.ParseArguments(invocation, context.Args);
636
637 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
638 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
639 return exception.Message() == expectedMessage;
640 });
641 }
642
643 TEST_METHOD(CreateCommand_ParseNetworkNameWhitespaceValue_ThrowsArgumentException)
644 {
645 auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"name= \" ubuntu sh");
646
647 ContainerCreateCommand command{L""};
648 CLIExecutionContext context;
649 command.ParseArguments(invocation, context.Args);
650
651 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
652 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
653 return exception.Message() == expectedMessage;
654 });
655 }
656
657 TEST_METHOD(ParseNetworkArgument_NameUnicodeWhitespaceValue_ThrowsArgumentException)
658 {
659 VERIFY_THROWS_SPECIFIC(
660 validation::ParseNetworkArgument(L"name=\u3000", L"network"), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
661 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
662 return exception.Message() == expectedMessage;
663 });
664 }
665
666 TEST_METHOD(CreateCommand_ParseNetworkAliasEmptyValue_ThrowsArgumentException)
667 {
668 auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,alias= ubuntu sh");
669
670 ContainerCreateCommand command{L""};
671 CLIExecutionContext context;
672 command.ParseArguments(invocation, context.Args);
673
674 VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
675 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkAliasEmptyError(L"network");
676 return exception.Message() == expectedMessage;
677 });
678 }
679
680 TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
681 {
682 auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");
683
684 ContainerCreateCommand command{L""};
685 CLIExecutionContext context;
686 command.ParseArguments(invocation, context.Args);
687
688 VERIFY_THROWS_SPECIFIC(
689 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
690 }
691
692 TEST_METHOD(CreateCommand_SetContainerOptionsInvalidNetwork_ThrowsArgumentExceptionWithArgumentName)
693 {
694 auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh");
695
696 ContainerCreateCommand command{L""};
697 CLIExecutionContext context;
698 command.ParseArguments(invocation, context.Args);
699
700 VERIFY_THROWS_SPECIFIC(
701 wsl::windows::wslc::task::SetContainerOptionsFromArgs(context), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
702 const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network");
703 return exception.Message() == expectedMessage;
704 });
705 }
706
707 // Test: Command Line test parsing all cases defined in CommandLineTestCases.h
708 // This test verifies the command line parsing logic used by the CLI and executes the same
709 // code as the CLI up to the point of command execution, including parsing and argument validtion.
710 // It does not actually verify the execution of the command, just that the correct command is
711 // found and the provided command line parsed correctly according to the command's defined arguments,
712 // and the argument validation rules are correctly applied. The test cases are defined in
713 // CommandLineTestCases.h and cover various valid and invalid command lines.
714 //
715 // Mirrors CoreMain's pipeline:
716 // 1. Globals scan (optionsOnly + stopOnUnknown): consume recognized
717 // globals, leave everything else in place. Env apply is intentionally
718 // skipped so test behavior is not affected by the host environment.
719 // 2. Subcommand resolution.
720 // 3. Leaf command parse + validate.
721 TEST_METHOD(CommandLineParsing_AllCases)
722 {
723 std::vector<CommandLineTestCase> testCases = {
724 #define COMMAND_LINE_TEST_CASE(cmdLine, expectedCmd, shouldPass) {cmdLine, expectedCmd, shouldPass},
725 #include "CommandLineTestCases.h"
726 #undef COMMAND_LINE_TEST_CASE
727 };
728
729 // Run all test cases
730 for (const auto& testCase : testCases)
731 {
732 LogComment(L"Testing: " + testCase.commandLine);
733
734 // Pre-pend executable name, which will get stripped off by CommandLineToArgvW
735 auto fullCommandLine = L"wslc " + testCase.commandLine;
736
737 // Process the command line as Windows does.
738 int argc = 0;
739 auto argv = CommandLineToArgvW(fullCommandLine.c_str(), &argc);
740 std::vector<std::wstring> args;
741 for (int i = 1; i < argc; ++i)
742 {
743 args.emplace_back(argv[i]);
744 }
745
746 // And now process the command line like WSLC does.
747 bool succeeded = true;
748 try
749 {
750 Invocation invocation{std::move(args)};
751 std::unique_ptr<Command> command = std::make_unique<RootCommand>();
752 const Command* const rootCommand = command.get();
753
754 // Pass 1: globals scan. Lenient on unknowns so non-global tokens
755 // (subcommands, root options, errors) flow to subsequent passes.
756 CLIExecutionContext context;
757 const auto cliGlobals = rootCommand->GetGlobalArguments();
758 rootCommand->ParseArguments(
759 invocation,
760 context.GlobalArgs,
761 cliGlobals,
762 /*optionsOnly*/ true,
763 /*stopOnUnknown*/ true);
764 rootCommand->ValidateArguments(context.GlobalArgs, cliGlobals, /*runInternalHook*/ false);
765
766 // Pass 2: walk down to the leaf subcommand.
767 std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation);
768 while (subCommand)
769 {
770 command = std::move(subCommand);
771 subCommand = command->FindSubCommand(invocation);
772 }
773
774 // Ensure we found the expected command
775 VERIFY_ARE_EQUAL(testCase.expectedCommand, command->Name());
776
777 // Pass 3: leaf parse + validate.
778 command->ParseArguments(invocation, context.Args);
779 command->ValidateArguments(context.Args);
780 }
781 catch (const CommandException& ce)
782 {
783 LogComment(L"Command line parsing threw an exception: " + ce.Message());
784 succeeded = false;
785 }
786 catch (...)
787 {
788 LogComment(L"Command line parsing threw an unexpected exception.");
789 succeeded = false;
790 }
791
792 VERIFY_ARE_EQUAL(testCase.shouldSucceed, succeeded);
793 }
794 }
795 };
796
797 class ForEachAsyncUnitTests
798 {
799 WSLC_TEST_CLASS(ForEachAsyncUnitTests)
800
801 TEST_METHOD(ForEachAsync_SuccessCallbackInvokedForAllItems)
802 {
803 const std::vector<int> items = {1, 2, 3, 4, 5};
804 std::vector<int> results;
805
806 ForEachAsync<int>(
807 items,
808 [](int item) { return item * 2; },
809 [&](int result) { results.push_back(result); },
810 [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); });
811
812 VERIFY_ARE_EQUAL(items.size(), results.size());
813 for (int item : items)
814 {
815 VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item * 2) != results.end());
816 }
817 }
818
819 TEST_METHOD(ForEachAsync_ErrorCallbackInvokedOnFailure)
820 {
821 const std::vector<int> items = {1, 2, 3};
822 std::vector<int> failedItems;
823 std::vector<int> succeededItems;
824
825 ForEachAsync<int>(
826 items,
827 [](int item) -> int {
828 if (item == 2)
829 {
830 THROW_HR(E_FAIL);
831 }
832 return item;
833 },
834 [&](int result) { succeededItems.push_back(result); },
835 [&](int item, wil::ResultException /*error*/) { failedItems.push_back(item); });
836
837 VERIFY_ARE_EQUAL(1u, failedItems.size());
838 VERIFY_ARE_EQUAL(2, failedItems[0]);
839 VERIFY_ARE_EQUAL(2u, succeededItems.size());
840 }
841
842 TEST_METHOD(ForEachAsync_EmptyInputProducesNoCallbacks)
843 {
844 const std::vector<int> items;
845 bool successCalled = false;
846 bool errorCalled = false;
847
848 ForEachAsync<int>(
849 items,
850 [](int item) { return item; },
851 [&](int /*result*/) { successCalled = true; },
852 [&](int /*item*/, wil::ResultException /*error*/) { errorCalled = true; });
853
854 VERIFY_IS_FALSE(successCalled);
855 VERIFY_IS_FALSE(errorCalled);
856 }
857
858 TEST_METHOD(ForEachAsync_BatchSizeOfOneProcessesAllItems)
859 {
860 const std::vector<int> items = {10, 20, 30, 40, 50};
861 std::vector<int> results;
862
863 ForEachAsync<int>(
864 items,
865 [](int item) { return item; },
866 [&](int result) { results.push_back(result); },
867 [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); },
868 /*batchSize=*/1);
869
870 VERIFY_ARE_EQUAL(items.size(), results.size());
871 for (int item : items)
872 {
873 VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item) != results.end());
874 }
875 }
876
877 TEST_METHOD(ForEachAsync_ErrorInOnErrorPropagatesThrow)
878 {
879 const std::vector<int> items = {1};
880
881 VERIFY_THROWS_SPECIFIC(
882 ForEachAsync<int>(
883 items,
884 [](int /*item*/) -> int { THROW_HR(E_ACCESSDENIED); },
885 [](int /*result*/) {},
886 [](int /*item*/, wil::ResultException error) { throw error; }),
887 wil::ResultException,
888 [](const wil::ResultException& ex) { return ex.GetErrorCode() == E_ACCESSDENIED; });
889 }
890 };
891
892 } // namespace WSLCCLIExecutionUnitTests