master
cpp 435 lines 14.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLICommandUnitTests.cpp
8
9 Abstract:
10
11 This file contains unit tests for WSLC CLI Command classes.
12
13 --*/
14
15 #include "precomp.h"
16 #include <unordered_map>
17 #include <unordered_set>
18 #include "windows/Common.h"
19 #include "WSLCCLITestHelpers.h"
20
21 #include "Command.h"
22 #include "RootCommand.h"
23 #include "ContainerCommand.h"
24 #include "ImageCommand.h"
25 #include "InspectCommand.h"
26 #include "NetworkCommand.h"
27 #include "SessionCommand.h"
28 #include "SystemCommand.h"
29 #include "VersionCommand.h"
30 #include "VolumeCommand.h"
31 #include "EnvironmentOptions.h"
32
33 using namespace wsl::windows::wslc;
34 using namespace WSLCTestHelpers;
35 using namespace WEX::Logging;
36 using namespace WEX::Common;
37 using namespace WEX::TestExecution;
38
39 namespace WSLCCLICommandUnitTests {
40 class WSLCCLICommandUnitTests
41 {
42 WSLC_TEST_CLASS(WSLCCLICommandUnitTests)
43
44 TEST_CLASS_SETUP(TestClassSetup)
45 {
46 Log::Comment(L"WSLC CLI Command Unit Tests - Class Setup");
47 return true;
48 }
49
50 TEST_CLASS_CLEANUP(TestClassCleanup)
51 {
52 Log::Comment(L"WSLC CLI Command Unit Tests - Class Cleanup");
53 return true;
54 }
55
56 // Test: Verify RootCommand has subcommands
57 TEST_METHOD(RootCommand_HasSubcommands)
58 {
59 auto cmd = RootCommand();
60
61 auto subcommands = cmd.GetCommands();
62
63 // Verify it has subcommands
64 VERIFY_IS_TRUE(subcommands.size() > 0);
65 LogComment(L"RootCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
66
67 // Verify each subcommand is valid
68 for (const auto& subcmd : subcommands)
69 {
70 VERIFY_IS_NOT_NULL(subcmd.get());
71 }
72 }
73
74 // Test: Verify SystemCommand has subcommands
75 TEST_METHOD(SystemCommand_HasSubcommands)
76 {
77 auto cmd = SystemCommand(L"system");
78 auto subcommands = cmd.GetCommands();
79
80 // Verify it has subcommands
81 VERIFY_IS_TRUE(subcommands.size() > 0);
82 LogComment(L"SystemCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
83
84 for (const auto& subcmd : subcommands)
85 {
86 VERIFY_IS_NOT_NULL(subcmd.get());
87 }
88 }
89
90 // Test: Verify SessionCommand has subcommands (now under system)
91 TEST_METHOD(SessionCommand_HasSubcommands)
92 {
93 auto cmd = SessionCommand(L"system session");
94 auto subcommands = cmd.GetCommands();
95
96 // Verify it has subcommands
97 VERIFY_IS_TRUE(subcommands.size() > 0);
98 LogComment(L"SessionCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
99
100 // Log subcommand types
101 for (const auto& subcmd : subcommands)
102 {
103 VERIFY_IS_NOT_NULL(subcmd.get());
104 }
105 }
106
107 // Test: Verify SessionEnterCommand has the expected arguments
108 TEST_METHOD(SessionEnterCommand_HasExpectedArguments)
109 {
110 auto cmd = SessionEnterCommand(L"system session");
111 auto args = cmd.GetArguments();
112
113 // Should have 2 arguments: storage-path (positional, required) and name (value, optional)
114 VERIFY_ARE_EQUAL(2u, args.size());
115
116 // Verify storage-path argument
117 auto& storagePath = args[0];
118 VERIFY_ARE_EQUAL(ArgType::StoragePath, storagePath.Type());
119 VERIFY_ARE_EQUAL(Kind::Positional, storagePath.Kind());
120 VERIFY_IS_TRUE(storagePath.Required());
121
122 // Verify name argument
123 auto& name = args[1];
124 VERIFY_ARE_EQUAL(ArgType::Name, name.Type());
125 VERIFY_ARE_EQUAL(Kind::Value, name.Kind());
126 VERIFY_IS_FALSE(name.Required());
127 }
128
129 // Test: Verify SessionEnterCommand descriptions are not empty
130 TEST_METHOD(SessionEnterCommand_HasDescriptions)
131 {
132 auto cmd = SessionEnterCommand(L"system session");
133
134 VERIFY_IS_FALSE(cmd.ShortDescription().empty());
135 VERIFY_IS_FALSE(cmd.LongDescription().empty());
136 }
137
138 // Test: Verify ContainerCommand has subcommands
139 TEST_METHOD(ContainerCommand_HasSubcommands)
140 {
141 auto cmd = ContainerCommand(L"container");
142 auto subcommands = cmd.GetCommands();
143
144 // Verify it has subcommands
145 VERIFY_IS_TRUE(subcommands.size() > 0);
146 LogComment(L"ContainerCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
147
148 // Log subcommand types
149 for (const auto& subcmd : subcommands)
150 {
151 VERIFY_IS_NOT_NULL(subcmd.get());
152 }
153 }
154
155 // Test: Verify VersionCommand has the correct name
156 TEST_METHOD(VersionCommand_HasCorrectName)
157 {
158 auto cmd = VersionCommand(L"wslc");
159 VERIFY_ARE_EQUAL(std::wstring_view(L"version"), cmd.Name());
160 }
161
162 // Test: Verify VersionCommand has no subcommands
163 TEST_METHOD(VersionCommand_HasNoSubcommands)
164 {
165 auto cmd = VersionCommand(L"wslc");
166 VERIFY_ARE_EQUAL(0u, cmd.GetCommands().size());
167 }
168
169 // Test: Verify VersionCommand exposes the --format argument (plus the auto-added --help)
170 TEST_METHOD(VersionCommand_HasFormatArgument)
171 {
172 auto cmd = VersionCommand(L"wslc");
173
174 auto args = cmd.GetArguments();
175 VERIFY_ARE_EQUAL(1u, args.size());
176
177 const auto& format = args[0];
178 VERIFY_ARE_EQUAL(ArgType::Format, format.Type());
179 VERIFY_ARE_EQUAL(Kind::Value, format.Kind());
180 VERIFY_IS_FALSE(format.Required());
181
182 // GetAllArguments also includes the auto-added --help.
183 VERIFY_ARE_EQUAL(2u, cmd.GetAllArguments().size());
184 }
185
186 // Test: Verify RootCommand contains VersionCommand as a subcommand
187 TEST_METHOD(RootCommand_ContainsVersionCommand)
188 {
189 auto root = RootCommand();
190 auto subcommands = root.GetCommands();
191
192 bool found = false;
193 for (const auto& subcmd : subcommands)
194 {
195 if (subcmd->Name() == VersionCommand::CommandName)
196 {
197 found = true;
198 break;
199 }
200 }
201
202 VERIFY_IS_TRUE(found, L"RootCommand should contain VersionCommand");
203 }
204
205 // Test: Verify SystemInfoCommand has the correct name
206 TEST_METHOD(SystemInfoCommand_HasCorrectName)
207 {
208 auto cmd = SystemInfoCommand(L"system");
209 VERIFY_ARE_EQUAL(std::wstring_view(L"info"), cmd.Name());
210 }
211
212 // Test: Verify SystemInfoCommand has no subcommands
213 TEST_METHOD(SystemInfoCommand_HasNoSubcommands)
214 {
215 auto cmd = SystemInfoCommand(L"system");
216 VERIFY_ARE_EQUAL(0u, cmd.GetCommands().size());
217 }
218
219 // Test: Verify SystemInfoCommand exposes the --format argument (plus the auto-added --help)
220 TEST_METHOD(SystemInfoCommand_HasFormatArgument)
221 {
222 auto cmd = SystemInfoCommand(L"system");
223
224 auto args = cmd.GetArguments();
225 VERIFY_ARE_EQUAL(1u, args.size());
226
227 const auto& format = args[0];
228 VERIFY_ARE_EQUAL(ArgType::Format, format.Type());
229 VERIFY_ARE_EQUAL(Kind::Value, format.Kind());
230 VERIFY_IS_FALSE(format.Required());
231
232 // GetAllArguments also includes the auto-added --help.
233 VERIFY_ARE_EQUAL(2u, cmd.GetAllArguments().size());
234 }
235
236 // Test: Verify SystemCommand contains SystemInfoCommand as a subcommand
237 TEST_METHOD(SystemCommand_ContainsSystemInfoCommand)
238 {
239 auto cmd = SystemCommand(L"system");
240 auto subcommands = cmd.GetCommands();
241
242 bool found = false;
243 for (const auto& subcmd : subcommands)
244 {
245 if (subcmd->Name() == SystemInfoCommand::CommandName)
246 {
247 found = true;
248 break;
249 }
250 }
251
252 VERIFY_IS_TRUE(found, L"SystemCommand should contain SystemInfoCommand");
253 }
254
255 // SystemInfoCommand is registered twice so that both `wslc system info` and the
256 // `wslc info` alias resolve; this pins the second registration.
257 TEST_METHOD(RootCommand_ContainsSystemInfoCommand)
258 {
259 auto root = RootCommand();
260 auto subcommands = root.GetCommands();
261
262 bool found = false;
263 for (const auto& subcmd : subcommands)
264 {
265 if (subcmd->Name() == SystemInfoCommand::CommandName)
266 {
267 found = true;
268 break;
269 }
270 }
271
272 VERIFY_IS_TRUE(found, L"RootCommand should contain SystemInfoCommand");
273 }
274
275 // RootCommand exposes Session as the sole CLI global option. The override
276 // is the entry point for future globals; the test pins the current shape.
277 TEST_METHOD(RootCommand_GlobalArguments_OnlySession)
278 {
279 auto root = RootCommand();
280 auto globals = root.GetGlobalArguments();
281
282 VERIFY_ARE_EQUAL(1u, globals.size());
283 VERIFY_ARE_EQUAL(ArgType::Session, globals[0].Type());
284 VERIFY_ARE_EQUAL(Kind::Value, globals[0].Kind());
285 }
286
287 // RootCommand exposes NoColor as the sole env-eligible global option.
288 TEST_METHOD(RootCommand_EnvArguments_OnlyNoColor)
289 {
290 auto root = RootCommand();
291 auto envArgs = root.GetEnvArguments();
292
293 VERIFY_ARE_EQUAL(1u, envArgs.size());
294 VERIFY_ARE_EQUAL(ArgType::NoColor, envArgs[0].Type());
295 VERIFY_ARE_EQUAL(Kind::Flag, envArgs[0].Kind());
296 }
297
298 // Every ArgType advertised by GetEnvArguments() must have at least one entry
299 // in c_envBindings; otherwise ApplyEnvironmentOptions() has nothing to apply
300 // and help output would lie about env support.
301 TEST_METHOD(RootCommand_EnvArguments_AllHaveBindings)
302 {
303 auto root = RootCommand();
304 auto envArgs = root.GetEnvArguments();
305
306 for (const auto& a : envArgs)
307 {
308 bool found = false;
309 for (const auto& b : c_envBindings)
310 {
311 if (b.Type == a.Type())
312 {
313 found = true;
314 break;
315 }
316 }
317
318 VERIFY_IS_TRUE(found, std::format(L"ArgType {} has no env binding", static_cast<size_t>(a.Type())).c_str());
319 }
320 }
321
322 // Every command in the inspect family exposes docker's `-f` alias for --format
323 // (docker/cli cli/command/system/inspect.go: flags.StringVarP(&opts.format, "format", "f", ...)).
324 TEST_METHOD(InspectCommands_FormatArgumentHasDockerAlias)
325 {
326 const auto VerifyFormatAlias = [](const Command& command) {
327 const auto args = command.GetArguments();
328 const auto found = std::ranges::find_if(args, [](const auto& arg) { return arg.Type() == ArgType::InspectFormat; });
329
330 VERIFY_IS_TRUE(found != args.end(), std::format(L"Command '{}' does not register --format", command.FullName()).c_str());
331 VERIFY_ARE_EQUAL(std::wstring(L"format"), found->Name());
332 VERIFY_ARE_EQUAL(std::wstring(L"f"), found->Alias());
333 };
334
335 VerifyFormatAlias(InspectCommand(L""));
336 VerifyFormatAlias(ContainerInspectCommand(L"container"));
337 VerifyFormatAlias(ImageInspectCommand(L"image"));
338 VerifyFormatAlias(NetworkInspectCommand(L"network"));
339 VerifyFormatAlias(VolumeInspectCommand(L"volume"));
340 }
341
342 // Walk every command in the root tree and verify no argument collisions.
343 TEST_METHOD(AllCommands_NoAmbiguousArgumentNamesOrAliases)
344 {
345 // Build a lookup table from ArgType -> enum name string using the same X-macro.
346 static constexpr const wchar_t* c_argTypeNames[] = {
347 #define WSLC_ARG_ENUM(EnumName, Name, Alias, Kind, ConvertedType, Desc) L## #EnumName,
348 WSLC_ARGUMENTS(WSLC_ARG_ENUM)
349 #undef WSLC_ARG_ENUM
350 };
351
352 const auto ArgTypeName = [](argument::ArgType type) -> std::wstring_view {
353 const auto index = static_cast<size_t>(type);
354 const auto max = static_cast<size_t>(argument::ArgType::Max);
355 if (index < max)
356 {
357 return c_argTypeNames[index];
358 }
359
360 return L"<unknown>";
361 };
362
363 // Starting with the Root command, verify no argument collisions.
364 std::vector<std::unique_ptr<Command>> commands;
365 commands.push_back(std::make_unique<RootCommand>());
366
367 while (!commands.empty())
368 {
369 auto current = std::move(commands.back());
370 commands.pop_back();
371 VERIFY_IS_NOT_NULL(current.get());
372
373 const std::wstring commandFullName(current->FullName());
374 std::unordered_set<size_t> seenTypes;
375 std::unordered_map<std::wstring, argument::ArgType> seenNames;
376 std::unordered_map<std::wstring, argument::ArgType> seenAliases;
377
378 for (const auto& arg : current->GetAllArguments())
379 {
380 // Check for duplicate ArgType registration.
381 if (!seenTypes.emplace(static_cast<size_t>(arg.Type())).second)
382 {
383 VERIFY_FAIL(std::format(L"Command '{}' registers ArgType '{}' more than once", commandFullName, ArgTypeName(arg.Type()))
384 .c_str());
385 }
386
387 // Check name collision between distinct ArgTypes.
388 const auto& name = arg.Name();
389 if (name.size() < 2)
390 {
391 VERIFY_FAIL(
392 std::format(L"Command '{}' uses invalid long-form name '--{}' for ArgType '{}'", commandFullName, name, ArgTypeName(arg.Type()))
393 .c_str());
394 }
395
396 auto [nameIt, nameInserted] = seenNames.emplace(name, arg.Type());
397 if (!nameInserted)
398 {
399 VERIFY_FAIL(std::format(
400 L"Command '{}' has duplicate name '--{}' (ArgType '{}' conflicts with ArgType '{}')",
401 commandFullName,
402 name,
403 ArgTypeName(arg.Type()),
404 ArgTypeName(nameIt->second))
405 .c_str());
406 }
407
408 // Check alias collision between distinct ArgTypes; skip empty aliases (NO_ALIAS).
409 const auto& alias = arg.Alias();
410 if (!alias.empty())
411 {
412 auto [aliasIt, aliasInserted] = seenAliases.emplace(alias, arg.Type());
413 if (!aliasInserted)
414 {
415 VERIFY_FAIL(std::format(
416 L"Command '{}' has duplicate alias '-{}' (ArgType '{}' conflicts with ArgType '{}')",
417 commandFullName,
418 alias,
419 ArgTypeName(arg.Type()),
420 ArgTypeName(aliasIt->second))
421 .c_str());
422 }
423 }
424 }
425
426 // Add any subcommands of this command for validation.
427 for (auto& sub : current->GetCommands())
428 {
429 commands.push_back(std::move(sub));
430 }
431 }
432 }
433 };
434
435 } // namespace WSLCCLICommandUnitTests