CLI: Help improvements - Table output with color and routing support (#40848)

David Bennett committed Jul 13, 2026 at 10:41 UTC 9eda7524301d61c6bc2c5ab0b1630affb50567ac
60 files changed +2105 -2388
localization/strings/en-US/Resources.resw
+15
@@ -2209,6 +2209,21 @@ Usage:
2209 <data name="WSLCCLI_HelpForDetails" xml:space="preserve">
2210 <value>For more details on a specific command, pass it the help argument.</value>
2211 </data>
2212 + <data name="WSLCCLI_HeadingCommands" xml:space="preserve">
2213 + <value>Commands:</value>
2214 + </data>
2215 + <data name="WSLCCLI_HeadingOptions" xml:space="preserve">
2216 + <value>Options:</value>
2217 + </data>
2218 + <data name="WSLCCLI_HeadingGlobalOptions" xml:space="preserve">
2219 + <value>Global Options:</value>
2220 + </data>
2221 + <data name="WSLCCLI_HeadingAliases" xml:space="preserve">
2222 + <value>Aliases:</value>
2223 + </data>
2224 + <data name="WSLCCLI_HeadingArguments" xml:space="preserve">
2225 + <value>Arguments:</value>
2226 + </data>
2227 <data name="WSLCCLI_InvalidNameError" xml:space="preserve">
2228 <value>Argument name was not recognized for the current command: '{}'</value>
2229 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslc/commands/ContainerCommand.cpp
+1 -1
@@ -57,6 +57,6 @@ std::wstring ContainerCommand::LongDescription() const
57
58 void ContainerCommand::ExecuteInternal(CLIExecutionContext& context) const
59 {
60 - OutputHelp();
60 + OutputHelp(context.Reporter);
61 }
62 } // namespace wsl::windows::wslc
src/windows/wslc/commands/ImageCommand.cpp
+2 -2
@@ -53,6 +53,6 @@ std::wstring ImageCommand::LongDescription() const
53
54 void ImageCommand::ExecuteInternal(CLIExecutionContext& context) const
55 {
56 - OutputHelp();
56 + OutputHelp(context.Reporter);
57 }
58 -} // namespace wsl::windows::wslc
\ No newline at end of file
58 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/NetworkCommand.cpp
+1 -1
@@ -49,6 +49,6 @@ std::wstring NetworkCommand::LongDescription() const
49
50 void NetworkCommand::ExecuteInternal(CLIExecutionContext& context) const
51 {
52 - OutputHelp();
52 + OutputHelp(context.Reporter);
53 }
54 } // namespace wsl::windows::wslc
src/windows/wslc/commands/RegistryCommand.cpp
+1 -1
@@ -85,7 +85,7 @@ std::wstring RegistryCommand::LongDescription() const
85
86 void RegistryCommand::ExecuteInternal(CLIExecutionContext& context) const
87 {
88 - OutputHelp();
88 + OutputHelp(context.Reporter);
89 }
90
91 // Registry Login Command
src/windows/wslc/commands/RootCommand.cpp
+1 -1
@@ -109,6 +109,6 @@ void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
109 return;
110 }
111
112 - OutputHelp();
112 + OutputHelp(context.Reporter);
113 }
114 } // namespace wsl::windows::wslc
src/windows/wslc/commands/SessionCommand.cpp
+1 -1
@@ -48,6 +48,6 @@ std::wstring SessionCommand::LongDescription() const
48
49 void SessionCommand::ExecuteInternal(CLIExecutionContext& context) const
50 {
51 - OutputHelp();
51 + OutputHelp(context.Reporter);
52 }
53 } // namespace wsl::windows::wslc
src/windows/wslc/commands/SystemCommand.cpp
+1 -1
@@ -43,6 +43,6 @@ std::wstring SystemCommand::LongDescription() const
43
44 void SystemCommand::ExecuteInternal(CLIExecutionContext& context) const
45 {
46 - OutputHelp();
46 + OutputHelp(context.Reporter);
47 }
48 } // namespace wsl::windows::wslc
src/windows/wslc/commands/VolumeCommand.cpp
+1 -1
@@ -47,6 +47,6 @@ std::wstring VolumeCommand::LongDescription() const
47
48 void VolumeCommand::ExecuteInternal(CLIExecutionContext& context) const
49 {
50 - OutputHelp();
50 + OutputHelp(context.Reporter);
51 }
52 } // namespace wsl::windows::wslc
src/windows/wslc/core/Command.cpp
+210 -120
@@ -15,13 +15,18 @@ Abstract:
15 #include "Command.h"
16 #include "Invocation.h"
17 #include "ArgumentParser.h"
18 +#include "RootCommand.h"
19 +#include "TableOutput.h"
20
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::wslutil;
23 +using namespace wsl::windows::common::vt;
24 using namespace wsl::windows::wslc::execution;
25
26 namespace wsl::windows::wslc {
27
28 +std::wstring s_ExecutableName = L"wslc";
29 +
30 Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliases, const std::wstring& parent) :
31 m_name(name), m_aliases(std::move(aliases))
32 {
@@ -38,33 +43,32 @@ Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliase
43 }
44 }
45
41 -// This is the header applied before every help output.
42 -// It is separate in case we need to show it in other contexts, such as error messages, or
43 -// during specific command executions.
44 -void Command::OutputIntroHeader() const
46 +void Command::OutputHelp(Reporter& reporter, const CommandException* exception) const
47 {
46 - std::wostringstream infoOut;
47 - infoOut << Localization::WSLCCLI_CopyrightHeader() << std::endl;
48 - PrintMessage(infoOut.str(), stdout);
49 -}
48 + constexpr size_t c_helpRowIndent = 2;
49 + constexpr size_t c_helpColumnPadding = 2;
50 + const auto helpLevel = exception ? Reporter::Level::Info : Reporter::Level::Output;
51
51 -void Command::OutputHelp(const CommandException* exception) const
52 -{
53 - // Header
54 - OutputIntroHeader();
52 + // Emphasis sequences for help output.
53 + static const auto& HelpHeadingEmphasis = Format::Bright;
54 + static const auto& HelpCommandEmphasis = Format::Bright;
55 + static const auto& HelpArgumentEmphasis = Format::Bright;
56 + static const auto& HelpMetaEmphasis = Format::Dim;
57 + static const auto& HelpPlaceholderEmphasis = Format::Fg::BrightCyan;
58 +
59 + // Copyright header (dimmed)
60 + reporter.Write(helpLevel, L"{}{}{}\n\n", HelpMetaEmphasis, Localization::WSLCCLI_CopyrightHeader(), Format::Default);
61
62 // Error if given
63 if (exception)
64 {
59 - PrintMessage(exception->Message(), stderr);
65 + reporter.Error(L"{}\n\n", exception->Message());
66 }
67
68 // Description
63 - std::wostringstream infoOut;
64 - infoOut << LongDescription() << std::endl << std::endl;
69 + reporter.Write(helpLevel, L"{}\n\n", LongDescription());
70
66 - // Example usage for this command
67 - // First create the command chain for output
71 + // Build command chain from full name (replace ParentSplitChar with spaces, strip root).
72 std::wstring commandChain = FullName();
73 size_t firstSplit = commandChain.find_first_of(ParentSplitChar);
74 if (firstSplit == std::wstring::npos)
@@ -83,37 +87,23 @@ void Command::OutputHelp(const CommandException* exception) const
87 }
88 }
89
86 - // Usage follows the Microsoft convention:
87 - // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
88 -
89 - // Output the command preamble and command chain
90 - infoOut << Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
91 -
90 auto commandAliases = Aliases();
91 auto commands = GetCommands();
92 auto arguments = GetAllArguments();
93
96 - // Separate arguments by Kind
94 std::vector<Argument> standardArgs;
95 std::vector<Argument> positionalArgs;
96 std::vector<Argument> forwardArgs;
100 - bool requiredPositionalArgsExist = false;
97 for (const auto& arg : arguments)
98 {
99 switch (arg.Kind())
100 {
101 case Kind::Flag:
106 - standardArgs.emplace_back(arg);
107 - break;
102 case Kind::Value:
103 standardArgs.emplace_back(arg);
104 break;
105 case Kind::Positional:
106 positionalArgs.emplace_back(arg);
113 - if (arg.Required())
114 - {
115 - requiredPositionalArgsExist = true;
116 - }
107 break;
108 case Kind::Forward:
109 forwardArgs.emplace_back(arg);
@@ -121,154 +111,254 @@ void Command::OutputHelp(const CommandException* exception) const
111 }
112 }
113
124 - bool hasArguments = !positionalArgs.empty();
125 - bool hasOptions = !standardArgs.empty();
126 - bool hasForwardArgs = !forwardArgs.empty();
114 + const bool hasArguments = !positionalArgs.empty();
115 + const bool hasOptions = !standardArgs.empty();
116 + const bool hasForwardArgs = !forwardArgs.empty();
117
128 - // Output the command token, made optional if arguments are present.
129 - if (!commands.empty())
118 + // Global options from the root command, shown on every command's help.
119 + auto globalArgs = RootCommand().GetGlobalArguments();
120 +
121 + // Build usage line with Write calls for each segment.
122 {
131 - infoOut << ' ';
123 + std::wstring usageText = Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
124
133 - if (!arguments.empty())
125 + while (!usageText.empty() && usageText.back() == L' ')
126 {
135 - infoOut << L'[';
127 + usageText.pop_back();
128 }
129
138 - infoOut << L'<' << Localization::WSLCCLI_Command() << L'>';
130 + reporter.Write(helpLevel, L"{}{}{}", HelpHeadingEmphasis, usageText, Format::Default);
131
140 - if (!arguments.empty())
132 + if (!commands.empty())
133 {
142 - infoOut << L']';
143 - }
144 - }
145 -
146 - // For WSLC format of command [<options>] <positional> <args | positional2..>
147 -
148 - // Add options to the usage if there are options present.
149 - if (hasOptions)
150 - {
151 - infoOut << L" [<" << Localization::WSLCCLI_Options() << L">]";
152 - }
134 + if (!arguments.empty())
135 + {
136 + reporter.Write(helpLevel, L" {}[{}", HelpMetaEmphasis, Format::Default);
137 + }
138 + else
139 + {
140 + reporter.Write(helpLevel, L" ");
141 + }
142
154 - // Add arguments to the usage if there are arguments present. Positional come after
155 - // options and may be optional or required.
156 - for (const auto& arg : positionalArgs)
157 - {
158 - infoOut << L' ';
143 + reporter.Write(
144 + helpLevel,
145 + L"{}<{}{}{}{}{}>{}",
146 + HelpMetaEmphasis,
147 + Format::Default,
148 + HelpPlaceholderEmphasis,
149 + Localization::WSLCCLI_Command(),
150 + Format::Default,
151 + HelpMetaEmphasis,
152 + Format::Default);
153 + if (!arguments.empty())
154 + {
155 + reporter.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
156 + }
157 + }
158
160 - if (!arg.Required())
159 + if (hasOptions)
160 {
162 - infoOut << L'[';
161 + reporter.Write(
162 + helpLevel,
163 + L" {}[<{}{}{}{}{}>]{}",
164 + HelpMetaEmphasis,
165 + Format::Default,
166 + HelpPlaceholderEmphasis,
167 + Localization::WSLCCLI_Options(),
168 + Format::Default,
169 + HelpMetaEmphasis,
170 + Format::Default);
171 }
172
165 - infoOut << L'<' << arg.Name() << L'>';
166 -
167 - if (arg.Limit() > 1)
173 + for (const auto& arg : positionalArgs)
174 {
169 - infoOut << L"...";
175 + reporter.Write(helpLevel, L" ");
176 + if (!arg.Required())
177 + {
178 + reporter.Write(helpLevel, L"{}[{}", HelpMetaEmphasis, Format::Default);
179 + }
180 +
181 + reporter.Write(
182 + helpLevel, L"{}<{}{}{}{}{}>{}", HelpMetaEmphasis, Format::Default, HelpPlaceholderEmphasis, arg.Name(), Format::Default, HelpMetaEmphasis, Format::Default);
183 + if (arg.Limit() > 1)
184 + {
185 + reporter.Write(helpLevel, L"{}...{}", HelpMetaEmphasis, Format::Default);
186 + }
187 +
188 + if (!arg.Required())
189 + {
190 + reporter.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
191 + }
192 }
193
172 - if (!arg.Required())
194 + if (hasForwardArgs)
195 {
174 - infoOut << L']';
196 + reporter.Write(
197 + helpLevel,
198 + L" {}[<{}{}{}{}{}>...]{}",
199 + HelpMetaEmphasis,
200 + Format::Default,
201 + HelpPlaceholderEmphasis,
202 + forwardArgs.front().Name(),
203 + Format::Default,
204 + HelpMetaEmphasis,
205 + Format::Default);
206 }
176 - }
207
178 - if (hasForwardArgs)
179 - {
180 - // Assume only one forward arg is present, as multiple forwards would be
181 - // ambiguous in usage. Revisit if this becomes a scenario.
182 - infoOut << L" [<" << forwardArgs.front().Name() << L">...]";
208 + reporter.Write(helpLevel, L"\n\n");
209 }
210
185 - infoOut << std::endl << std::endl;
186 -
211 if (!commandAliases.empty())
212 {
189 - infoOut << Localization::WSLCCLI_AvailableCommandAliases() << L' ';
190 - infoOut << string::Join(commandAliases, L' ');
191 - infoOut << std::endl << std::endl;
192 - }
213 + reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingAliases(), Format::Default);
214
194 - if (!commands.empty())
195 - {
196 - if (Name() == FullName())
215 + std::wstring aliasLine;
216 + for (size_t i = 0; i < commandAliases.size(); ++i)
217 {
198 - infoOut << Localization::WSLCCLI_AvailableCommands() << std::endl;
199 - }
200 - else
201 - {
202 - infoOut << Localization::WSLCCLI_AvailableSubcommands() << std::endl;
218 + if (i != 0)
219 + {
220 + aliasLine += L", ";
221 + }
222 + aliasLine += commandAliases[i];
223 }
224
205 - size_t maxCommandNameLength = 0;
206 - for (const auto& command : commands)
207 - {
208 - maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length());
209 - }
225 + reporter.Write(helpLevel, L"{}{}\n\n", std::wstring(c_helpRowIndent, L' '), aliasLine);
226 + }
227
228 + // Col0: name/command
229 + // Col1: description (word-wraps at computed column width)
230 + const auto MakeHelpTable = [&reporter, helpLevel]() -> TableOutput<2> {
231 + TableOutput<2> table{reporter, {L"", L""}, 50, c_helpColumnPadding, helpLevel};
232 + table.SetShowHeader(false);
233 + table.SetRowIndent(c_helpRowIndent);
234 + table.SetColumnConfig(
235 + 1,
236 + ColumnWidthConfig{
237 + .MinWidth = ColumnWidthConfig::NoLimit,
238 + .MaxWidth = ColumnWidthConfig::NoLimit,
239 + .Overflow = ColumnOverflow::Wrap,
240 + });
241 + return table;
242 + };
243 +
244 + if (!commands.empty())
245 + {
246 + reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingCommands(), Format::Default);
247 +
248 + auto table = MakeHelpTable();
249 for (const auto& command : commands)
250 {
213 - size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2;
214 - infoOut << L" " << command->Name() << std::wstring(fillChars, L' ') << command->ShortDescription() << std::endl;
251 + table.WriteRow({
252 + FormattedCell(command->Name(), HelpCommandEmphasis),
253 + FormattedCell(command->ShortDescription()),
254 + });
255 }
256 + table.Complete();
257
217 - infoOut << std::endl << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L']' << std::endl;
258 + reporter.Write(helpLevel, L"\n{} [{}]\n", Localization::WSLCCLI_HelpForDetails(), WSLC_CLI_HELP_ARG_STRING);
259 }
260
261 if (!arguments.empty())
262 {
263 if (!commands.empty())
264 {
224 - infoOut << std::endl;
265 + reporter.Write(helpLevel, L"\n");
266 }
267
227 - size_t maxArgNameLength = 0;
228 - for (const auto& arg : arguments)
268 + // Arguments table: positional and forward args, name (emphasized) | description
269 + if (hasArguments || hasForwardArgs)
270 {
230 - auto argLength = arg.GetUsageString().length();
231 - maxArgNameLength = std::max(maxArgNameLength, argLength);
232 - }
271 + reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingArguments(), Format::Default);
272
234 - if (hasArguments)
235 - {
236 - infoOut << Localization::WSLCCLI_AvailableArguments() << std::endl;
273 + auto table = MakeHelpTable();
274
275 for (const auto& arg : positionalArgs)
276 {
240 - size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
241 - infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
277 + table.WriteRow({
278 + FormattedCell(arg.Name(), HelpArgumentEmphasis),
279 + FormattedCell(arg.Description()),
280 + });
281 }
243 - }
282
245 - if (hasForwardArgs)
246 - {
283 for (const auto& arg : forwardArgs)
284 {
249 - size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
250 - infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
285 + table.WriteRow({
286 + FormattedCell(arg.Name(), HelpArgumentEmphasis),
287 + FormattedCell(arg.Description()),
288 + });
289 }
290 +
291 + table.Complete();
292 }
293 + }
294
254 - if (hasOptions)
295 + // Col0: short alias (e.g. "-f")
296 + // Col1: long name (e.g. "--force")
297 + // Col2: description (word-wraps at computed column width)
298 + const auto MakeOptionsTable = [&reporter, helpLevel]() -> TableOutput<3> {
299 + TableOutput<3> table{reporter, {L"", L"", L""}, {}, 50, c_helpColumnPadding, helpLevel};
300 + table.SetShowHeader(false);
301 + table.SetRowIndent(c_helpRowIndent);
302 + table.SetColumnConfig(
303 + 2,
304 + ColumnWidthConfig{
305 + .MinWidth = ColumnWidthConfig::NoLimit,
306 + .MaxWidth = ColumnWidthConfig::NoLimit,
307 + .Overflow = ColumnOverflow::Wrap,
308 + });
309 + return table;
310 + };
311 +
312 + // Options table: alias (emphasized) | long name (emphasized) | description
313 + // Global options are appended to the same table so column widths are shared.
314 + if (hasOptions || !globalArgs.empty())
315 + {
316 + if (hasArguments || hasForwardArgs)
317 {
256 - if (hasArguments || hasForwardArgs)
318 + reporter.Write(helpLevel, L"\n");
319 + }
320 + else if (!commands.empty() && arguments.empty())
321 + {
322 + reporter.Write(helpLevel, L"\n");
323 + }
324 +
325 + auto table = MakeOptionsTable();
326 +
327 + const auto AddOptionRows = [&table](const std::vector<Argument>& args) {
328 + for (const auto& arg : args)
329 {
258 - infoOut << std::endl;
330 + FormattedCell aliasCell{L""};
331 + if (!arg.Alias().empty())
332 + {
333 + aliasCell = FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Alias(), HelpArgumentEmphasis);
334 + }
335 +
336 + table.WriteRow({
337 + std::move(aliasCell),
338 + FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Name(), HelpArgumentEmphasis),
339 + FormattedCell(arg.Description()),
340 + });
341 }
342 + };
343
261 - infoOut << Localization::WSLCCLI_AvailableOptions() << std::endl;
262 - for (const auto& arg : standardArgs)
344 + if (hasOptions)
345 + {
346 + table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingOptions(), HelpHeadingEmphasis));
347 + AddOptionRows(standardArgs);
348 + }
349 +
350 + if (!globalArgs.empty())
351 + {
352 + if (hasOptions)
353 {
264 - auto usage = arg.GetUsageString();
265 - size_t fillChars = (maxArgNameLength - usage.length()) + 2;
266 - infoOut << L" " << usage << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
354 + table.WriteLine();
355 }
356 + table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingGlobalOptions(), HelpHeadingEmphasis));
357 + AddOptionRows(globalArgs);
358 }
269 - }
359
271 - PrintMessage(infoOut.str(), stdout);
360 + table.Complete();
361 + }
362 }
363
364 std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
@@ -375,7 +465,7 @@ void Command::Execute(CLIExecutionContext& context) const
465 // If Help was part of the validated argument set, we will output help instead of executing.
466 if (context.Args.Contains(ArgType::Help))
467 {
378 - OutputHelp();
468 + OutputHelp(context.Reporter);
469 }
470 else
471 {
src/windows/wslc/core/Command.h
+4 -3
@@ -18,6 +18,7 @@ Abstract:
18 #include "CLIExecutionContext.h"
19 #include "Invocation.h"
20 #include "ArgumentParser.h"
21 +#include "Reporter.h"
22
23 #include <memory>
24 #include <optional>
@@ -30,7 +31,8 @@ using namespace wsl::windows::wslc::argument;
31
32 namespace wsl::windows::wslc {
33
33 -constexpr std::wstring_view s_ExecutableName = L"wslc";
34 +// The executable name shown in usage/help output, set from argv[0] at startup.
35 +extern std::wstring s_ExecutableName;
36
37 struct Command
38 {
@@ -99,8 +101,7 @@ struct Command
101 virtual std::wstring ShortDescription() const = 0;
102 virtual std::wstring LongDescription() const = 0;
103
102 - void OutputIntroHeader() const;
103 - void OutputHelp(const CommandException* exception = nullptr) const;
104 + void OutputHelp(Reporter& reporter, const CommandException* exception = nullptr) const;
105
106 std::unique_ptr<Command> FindSubCommand(Invocation& inv) const;
107
src/windows/wslc/core/Main.cpp
+18 -1
@@ -36,6 +36,23 @@ try
36 wslutil::ConfigureCrt();
37 wslutil::InitializeWil();
38
39 + // Extract the executable name from argv[0] for use in help/usage output.
40 + if (argc > 0 && argv[0])
41 + {
42 + std::wstring_view exe{argv[0]};
43 + auto lastSlash = exe.find_last_of(L"\\/");
44 + if (lastSlash != std::wstring_view::npos)
45 + {
46 + exe = exe.substr(lastSlash + 1);
47 + }
48 + auto dot = exe.rfind(L'.');
49 + if (dot != std::wstring_view::npos)
50 + {
51 + exe = exe.substr(0, dot);
52 + }
53 + s_ExecutableName = exe;
54 + }
55 +
56 WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild);
57 auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); });
58
@@ -121,7 +138,7 @@ try
138 catch (const CommandException& ce)
139 {
140 // Input failure: show help alongside the error so the user can correct it.
124 - command->OutputHelp(&ce);
141 + command->OutputHelp(context.Reporter, &ce);
142 return 1;
143 }
144 catch (...)
src/windows/wslc/core/TableOutput.cpp new
+174
@@ -0,0 +1,174 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + TableOutput.cpp
8 +
9 +Abstract:
10 +
11 + Non-templated implementation for TableOutput: FormattedCell rendering and
12 + the WrapText helper. The TableOutput<FieldCount> class template remains in
13 + the header.
14 +
15 +--*/
16 +#include "precomp.h"
17 +#include "TableOutput.h"
18 +
19 +using namespace wsl::windows::common::vt;
20 +
21 +namespace wsl::windows::wslc {
22 +
23 +FormattedCell::FormattedCell(std::wstring_view text, const Sequence& seq) : sequences({&seq, &Format::Default})
24 +{
25 + WI_ASSERT(text.find(L"{}") == std::wstring_view::npos);
26 + fmt.reserve(4 + text.size());
27 + fmt += L"{}";
28 + fmt += text;
29 + fmt += L"{}";
30 +}
31 +
32 +size_t FormattedCell::VisibleWidth() const
33 +{
34 + if (sequences.empty())
35 + {
36 + return fmt.size();
37 + }
38 +
39 + size_t width = 0;
40 + for (size_t i = 0; i < fmt.size(); ++i)
41 + {
42 + if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
43 + {
44 + ++i; // skip the pair
45 + }
46 + else
47 + {
48 + ++width;
49 + }
50 + }
51 + return width;
52 +}
53 +
54 +std::wstring FormattedCell::Render(bool vtEnabled, bool colorEnabled) const
55 +{
56 + if (sequences.empty())
57 + {
58 + return fmt; // plain text, no placeholders
59 + }
60 +
61 + std::wstring result;
62 + result.reserve(fmt.size() + (vtEnabled ? sequences.size() * 8 : 0));
63 + size_t seqIdx = 0;
64 +
65 + for (size_t i = 0; i < fmt.size(); ++i)
66 + {
67 + if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
68 + {
69 + if (vtEnabled && seqIdx < sequences.size() && (colorEnabled || !sequences[seqIdx]->IsColor()))
70 + {
71 + result.append(sequences[seqIdx]->Get());
72 + }
73 + ++seqIdx;
74 + ++i; // skip the pair
75 + }
76 + else
77 + {
78 + result += fmt[i];
79 + }
80 + }
81 +
82 + return result;
83 +}
84 +
85 +std::wstring FormattedCell::RenderTruncated(size_t maxWidth, bool vtEnabled, bool colorEnabled) const
86 +{
87 + if (sequences.empty())
88 + {
89 + // Plain text: simple truncation.
90 + if (fmt.size() <= maxWidth)
91 + {
92 + return fmt;
93 + }
94 + return fmt.substr(0, maxWidth > 0 ? maxWidth - 1 : 0) + L"\u2026";
95 + }
96 +
97 + std::wstring result;
98 + result.reserve(fmt.size());
99 + size_t seqIdx = 0;
100 + size_t visibleChars = 0;
101 + bool truncated = (maxWidth == 0);
102 + const size_t truncateAt = maxWidth > 1 ? maxWidth - 1 : 0;
103 +
104 + for (size_t i = 0; i < fmt.size(); ++i)
105 + {
106 + if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
107 + {
108 + // Always emit sequences (they're invisible); they handle resets after truncation.
109 + if (vtEnabled && seqIdx < sequences.size() && (colorEnabled || !sequences[seqIdx]->IsColor()))
110 + {
111 + result.append(sequences[seqIdx]->Get());
112 + }
113 + ++seqIdx;
114 + ++i;
115 + }
116 + else if (!truncated)
117 + {
118 + if (visibleChars < truncateAt)
119 + {
120 + result += fmt[i];
121 + ++visibleChars;
122 + }
123 + else
124 + {
125 + result += L'\u2026';
126 + truncated = true;
127 + }
128 + }
129 + // After truncation, skip remaining visible chars but continue to emit sequences.
130 + }
131 +
132 + return result;
133 +}
134 +
135 +namespace details {
136 +
137 + std::vector<std::wstring> WrapText(const std::wstring& text, size_t maxWidth)
138 + {
139 + if (maxWidth == 0 || text.length() <= maxWidth)
140 + {
141 + return {text};
142 + }
143 +
144 + std::vector<std::wstring> lines;
145 + size_t pos = 0;
146 +
147 + while (pos < text.length())
148 + {
149 + size_t chunkEnd = std::min(pos + maxWidth, text.length());
150 +
151 + if (chunkEnd < text.length())
152 + {
153 + size_t breakAt = text.rfind(L' ', chunkEnd);
154 + if (breakAt != std::wstring::npos && breakAt > pos)
155 + {
156 + chunkEnd = breakAt;
157 + }
158 + }
159 +
160 + lines.emplace_back(text.substr(pos, chunkEnd - pos));
161 +
162 + pos = chunkEnd;
163 + while (pos < text.length() && text[pos] == L' ')
164 + {
165 + ++pos;
166 + }
167 + }
168 +
169 + return lines;
170 + }
171 +
172 +} // namespace details
173 +
174 +} // namespace wsl::windows::wslc
src/windows/wslc/core/TableOutput.h
+378 -189
@@ -8,119 +8,178 @@ Module Name:
8
9 Abstract:
10
11 - Header file for outputting data in a table format.
11 + Structured table output for the WSLC CLI. Cells are either plain text or
12 + format-string + Sequence args. Sequences are zero display width; the table
13 + measures visible width by counting non-placeholder characters. At render
14 + time, sequences are emitted or stripped based on Reporter color state.
15
16 --*/
17 #pragma once
18
19 #include <algorithm>
20 #include <array>
18 -#include <cwchar>
19 -#include <functional>
20 -#include <sstream>
21 +#include <initializer_list>
22 +#include <optional>
23 #include <string>
24 #include <utility>
25 +#include <variant>
26 #include <vector>
24 -#include <wslutil.h>
27 +#include <wil/result_macros.h>
28 +#include "Reporter.h"
29 +#include "VTSupport.h"
30
31 namespace wsl::windows::wslc {
32
28 -namespace detail {
29 - // This function outputs a table line.
30 - inline void PrintTableLine(const std::wstring& line, FILE* stream)
33 +using wsl::windows::common::vt::Sequence;
34 +
35 +// A table cell: either plain text or a format string with Sequence placeholders.
36 +// Every {} in the format string corresponds to a Sequence (zero display width).
37 +// Visible width is the count of non-placeholder characters in the format string.
38 +struct FormattedCell
39 +{
40 + std::wstring fmt;
41 + std::vector<const Sequence*> sequences;
42 +
43 + // Default constructor — empty cell.
44 + FormattedCell() = default;
45 +
46 + // Implicit from wstring — plain text cell (no formatting).
47 + FormattedCell(std::wstring text) : fmt(std::move(text))
48 {
32 - ::wsl::windows::common::wslutil::PrintMessage(line, stream);
49 }
34 -} // namespace detail
50
36 -// Helper function to get display width of a string
37 -// For now, uses simple length (can be enhanced with proper Unicode width calculation)
38 -inline size_t GetStringColumnWidth(const wchar_t* str)
39 -{
40 - if (!str)
51 + // Implicit from wstring_view.
52 + FormattedCell(std::wstring_view text) : fmt(text)
53 {
42 - return 0;
54 }
44 - return wcslen(str);
45 -}
55
47 -// Helper function to trim string to a specific column width
48 -inline std::wstring TrimStringToColumnWidth(const wchar_t* str, size_t maxWidth, size_t& actualWidth)
49 -{
50 - if (!str)
56 + // Implicit from literal.
57 + FormattedCell(const wchar_t* text) : fmt(text)
58 {
52 - actualWidth = 0;
53 - return L"";
59 }
60
56 - size_t len = wcslen(str);
57 - if (len <= maxWidth)
61 + // Formatted cell: format string with Sequence placeholders.
62 + FormattedCell(std::wstring format, std::initializer_list<const Sequence*> seqs) : fmt(std::move(format)), sequences(seqs)
63 {
59 - actualWidth = len;
60 - return std::wstring(str);
64 }
65
63 - actualWidth = maxWidth;
64 - return std::wstring(str, maxWidth);
65 -}
66 + // Single-sequence cell: wraps text with the sequence and a trailing reset.
67 + FormattedCell(std::wstring_view text, const Sequence& seq);
68 +
69 + // Block temporaries: the cell only stores a pointer to seq, so binding a Sequence rvalue (including
70 + // derived types such as the ConstructedSequence returned by Sgr()) would dangle once the full
71 + // expression ends. Only long-lived Sequence instances may be used here.
72 + FormattedCell(std::wstring_view text, const Sequence&& seq) = delete;
73 +
74 + // Visible width: count characters that are not part of {} placeholders.
75 + size_t VisibleWidth() const;
76 +
77 + // Renders the cell with or without sequences.
78 + // When vtEnabled is false, all {} placeholders are skipped (no VT output).
79 + // When vtEnabled is true but colorEnabled is false, only non-color sequences are emitted.
80 + // When both are true, all sequences are emitted.
81 + std::wstring Render(bool vtEnabled, bool colorEnabled) const;
82 +
83 + // Renders with visible text truncated to maxWidth characters, appending ellipsis.
84 + // Sequences after the truncation point are still emitted (for resets).
85 + std::wstring RenderTruncated(size_t maxWidth, bool vtEnabled, bool colorEnabled) const;
86 +};
87 +
88 +// Controls how a column handles content that exceeds its available width.
89 +enum class ColumnOverflow
90 +{
91 + // Truncates content with an ellipsis at MaxWidth; column width is fixed and does not
92 + // participate in the shrink loop.
93 + Truncate,
94 +
95 + // Participates in the shrink loop: reduced largest-first down to MinWidth, then truncated.
96 + // PreferredShrink=true marks this as a higher-priority shrink target.
97 + Shrink,
98 +
99 + // Wraps long values across multiple physical rows; width is remaining space after other columns.
100 + Wrap,
101 +};
102
67 -// Column width configuration options
103 struct ColumnWidthConfig
104 {
105 static constexpr size_t NoLimit = 0;
106
72 - size_t MinWidth = NoLimit; // Minimum column width (NoLimit = use header width)
73 - size_t MaxWidth = NoLimit; // Maximum column width (NoLimit = unlimited)
74 - bool PreferredShrink = true; // Should this column shrink first when space is limited?
107 + size_t MinWidth = NoLimit; // Minimum visible width (NoLimit = header width).
108 + size_t MaxWidth = NoLimit; // Maximum visible width cap (NoLimit = unlimited).
109 + ColumnOverflow Overflow = ColumnOverflow::Truncate;
110 + bool PreferredShrink = true; // Prioritizes this column in the shrink loop.
111 };
112
77 -// Column definition with name and configuration
113 struct ColumnDefinition
114 {
115 std::wstring Name;
116 ColumnWidthConfig Config;
117 };
118
84 -// Enables output data in a table format.
85 -// TODO: Improve for use with sparse data.
119 +namespace details {
120 +
121 + // Splits visible text into word-boundary chunks of at most maxWidth chars.
122 + std::vector<std::wstring> WrapText(const std::wstring& text, size_t maxWidth);
123 +
124 +} // namespace details
125 +
126 template <size_t FieldCount>
127 struct TableOutput
128 {
129 static_assert(FieldCount > 0, "TableOutput requires at least one column");
130
131 using header_t = std::array<std::wstring, FieldCount>;
92 - using line_t = std::array<std::wstring, FieldCount>;
132 + using line_t = std::array<FormattedCell, FieldCount>;
133 using column_config_t = std::array<ColumnWidthConfig, FieldCount>;
134 using column_def_t = std::array<ColumnDefinition, FieldCount>;
95 - using OutputFn = std::function<void(const std::wstring&)>;
135
136 static constexpr size_t DefaultColumnPadding = 3; // Docker-like spacing between columns
137
99 - // For redirected console the receiver controls the width. This should be a large value but not
100 - // too large. A few thousand should be reasonable and prevents potential arithmetic issues later.
138 + // Generous fallback used when the destination is redirected (no real console width).
139 + // The wrap pass is skipped in that case so the receiver controls its own width.
140 static constexpr size_t DefaultRedirectedConsoleWidth = 2000;
141
103 - // Constructor with default behavior (no column limits)
104 - TableOutput(header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
105 - m_sizingBuffer(sizingBuffer), m_limitColumnWidths(false), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
142 + TableOutput(Reporter& reporter, header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding, Reporter::Level level = Reporter::Level::Output) :
143 + m_reporter(reporter),
144 + m_outputLevel(level),
145 + m_vtEnabled(reporter.IsVTEnabled(level)),
146 + m_colorEnabled(reporter.IsColorEnabled(level)),
147 + m_sizingBuffer(sizingBuffer),
148 + m_columnPadding(columnPadding)
149 {
150 InitializeColumns(std::move(header));
151 }
152
110 - // Constructor with column width configuration (legacy)
111 - TableOutput(header_t&& header, column_config_t&& config, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
153 + TableOutput(
154 + Reporter& reporter,
155 + header_t&& header,
156 + column_config_t&& config,
157 + size_t sizingBuffer = 50,
158 + size_t columnPadding = DefaultColumnPadding,
159 + Reporter::Level level = Reporter::Level::Output) :
160 + m_reporter(reporter),
161 + m_outputLevel(level),
162 + m_vtEnabled(reporter.IsVTEnabled(level)),
163 + m_colorEnabled(reporter.IsColorEnabled(level)),
164 m_sizingBuffer(sizingBuffer),
113 - m_limitColumnWidths(true),
165 m_columnPadding(columnPadding),
115 - m_columnConfigs(std::move(config)),
116 - m_outputFn(DefaultOutputFn())
166 + m_columnConfigs(std::move(config))
167 {
168 InitializeColumns(std::move(header));
169 }
170
121 - // Constructor with column definitions (name + config together)
122 - TableOutput(column_def_t&& columns, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
123 - m_sizingBuffer(sizingBuffer), m_limitColumnWidths(true), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
171 + TableOutput(
172 + Reporter& reporter,
173 + column_def_t&& columns,
174 + size_t sizingBuffer = 50,
175 + size_t columnPadding = DefaultColumnPadding,
176 + Reporter::Level level = Reporter::Level::Output) :
177 + m_reporter(reporter),
178 + m_outputLevel(level),
179 + m_vtEnabled(reporter.IsVTEnabled(level)),
180 + m_colorEnabled(reporter.IsColorEnabled(level)),
181 + m_sizingBuffer(sizingBuffer),
182 + m_columnPadding(columnPadding)
183 {
184 header_t headers;
185 for (size_t i = 0; i < FieldCount; ++i)
@@ -131,56 +190,49 @@ struct TableOutput
190 InitializeColumns(std::move(headers));
191 }
192
134 - // Enable/disable column width limiting
135 - void SetColumnWidthLimiting(bool enable)
136 - {
137 - m_limitColumnWidths = enable;
138 - }
139 -
140 - // Set configuration for a specific column
193 + // Updates config store and Column state; safe to call after construction.
194 void SetColumnConfig(size_t columnIndex, const ColumnWidthConfig& config)
195 {
196 if (columnIndex < FieldCount)
197 {
198 m_columnConfigs[columnIndex] = config;
199 + SyncColumnFromConfig(columnIndex);
200 }
201 }
202
149 - // Set whether to always show header even when there are no rows
203 void SetAlwaysShowHeader(bool alwaysShow)
204 {
205 m_alwaysShowHeader = alwaysShow;
206 }
154 -
155 - // Set whether to show the header row
207 void SetShowHeader(bool showHeader)
208 {
209 m_showHeader = showHeader;
210 }
160 -
161 - // Override the output function (e.g. redirect to a stringstream in tests).
162 - void SetOutputFunction(OutputFn fn)
211 + // Sets spaces prepended to every row. Does not affect column width calculations.
212 + void SetRowIndent(size_t spaces)
213 {
164 - FAIL_FAST_IF_MSG(!fn, "OutputFn must not be empty");
165 - m_outputFn = std::move(fn);
214 + m_rowIndent = spaces;
215 }
216
168 - // Override the console width used for column shrinking (useful in tests).
169 - // Pass 0 to restore the default behaviour (query the real console).
217 + // Overrides console width for column shrinking; pass 0 to restore default (Reporter-derived).
218 + // When set, the wrap pass also runs as if a real console were attached.
219 void SetConsoleWidthOverride(size_t width)
220 {
221 m_consoleWidthOverride = width;
222 }
223
175 - void OutputLine(line_t&& line)
224 + void WriteRow(line_t&& line)
225 {
226 m_empty = false;
227
179 - // When width limiting is disabled, buffer all rows to ensure accurate column sizing
180 - // and prevent truncation (e.g., for --no-trunc flag)
181 - if (!m_limitColumnWidths || m_buffer.size() < m_sizingBuffer)
228 + // Buffer rows to size columns before flush. When every column is unbounded (no MaxWidth cap
229 + // and no Wrap/Shrink overflow), buffer all rows so column widths grow to fit the widest value
230 + // regardless of row order. With an overflow policy in play, cap the buffer at m_sizingBuffer
231 + // and stream the remainder to bound memory for large result sets.
232 + if (m_dataRowCount < m_sizingBuffer || AllColumnsUnbounded())
233 {
234 m_buffer.emplace_back(std::move(line));
235 + ++m_dataRowCount;
236 }
237 else
238 {
@@ -189,6 +241,22 @@ struct TableOutput
241 }
242 }
243
244 + // Emits a standalone text line that does not participate in column sizing.
245 + // Use for section headers or blank separators between data rows.
246 + void WriteLine(FormattedCell cell = {})
247 + {
248 + m_empty = false;
249 +
250 + if (!m_bufferEvaluated)
251 + {
252 + m_buffer.emplace_back(std::move(cell));
253 + }
254 + else
255 + {
256 + OutputCellLineToStream(cell);
257 + }
258 + }
259 +
260 void Complete()
261 {
262 if (!m_empty)
@@ -207,7 +275,9 @@ struct TableOutput
275 }
276
277 private:
210 - // A column in the table.
278 + // A break entry is a FormattedCell rendered as a standalone line (section header or blank).
279 + using buffer_entry_t = std::variant<line_t, FormattedCell>;
280 +
281 struct Column
282 {
283 std::wstring Name;
@@ -215,26 +285,58 @@ private:
285 size_t MaxLength = 0;
286 size_t ConfiguredMaxLength = 0; // Max length from configuration
287 bool SpaceAfter = true;
288 + ColumnOverflow Overflow = ColumnOverflow::Truncate;
289 };
290
291 + Reporter& m_reporter;
292 + Reporter::Level m_outputLevel;
293 + const bool m_vtEnabled;
294 + const bool m_colorEnabled;
295 std::array<Column, FieldCount> m_columns;
296 column_config_t m_columnConfigs;
297 size_t m_sizingBuffer;
298 size_t m_columnPadding;
224 - std::vector<line_t> m_buffer;
299 + size_t m_rowIndent = 0;
300 + std::vector<buffer_entry_t> m_buffer;
301 + size_t m_dataRowCount = 0;
302 bool m_bufferEvaluated = false;
303 bool m_empty = true;
227 - bool m_limitColumnWidths = false;
304 bool m_alwaysShowHeader = true;
305 bool m_showHeader = true;
306 bool m_dropEmptyColumns = false;
231 - std::wstringstream m_stream;
232 - OutputFn m_outputFn;
307 size_t m_consoleWidthOverride = 0;
308
235 - static OutputFn DefaultOutputFn()
309 + // True when no column constrains its width (no MaxWidth cap and no Wrap/Shrink overflow).
310 + // Such tables buffer every row so a late, wide value is never truncated or misaligned.
311 + bool AllColumnsUnbounded() const
312 {
237 - return [](const std::wstring& line) { detail::PrintTableLine(line, stdout); };
313 + for (size_t i = 0; i < FieldCount; ++i)
314 + {
315 + if (m_columns[i].ConfiguredMaxLength != 0 || m_columns[i].Overflow != ColumnOverflow::Truncate)
316 + {
317 + return false;
318 + }
319 + }
320 + return true;
321 + }
322 +
323 + // Syncs Column state from m_columnConfigs[i]; call whenever a config entry changes.
324 + void SyncColumnFromConfig(size_t i)
325 + {
326 + auto& col = m_columns[i];
327 + const auto& cfg = m_columnConfigs[i];
328 +
329 + col.Overflow = cfg.Overflow;
330 + col.ConfiguredMaxLength = (cfg.MaxWidth != ColumnWidthConfig::NoLimit) ? cfg.MaxWidth : 0;
331 +
332 + if (cfg.MinWidth != ColumnWidthConfig::NoLimit)
333 + {
334 + col.MinLength = std::max(col.Name.size(), cfg.MinWidth);
335 + }
336 + else
337 + {
338 + col.MinLength = col.Name.size();
339 + }
340 }
341
342 void InitializeColumns(header_t&& header)
@@ -242,59 +344,101 @@ private:
344 for (size_t i = 0; i < FieldCount; ++i)
345 {
346 m_columns[i].Name = std::move(header[i]);
245 - m_columns[i].MinLength = GetStringColumnWidth(m_columns[i].Name.c_str());
347 m_columns[i].MaxLength = 0;
247 -
248 - // Apply configured max width if limiting is enabled
249 - if (m_limitColumnWidths && m_columnConfigs[i].MaxWidth != ColumnWidthConfig::NoLimit)
250 - {
251 - m_columns[i].ConfiguredMaxLength = m_columnConfigs[i].MaxWidth;
252 - }
253 -
254 - // Apply configured min width
255 - if (m_columnConfigs[i].MinWidth != ColumnWidthConfig::NoLimit)
256 - {
257 - m_columns[i].MinLength = std::max(m_columns[i].MinLength, m_columnConfigs[i].MinWidth);
258 - }
348 + SyncColumnFromConfig(i);
349 }
350 }
351
262 - size_t GetConsoleWidth()
352 + // Returns the effective console width (in columns) of the destination, or std::nullopt
353 + // when the destination is redirected. SetConsoleWidthOverride() takes precedence and is
354 + // treated as a real console (the wrap pass uses has_value() to gate its behavior).
355 + std::optional<size_t> GetEffectiveConsoleWidth() const
356 {
357 if (m_consoleWidthOverride > 0)
358 {
359 return m_consoleWidthOverride;
360 }
361
269 - CONSOLE_SCREEN_BUFFER_INFO consoleInfo{};
270 - HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
362 + if (const auto width = m_reporter.GetConsoleWidth(m_outputLevel); width.has_value())
363 + {
364 + return static_cast<size_t>(*width);
365 + }
366 +
367 + return std::nullopt;
368 + }
369 +
370 + // Wraps a cell's visible text into chunks, preserving formatting on each chunk.
371 + std::vector<FormattedCell> BuildWrappedCells(const FormattedCell& cell, const Column& col) const
372 + {
373 + if (col.Overflow != ColumnOverflow::Wrap || col.MaxLength == 0)
374 + {
375 + return {cell};
376 + }
377 +
378 + // Extract the visible text for wrapping.
379 + const size_t visWidth = cell.VisibleWidth();
380 + if (visWidth <= col.MaxLength)
381 + {
382 + return {cell};
383 + }
384 +
385 + // For plain cells, wrap the text directly.
386 + if (cell.sequences.empty())
387 + {
388 + auto chunks = details::WrapText(cell.fmt, col.MaxLength);
389 + std::vector<FormattedCell> result;
390 + result.reserve(chunks.size());
391 + for (auto& chunk : chunks)
392 + {
393 + result.emplace_back(std::move(chunk));
394 + }
395 + return result;
396 + }
397
272 - if (GetConsoleScreenBufferInfo(hConsole, &consoleInfo))
398 + // Wrapping only supports single-style cells (open + reset). Complex cells with
399 + // multiple sequences (e.g., hyperlinks with distinct open/close pairs) cannot be
400 + // reliably split across wrapped lines. Callers needing rich formatting in a wrapped
401 + // column should use a single constructed Sequence that combines all escape codes.
402 + THROW_HR_IF(E_INVALIDARG, cell.sequences.size() > 2);
403 +
404 + // For formatted cells, extract visible text, wrap it, then re-apply formatting.
405 + std::wstring visibleText;
406 + visibleText.reserve(visWidth);
407 + for (size_t i = 0; i < cell.fmt.size(); ++i)
408 {
274 - return static_cast<size_t>(consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
409 + if (i + 1 < cell.fmt.size() && cell.fmt[i] == L'{' && cell.fmt[i + 1] == L'}')
410 + {
411 + ++i;
412 + }
413 + else
414 + {
415 + visibleText += cell.fmt[i];
416 + }
417 }
418
277 - // stdout is not a real console (e.g. redirected/piped). Return a large value
278 - // so column shrinking is not applied — the receiver controls its own display width.
279 - return DefaultRedirectedConsoleWidth;
419 + auto chunks = details::WrapText(visibleText, col.MaxLength);
420 + std::vector<FormattedCell> result;
421 + result.reserve(chunks.size());
422 + for (auto& chunk : chunks)
423 + {
424 + result.emplace_back(FormattedCell(std::wstring_view{chunk}, *cell.sequences.front()));
425 + }
426 + return result;
427 }
428
429 void OutputHeaderOnly()
430 {
284 - // Set MaxLength to MinLength for all columns (header width only)
431 for (size_t i = 0; i < FieldCount; ++i)
432 {
433 m_columns[i].MaxLength = m_columns[i].MinLength;
434 }
435
290 - // Set spacing configuration
436 m_columns[FieldCount - 1].SpaceAfter = false;
437
293 - // Output the header
438 line_t headerLine;
439 for (size_t i = 0; i < FieldCount; ++i)
440 {
297 - headerLine[i] = m_columns[i].Name.c_str();
441 + headerLine[i] = FormattedCell(m_columns[i].Name);
442 }
443
444 OutputLineToStream(headerLine);
@@ -308,26 +452,29 @@ private:
452 return;
453 }
454
311 - // Determine the maximum length for all columns
312 - for (const auto& line : m_buffer)
455 + // Determine the maximum visible width for each column across all buffered data rows.
456 + for (const auto& entry : m_buffer)
457 {
458 + const auto* line = std::get_if<line_t>(&entry);
459 + if (!line)
460 + {
461 + continue;
462 + }
463 +
464 for (size_t i = 0; i < FieldCount; ++i)
465 {
316 - size_t columnWidth = GetStringColumnWidth(line[i].c_str());
466 + size_t w = (*line)[i].VisibleWidth();
467
318 - // Apply configured max width if limiting is enabled
319 - if (m_limitColumnWidths && m_columns[i].ConfiguredMaxLength != ColumnWidthConfig::NoLimit)
468 + if (m_columns[i].ConfiguredMaxLength != ColumnWidthConfig::NoLimit)
469 {
321 - columnWidth = std::min(columnWidth, m_columns[i].ConfiguredMaxLength);
470 + w = std::min(w, m_columns[i].ConfiguredMaxLength);
471 }
472
324 - m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, columnWidth);
473 + m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, w);
474 }
475 }
476
328 - // If there are actually columns with data, then also bring in the minimum size.
329 - // When m_dropEmptyColumns is false, always apply MinLength so empty columns
330 - // still render at least as wide as their header.
477 + // Apply MinLength so empty columns still render at least as wide as their header.
478 for (size_t i = 0; i < FieldCount; ++i)
479 {
480 if (m_columns[i].MaxLength || !m_dropEmptyColumns)
@@ -336,84 +483,102 @@ private:
483 }
484 }
485
339 - // Only output the extra space if:
340 - // 1. Not the last field
486 + // Last column never needs trailing padding.
487 m_columns[FieldCount - 1].SpaceAfter = false;
488
343 - // 2. Not empty (taken care of by not doing anything if empty)
344 - // 3. There are non-empty fields after
489 + // Disable SpaceAfter on columns that are followed only by empty columns.
490 for (size_t i = FieldCount - 1; i > 0; --i)
491 {
492 if (m_columns[i].MaxLength)
493 {
494 break;
495 }
351 - else
352 - {
353 - m_columns[i - 1].SpaceAfter = false;
354 - }
496 + m_columns[i - 1].SpaceAfter = false;
497 }
498
357 - // Determine the total width required to not truncate any columns
499 + // Compute total visible width required to not truncate any columns.
500 size_t totalRequired = 0;
359 -
501 for (size_t i = 0; i < FieldCount; ++i)
502 {
503 totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? m_columnPadding : 0);
504 }
505
365 - // Only apply console width constraints if m_limitColumnWidths is true
366 - if (m_limitColumnWidths)
506 + const auto consoleWidthOpt = GetEffectiveConsoleWidth();
507 + const size_t consoleWidth = consoleWidthOpt.value_or(DefaultRedirectedConsoleWidth);
508 + const size_t availableWidth = (consoleWidth > m_rowIndent) ? consoleWidth - m_rowIndent : 0;
509 +
510 + // Shrink pass: reduce Shrink columns until the total fits within the available width.
511 + if (totalRequired > availableWidth)
512 {
368 - size_t consoleWidth = GetConsoleWidth();
513 + size_t extra = totalRequired - availableWidth;
514
370 - // If the total space would be too big, shrink them.
371 - // We don't want to use the last column, lest we auto-wrap
372 - if (totalRequired >= consoleWidth)
515 + while (extra > 0)
516 {
374 - size_t extra = (totalRequired - consoleWidth) + 1;
517 + size_t targetIndex = FieldCount;
518 + size_t targetVal = 0;
519
376 - while (extra > 0)
520 + for (size_t j = 0; j < FieldCount; ++j)
521 {
378 - // Find the largest shrinkable column
379 - size_t targetIndex = 0;
380 - size_t targetVal = 0;
381 -
382 - for (size_t j = 0; j < FieldCount; ++j)
522 + if (m_columns[j].Overflow != ColumnOverflow::Shrink)
523 {
384 - // Skip columns at or below minimum
385 - if (m_columns[j].MaxLength <= m_columns[j].MinLength)
386 - {
387 - continue;
388 - }
389 -
390 - // Prefer columns marked as preferredShrink
391 - bool isPreferredShrink = m_columnConfigs[j].PreferredShrink;
392 - bool currentIsPreferred = m_columnConfigs[targetIndex].PreferredShrink;
393 -
394 - if (isPreferredShrink && !currentIsPreferred)
395 - {
396 - targetIndex = j;
397 - targetVal = m_columns[j].MaxLength;
398 - }
399 - else if (isPreferredShrink == currentIsPreferred && m_columns[j].MaxLength > targetVal)
400 - {
401 - targetIndex = j;
402 - targetVal = m_columns[j].MaxLength;
403 - }
524 + continue;
525 }
526 + if (m_columns[j].MaxLength <= m_columns[j].MinLength)
527 + {
528 + continue;
529 + }
530 +
531 + const bool isPreferred = m_columnConfigs[j].PreferredShrink;
532 + const bool currentPreferred = (targetIndex < FieldCount) ? m_columnConfigs[targetIndex].PreferredShrink : false;
533
406 - // If no shrinkable column found, break
407 - if (targetVal == 0)
534 + if (targetIndex == FieldCount || (isPreferred && !currentPreferred) ||
535 + (isPreferred == currentPreferred && m_columns[j].MaxLength > targetVal))
536 {
409 - break;
537 + targetIndex = j;
538 + targetVal = m_columns[j].MaxLength;
539 }
540 + }
541
412 - m_columns[targetIndex].MaxLength -= 1;
413 - extra -= 1;
542 + if (targetIndex == FieldCount)
543 + {
544 + break;
545 }
546
416 - totalRequired = std::min(totalRequired, consoleWidth - 1);
547 + m_columns[targetIndex].MaxLength -= 1;
548 + extra -= 1;
549 + }
550 + }
551 +
552 + // Wrap pass: clamp each Wrap column to remaining space after all other columns.
553 + // Skipped when the destination is redirected so the receiver controls its own width.
554 + if (consoleWidthOpt.has_value())
555 + {
556 + for (size_t i = 0; i < FieldCount; ++i)
557 + {
558 + if (m_columns[i].Overflow != ColumnOverflow::Wrap || !m_columns[i].MaxLength)
559 + {
560 + continue;
561 + }
562 +
563 + size_t otherWidth = 0;
564 + for (size_t j = 0; j < FieldCount; ++j)
565 + {
566 + if (j != i)
567 + {
568 + otherWidth += m_columns[j].MaxLength + (m_columns[j].SpaceAfter ? m_columnPadding : 0);
569 + }
570 + }
571 + if (m_columns[i].SpaceAfter)
572 + {
573 + otherWidth += m_columnPadding;
574 + }
575 +
576 + const size_t wrapBudget = (availableWidth > otherWidth) ? availableWidth - otherWidth : 1;
577 +
578 + if (m_columns[i].MaxLength > wrapBudget)
579 + {
580 + m_columns[i].MaxLength = std::max(wrapBudget, m_columns[i].MinLength);
581 + }
582 }
583 }
584
@@ -422,63 +587,87 @@ private:
587 line_t headerLine;
588 for (size_t i = 0; i < FieldCount; ++i)
589 {
425 - headerLine[i] = m_columns[i].Name.c_str();
590 + headerLine[i] = FormattedCell(m_columns[i].Name);
591 }
427 -
592 OutputLineToStream(headerLine);
593 }
594
431 - for (const auto& line : m_buffer)
595 + for (const auto& entry : m_buffer)
596 {
433 - OutputLineToStream(line);
597 + if (const auto* line = std::get_if<line_t>(&entry))
598 + {
599 + OutputLineToStream(*line);
600 + }
601 + else if (const auto* cell = std::get_if<FormattedCell>(&entry))
602 + {
603 + OutputCellLineToStream(*cell);
604 + }
605 }
606
607 m_bufferEvaluated = true;
608 }
609
610 + void OutputCellLineToStream(const FormattedCell& cell)
611 + {
612 + m_reporter.Write(m_outputLevel, L"{}\n", cell.Render(m_vtEnabled, m_colorEnabled));
613 + }
614 +
615 + // Renders a logical row, emitting multiple physical rows for word-wrapping columns.
616 void OutputLineToStream(const line_t& line)
617 {
618 + size_t physicalRows = 1;
619 + std::array<std::vector<FormattedCell>, FieldCount> wrappedCells;
620 for (size_t i = 0; i < FieldCount; ++i)
621 {
443 - const auto& col = m_columns[i];
622 + wrappedCells[i] = BuildWrappedCells(line[i], m_columns[i]);
623 + physicalRows = std::max(physicalRows, wrappedCells[i].size());
624 + }
625
445 - if (col.MaxLength)
626 + for (size_t row = 0; row < physicalRows; ++row)
627 + {
628 + std::wstring rowStr;
629 +
630 + if (m_rowIndent > 0)
631 {
447 - size_t valueLength = GetStringColumnWidth(line[i].c_str());
632 + rowStr.append(m_rowIndent, L' ');
633 + }
634
449 - if (valueLength > col.MaxLength)
635 + for (size_t i = 0; i < FieldCount; ++i)
636 + {
637 + const auto& col = m_columns[i];
638 + if (!col.MaxLength)
639 {
451 - size_t actualWidth;
452 - m_stream << TrimStringToColumnWidth(line[i].c_str(), col.MaxLength - 1, actualWidth) << L"\u2026"; // Unicode ellipsis character
640 + continue;
641 + }
642
454 - // Some characters take 2 unit space, the trimmed string length might be 1 less than the expected length.
455 - if (actualWidth != col.MaxLength - 1)
456 - {
457 - m_stream << L' ';
458 - }
643 + // On continuation rows, exhausted columns render as blank.
644 + static const FormattedCell emptyCell{L""};
645 + const FormattedCell& cell = (row < wrappedCells[i].size()) ? wrappedCells[i][row] : emptyCell;
646 + const size_t valueLength = cell.VisibleWidth();
647 +
648 + if (col.Overflow != ColumnOverflow::Wrap && valueLength > col.MaxLength)
649 + {
650 + // Truncate and append ellipsis.
651 + rowStr.append(cell.RenderTruncated(col.MaxLength, m_vtEnabled, m_colorEnabled));
652
653 if (col.SpaceAfter)
654 {
462 - m_stream << std::wstring(m_columnPadding, L' ');
655 + rowStr.append(m_columnPadding, L' ');
656 }
657 }
658 else
659 {
467 - m_stream << line[i];
660 + rowStr.append(cell.Render(m_vtEnabled, m_colorEnabled));
661
662 if (col.SpaceAfter)
663 {
471 - m_stream << std::wstring(col.MaxLength - valueLength + m_columnPadding, L' ');
664 + rowStr.append(col.MaxLength - valueLength + m_columnPadding, L' ');
665 }
666 }
667 }
475 - }
668
477 - const std::wstring rendered = m_stream.str();
478 - m_stream.str(L"");
479 - m_stream.clear();
480 -
481 - m_outputFn(rendered);
669 + m_reporter.Write(m_outputLevel, L"{}\n", rowStr);
670 + }
671 }
672 };
673
src/windows/wslc/tasks/ContainerTasks.cpp
+24 -18
@@ -569,18 +569,21 @@ void ListContainers(CLIExecutionContext& context)
569 }
570 case FormatType::Table:
571 {
572 - using Config = wsl::windows::wslc::ColumnWidthConfig;
572 bool trunc = !context.Args.Contains(ArgType::NoTrunc);
573 + using enum ColumnOverflow;
574
575 // Create table with or without column limits based on --no-trunc flag
576 auto table = trunc ? wsl::windows::wslc::TableOutput<6>(
577 - {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
578 - {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
579 - {Localization::WSLCCLI_TableHeaderImage(), {Config::NoLimit, 20, false}},
580 - {Localization::WSLCCLI_TableHeaderCreated(), {Config::NoLimit, Config::NoLimit, false}},
581 - {Localization::WSLCCLI_TableHeaderStatus(), {Config::NoLimit, Config::NoLimit, false}},
582 - {Localization::WSLCCLI_TableHeaderPorts(), {Config::NoLimit, Config::NoLimit, false}}}})
577 + context.Reporter,
578 + {{{Localization::WSLCCLI_TableHeaderContainerId(), {.MaxWidth = 12, .Overflow = Shrink}},
579 + {Localization::WSLCCLI_TableHeaderName(), {.MaxWidth = 20, .Overflow = Shrink}},
580 + {Localization::WSLCCLI_TableHeaderImage(), {.MaxWidth = 20, .Overflow = Shrink}},
581 + {Localization::WSLCCLI_TableHeaderCreated(), {.Overflow = Shrink}},
582 + {Localization::WSLCCLI_TableHeaderStatus(), {.Overflow = Shrink}},
583 + {Localization::WSLCCLI_TableHeaderPorts(), {.Overflow = Shrink}}}},
584 + containers.size())
585 : wsl::windows::wslc::TableOutput<6>(
586 + context.Reporter,
587 {Localization::WSLCCLI_TableHeaderContainerId(),
588 Localization::WSLCCLI_TableHeaderName(),
589 Localization::WSLCCLI_TableHeaderImage(),
@@ -591,7 +594,7 @@ void ListContainers(CLIExecutionContext& context)
594 // Add each container as a row
595 for (const auto& container : containers)
596 {
594 - table.OutputLine({
597 + table.WriteRow({
598 MultiByteToWide(trunc ? TruncateId(container.Id) : container.Id),
599 MultiByteToWide(container.Name),
600 MultiByteToWide(container.Image),
@@ -972,19 +975,22 @@ void ShowContainerStats(CLIExecutionContext& context)
975 }
976 case FormatType::Table:
977 {
975 - using Config = wsl::windows::wslc::ColumnWidthConfig;
978 bool trunc = !context.Args.Contains(ArgType::NoTrunc);
979 + using enum ColumnOverflow;
980
981 auto table = trunc ? wsl::windows::wslc::TableOutput<8>(
979 - {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
980 - {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
981 - {Localization::WSLCCLI_TableHeaderCpuPercent(), {Config::NoLimit, Config::NoLimit, false}},
982 - {Localization::WSLCCLI_TableHeaderMemUsageLimit(), {Config::NoLimit, Config::NoLimit, false}},
983 - {Localization::WSLCCLI_TableHeaderMemPercent(), {Config::NoLimit, Config::NoLimit, false}},
984 - {Localization::WSLCCLI_TableHeaderNetIo(), {Config::NoLimit, Config::NoLimit, false}},
985 - {Localization::WSLCCLI_TableHeaderBlockIo(), {Config::NoLimit, Config::NoLimit, false}},
986 - {Localization::WSLCCLI_TableHeaderPids(), {Config::NoLimit, Config::NoLimit, false}}}})
982 + context.Reporter,
983 + {{{Localization::WSLCCLI_TableHeaderContainerId(), {.MaxWidth = 12, .Overflow = Shrink}},
984 + {Localization::WSLCCLI_TableHeaderName(), {.MaxWidth = 20, .Overflow = Shrink}},
985 + {Localization::WSLCCLI_TableHeaderCpuPercent(), {.Overflow = Shrink}},
986 + {Localization::WSLCCLI_TableHeaderMemUsageLimit(), {.Overflow = Shrink}},
987 + {Localization::WSLCCLI_TableHeaderMemPercent(), {.Overflow = Shrink}},
988 + {Localization::WSLCCLI_TableHeaderNetIo(), {.Overflow = Shrink}},
989 + {Localization::WSLCCLI_TableHeaderBlockIo(), {.Overflow = Shrink}},
990 + {Localization::WSLCCLI_TableHeaderPids(), {.Overflow = Shrink}}}},
991 + statsJson.size())
992 : wsl::windows::wslc::TableOutput<8>(
993 + context.Reporter,
994 {Localization::WSLCCLI_TableHeaderContainerId(),
995 Localization::WSLCCLI_TableHeaderName(),
996 Localization::WSLCCLI_TableHeaderCpuPercent(),
@@ -997,7 +1003,7 @@ void ShowContainerStats(CLIExecutionContext& context)
1003 for (const auto& entry : statsJson)
1004 {
1005 const auto id = entry["ID"].get<std::string>();
1000 - table.OutputLine({
1006 + table.WriteRow({
1007 MultiByteToWide(trunc ? TruncateId(id) : id),
1008 MultiByteToWide(entry["Name"].get<std::string>()),
1009 MultiByteToWide(entry["CPUPerc"].get<std::string>()),
src/windows/wslc/tasks/ImageTasks.cpp
+14 -10
@@ -184,22 +184,26 @@ void ListImages(CLIExecutionContext& context)
184 }
185 case FormatType::Table:
186 {
187 - using Config = wsl::windows::wslc::ColumnWidthConfig;
187 bool trunc = !context.Args.Contains(ArgType::NoTrunc);
188 + using enum ColumnOverflow;
189
190 - // Create table — only IMAGE ID uses fixed width; other columns auto-size.
190 + // Create table — only IMAGE ID uses fixed width; other columns shrink to fit the console.
191 // When --no-trunc is passed, IMAGE ID also shows full length via TruncateId().
192 - auto table = trunc ? wsl::windows::wslc::TableOutput<5>(
193 - {{{L"REPOSITORY", {Config::NoLimit, Config::NoLimit, false}},
194 - {L"TAG", {Config::NoLimit, Config::NoLimit, false}},
195 - {L"IMAGE ID", {12, 12, false}},
196 - {L"CREATED", {Config::NoLimit, Config::NoLimit, false}},
197 - {L"SIZE", {Config::NoLimit, Config::NoLimit, false}}}})
198 - : wsl::windows::wslc::TableOutput<5>({L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
192 + auto table =
193 + trunc
194 + ? wsl::windows::wslc::TableOutput<5>(
195 + context.Reporter,
196 + {{{L"REPOSITORY", {.Overflow = Shrink}},
197 + {L"TAG", {.Overflow = Shrink}},
198 + {L"IMAGE ID", {.MinWidth = 12, .MaxWidth = 12, .Overflow = Shrink}},
199 + {L"CREATED", {.Overflow = Shrink}},
200 + {L"SIZE", {.Overflow = Shrink}}}},
201 + images.size())
202 + : wsl::windows::wslc::TableOutput<5>(context.Reporter, {L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
203
204 for (const auto& image : images)
205 {
202 - table.OutputLine({
206 + table.WriteRow({
207 MultiByteToWide(image.Repository.value_or("<untagged>")),
208 MultiByteToWide(image.Tag.value_or("<untagged>")),
209 MultiByteToWide(TruncateId(image.Id, trunc)),
src/windows/wslc/tasks/NetworkTasks.cpp
+2 -2
@@ -191,10 +191,10 @@ void ListNetworks(CLIExecutionContext& context)
191 }
192 case FormatType::Table:
193 {
194 - auto table = wsl::windows::wslc::TableOutput<3>({L"NETWORK ID", L"NAME", L"DRIVER"});
194 + auto table = wsl::windows::wslc::TableOutput<3>(context.Reporter, {L"NETWORK ID", L"NAME", L"DRIVER"});
195 for (const auto& network : networks)
196 {
197 - table.OutputLine({
197 + table.WriteRow({
198 MultiByteToWide(TruncateId(network.Id)),
199 MultiByteToWide(network.Name),
200 MultiByteToWide(network.Driver),
src/windows/wslc/tasks/SessionTasks.cpp
+2 -1
@@ -74,11 +74,12 @@ void ListSessions(CLIExecutionContext& context)
74 }
75
76 TableOutput<3> table(
77 + context.Reporter,
78 {Localization::MessageWslcHeaderId(), Localization::MessageWslcHeaderCreatorPid(), Localization::MessageWslcHeaderDisplayName()});
79
80 for (const auto& session : sessions)
81 {
81 - table.OutputLine({
82 + table.WriteRow({
83 std::to_wstring(session.SessionId),
84 std::to_wstring(session.CreatorPid),
85 session.DisplayName,
src/windows/wslc/tasks/VolumeTasks.cpp
+2 -2
@@ -183,10 +183,10 @@ void ListVolumes(CLIExecutionContext& context)
183 }
184 case FormatType::Table:
185 {
186 - auto table = wsl::windows::wslc::TableOutput<2>({L"DRIVER", L"VOLUME NAME"});
186 + auto table = wsl::windows::wslc::TableOutput<2>(context.Reporter, {L"DRIVER", L"VOLUME NAME"});
187 for (const auto& volume : volumes)
188 {
189 - table.OutputLine({
189 + table.WriteRow({
190 MultiByteToWide(volume.Driver),
191 MultiByteToWide(volume.Name),
192 });
test/windows/wslc/WSLCCLITableOutputUnitTests.cpp
+768 -147
@@ -17,18 +17,20 @@ Abstract:
17 #include "WSLCCLITestHelpers.h"
18
19 #include "TableOutput.h"
20 +#include "VTSupport.h"
21
22 using namespace wsl::windows::wslc;
23 +using namespace wsl::windows::common::vt;
24 using namespace WSLCTestHelpers;
25 using namespace WEX::Logging;
26 using namespace WEX::Common;
27 using namespace WEX::TestExecution;
28
27 -namespace WSLCTableOutputUnitTests {
29 +namespace WSLCCLITableOutputUnitTests {
30
29 -class WSLCTableOutputUnitTests
31 +class WSLCCLITableOutputUnitTests
32 {
31 - WSLC_TEST_CLASS(WSLCTableOutputUnitTests)
33 + WSLC_TEST_CLASS(WSLCCLITableOutputUnitTests)
34
35 TEST_CLASS_SETUP(TestClassSetup)
36 {
@@ -40,99 +42,82 @@ class WSLCTableOutputUnitTests
42 return true;
43 }
44
43 - // Test: header line is emitted as the first row, even with no data rows.
45 TEST_METHOD(TableOutput_AlwaysShowHeader_EmitsHeaderWhenEmpty)
46 {
47 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
48 cap.table.SetAlwaysShowHeader(true);
48 -
49 cap.table.Complete();
50
51 - VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
52 - // Header line must contain both column names
53 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
54 - VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
51 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
52 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
53 + VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
54 }
55
57 - // Test: no output at all when empty and AlwaysShowHeader is false.
56 TEST_METHOD(TableOutput_NoHeader_EmitsNothingWhenEmpty)
57 {
58 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
59 cap.table.SetAlwaysShowHeader(false);
62 -
60 cap.table.Complete();
61
65 - VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
62 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
63 }
64
68 - // Test: one data row produces header + one data line.
65 TEST_METHOD(TableOutput_SingleRow_EmitsHeaderPlusOneDataLine)
66 {
67 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
68
73 - cap.table.OutputLine({L"my-container", L"running"});
69 + cap.table.WriteRow({L"my-container", L"running"});
70 cap.table.Complete();
71
76 - // Expect: header row + 1 data row = 2 lines total
77 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
78 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
79 - VERIFY_IS_TRUE(cap.lines[1].find(L"my-container") != std::wstring::npos);
80 - VERIFY_IS_TRUE(cap.lines[1].find(L"running") != std::wstring::npos);
72 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
73 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
74 + VERIFY_IS_TRUE(cap.lines()[1].find(L"my-container") != std::wstring::npos);
75 + VERIFY_IS_TRUE(cap.lines()[1].find(L"running") != std::wstring::npos);
76 }
77
83 - // Test: multiple data rows all appear after the header.
78 TEST_METHOD(TableOutput_MultipleRows_AllRowsEmittedAfterHeader)
79 {
80 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
81
88 - cap.table.OutputLine({L"container-a", L"running"});
89 - cap.table.OutputLine({L"container-b", L"stopped"});
90 - cap.table.OutputLine({L"container-c", L"paused"});
82 + cap.table.WriteRow({L"container-a", L"running"});
83 + cap.table.WriteRow({L"container-b", L"stopped"});
84 + cap.table.WriteRow({L"container-c", L"paused"});
85 cap.table.Complete();
86
93 - VERIFY_ARE_EQUAL(static_cast<size_t>(4), cap.lines.size()); // header + 3 rows
94 -
95 - VERIFY_IS_TRUE(cap.lines[1].find(L"container-a") != std::wstring::npos);
96 - VERIFY_IS_TRUE(cap.lines[2].find(L"container-b") != std::wstring::npos);
97 - VERIFY_IS_TRUE(cap.lines[3].find(L"container-c") != std::wstring::npos);
87 + VERIFY_ARE_EQUAL(static_cast<size_t>(4), cap.lines().size());
88 + VERIFY_IS_TRUE(cap.lines()[1].find(L"container-a") != std::wstring::npos);
89 + VERIFY_IS_TRUE(cap.lines()[2].find(L"container-b") != std::wstring::npos);
90 + VERIFY_IS_TRUE(cap.lines()[3].find(L"container-c") != std::wstring::npos);
91 }
92
100 - // Test: columns are separated by the correct number of spaces.
93 TEST_METHOD(TableOutput_ColumnPadding_DefaultPaddingApplied)
94 {
103 - // Use a custom padding of 3 (the default) and verify the data row
104 - // contains at least 3 spaces between the first column value and the
105 - // start of the second column value.
95 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3);
96
108 - cap.table.OutputLine({L"abc", L"ok"});
97 + cap.table.WriteRow({L"abc", L"ok"});
98 cap.table.Complete();
99
111 - // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok"
112 - // Expected: "abc ok" with appropriate spacing
113 - const std::wstring& dataLine = cap.lines[1];
100 + auto lines = cap.lines();
101 + const auto& dataLine = lines[1];
102 VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos);
103 VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos);
104
117 - // There must be at least 3 spaces between the two values
105 auto columnPadding = 3;
106 auto namePos = dataLine.find(L"abc");
107 auto statusPos = dataLine.find(L"ok");
108 VERIFY_IS_TRUE(statusPos >= namePos + wcslen(L"abc") + columnPadding);
109 }
110
124 - // Test: custom column padding is respected.
111 TEST_METHOD(TableOutput_ColumnPadding_CustomPaddingApplied)
112 {
113 constexpr size_t customPadding = 5;
114 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"A", L"B"}, /*sizingBuffer=*/50, customPadding);
115
130 - cap.table.OutputLine({L"x", L"y"});
116 + cap.table.WriteRow({L"x", L"y"});
117 cap.table.Complete();
118
133 - // "A" header is 1 char wide, "x" value is 1 char wide.
134 - // With 5-space padding, "y" must start at position >= 1 + 5 = 6.
135 - const std::wstring& dataLine = cap.lines[1];
119 + auto lines = cap.lines();
120 + const auto& dataLine = lines[1];
121 auto posX = dataLine.find(L'x');
122 auto posY = dataLine.find(L'y');
123 VERIFY_IS_TRUE(posX != std::wstring::npos);
@@ -140,67 +125,48 @@ class WSLCTableOutputUnitTests
125 VERIFY_IS_TRUE(posY >= posX + 1 + customPadding);
126 }
127
143 - // Test: column width expands to fit the widest data value.
128 TEST_METHOD(TableOutput_ColumnWidth_ExpandsToFitData)
129 {
130 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"NAME"});
131
148 - cap.table.OutputLine({L"1", L"short"});
149 - cap.table.OutputLine({L"2", L"a-very-long-container-name"});
132 + cap.table.WriteRow({L"1", L"short"});
133 + cap.table.WriteRow({L"2", L"a-very-long-container-name"});
134 cap.table.Complete();
135
152 - // The second column must accommodate the widest value in every row.
153 - for (size_t i = 1; i < cap.lines.size(); ++i)
154 - {
155 - // The long value must not have been truncated.
156 - if (cap.lines[i].find(L"a-very-long-container-name") != std::wstring::npos)
157 - {
158 - LogComment(L"Long value found intact in row " + std::to_wstring(i));
159 - }
160 - }
161 - VERIFY_IS_TRUE(cap.lines[2].find(L"a-very-long-container-name") != std::wstring::npos);
136 + VERIFY_IS_TRUE(cap.lines()[2].find(L"a-very-long-container-name") != std::wstring::npos);
137 }
138
164 - // Test: column width is at least as wide as the header.
139 TEST_METHOD(TableOutput_ColumnWidth_AtLeastHeaderWidth)
140 {
167 - // Header "CONTAINER_NAME" is 14 chars; data value is only 3 chars.
168 - // The data line must still be padded to the header width.
141 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"CONTAINER_NAME", L"ST"});
142
171 - cap.table.OutputLine({L"abc", L"ok"});
143 + cap.table.WriteRow({L"abc", L"ok"});
144 cap.table.Complete();
145
174 - // Header line: "CONTAINER_NAME" starts at position 0.
175 - // Data line: "abc" starts at position 0, "ok" must not start before
176 - // position 14 + padding.
177 - const std::wstring& dataLine = cap.lines[1];
146 + auto lines = cap.lines();
147 + const auto& dataLine = lines[1];
148 auto posOk = dataLine.find(L"ok");
149 VERIFY_IS_TRUE(posOk != std::wstring::npos);
180 - // "CONTAINER_NAME" = 14 chars, padding = 3 -> "ok" must be at >= 17
150 VERIFY_IS_TRUE(posOk >= static_cast<size_t>(14 + TableOutput<2>::DefaultColumnPadding));
151 }
152
184 - // Test: values exceeding MaxWidth are truncated and an ellipsis appended.
153 TEST_METHOD(TableOutput_MaxWidth_LongValueIsTruncatedWithEllipsis)
154 {
155 TableOutput<2>::column_config_t configs{};
188 - configs[0].MaxWidth = 8; // limit first column to 8 chars
156 + configs[0].MaxWidth = 8;
157 configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
158
159 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
160
193 - cap.table.OutputLine({L"a-very-long-name", L"running"});
161 + cap.table.WriteRow({L"a-very-long-name", L"running"});
162 cap.table.Complete();
163
196 - const std::wstring& dataLine = cap.lines[1];
197 - // Ellipsis character (U+2026) must be present
164 + auto lines = cap.lines();
165 + const auto& dataLine = lines[1];
166 VERIFY_IS_TRUE(dataLine.find(L"\x2026") != std::wstring::npos);
199 - // Full original value must NOT be present
167 VERIFY_IS_TRUE(dataLine.find(L"a-very-long-name") == std::wstring::npos);
168 }
169
203 - // Test: values within MaxWidth are not truncated.
170 TEST_METHOD(TableOutput_MaxWidth_ShortValueNotTruncated)
171 {
172 TableOutput<2>::column_config_t configs{};
@@ -209,189 +175,844 @@ class WSLCTableOutputUnitTests
175
176 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
177
212 - cap.table.OutputLine({L"short", L"running"});
178 + cap.table.WriteRow({L"short", L"running"});
179 cap.table.Complete();
180
215 - const std::wstring& dataLine = cap.lines[1];
181 + auto lines = cap.lines();
182 + const auto& dataLine = lines[1];
183 VERIFY_IS_TRUE(dataLine.find(L"short") != std::wstring::npos);
184 VERIFY_IS_TRUE(dataLine.find(L"\x2026") == std::wstring::npos);
185 }
186
220 - // Test: columns shrink when total width exceeds console width.
187 TEST_METHOD(TableOutput_ConsoleWidthLimit_PreferredShrinkColumnIsShrunk)
188 {
223 - // Two columns, first marked preferredShrink=false, second preferredShrink=true.
224 - // With a very narrow console the second column should absorb the cut.
189 TableOutput<2>::column_config_t configs{};
190 configs[0].MaxWidth = ColumnWidthConfig::NoLimit;
191 + configs[0].Overflow = ColumnOverflow::Shrink;
192 configs[0].PreferredShrink = false;
193 configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
194 + configs[1].Overflow = ColumnOverflow::Shrink;
195 configs[1].PreferredShrink = true;
196
197 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"DESCRIPTION"}, std::move(configs));
232 - // Override with a very narrow console: only 20 chars wide.
198 cap.table.SetConsoleWidthOverride(20);
234 - cap.table.SetColumnWidthLimiting(true);
199
236 - cap.table.OutputLine({L"abc123", L"this-is-a-long-description-value"});
200 + cap.table.WriteRow({L"abc123", L"this-is-a-long-description-value"});
201 cap.table.Complete();
202
239 - // The output must fit within 20 chars.
240 - for (const auto& line : cap.lines)
203 + auto lines = cap.lines();
204 + for (const auto& line : lines)
205 {
206 VERIFY_IS_TRUE(line.size() <= static_cast<size_t>(20));
207 }
208 }
209
246 - // Test: IsEmpty returns true before any rows are added, and false after a row is added.
210 TEST_METHOD(TableOutput_IsEmpty)
211 {
212 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
213 VERIFY_IS_TRUE(cap.table.IsEmpty());
214
252 - cap.table.OutputLine({L"foo", L"bar"});
215 + cap.table.WriteRow({L"foo", L"bar"});
216 VERIFY_IS_FALSE(cap.table.IsEmpty());
217 }
218
256 - // Test: column-definition constructor wires up names and configs correctly.
219 TEST_METHOD(TableOutput_ColumnDefinition_NameAndConfigUsed)
220 {
221 TableOutput<2>::column_def_t defs{{
260 - ColumnDefinition{L"MYID", {ColumnWidthConfig::NoLimit, 6, false}},
261 - ColumnDefinition{L"MYNAME", {ColumnWidthConfig::NoLimit, ColumnWidthConfig::NoLimit, true}},
222 + ColumnDefinition{L"MYID", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = 6, .Overflow = ColumnOverflow::Shrink, .PreferredShrink = false}},
223 + ColumnDefinition{L"MYNAME", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = ColumnWidthConfig::NoLimit}},
224 }};
225
226 TableOutputCapture<2> cap(std::move(defs));
227
266 - cap.table.OutputLine({L"id-value", L"name-value"});
228 + cap.table.WriteRow({L"id-value", L"name-value"});
229 cap.table.Complete();
230
269 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
270 - VERIFY_IS_TRUE(cap.lines[0].find(L"MYID") != std::wstring::npos);
271 - VERIFY_IS_TRUE(cap.lines[0].find(L"MYNAME") != std::wstring::npos);
272 -
273 - // "id-value" is 8 chars but MaxWidth=6 -> must be truncated
274 - VERIFY_IS_TRUE(cap.lines[1].find(L"\x2026") != std::wstring::npos);
231 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
232 + VERIFY_IS_TRUE(cap.lines()[0].find(L"MYID") != std::wstring::npos);
233 + VERIFY_IS_TRUE(cap.lines()[0].find(L"MYNAME") != std::wstring::npos);
234 + VERIFY_IS_TRUE(cap.lines()[1].find(L"\x2026") != std::wstring::npos);
235 }
236
277 - // Test: SetShowHeader(false) suppresses header when there are data rows.
237 TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderWithDataRows)
238 {
239 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
240 cap.table.SetShowHeader(false);
241
283 - cap.table.OutputLine({L"my-container", L"running"});
242 + cap.table.WriteRow({L"my-container", L"running"});
243 cap.table.Complete();
244
286 - // Only the data row should be emitted.
287 - VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
288 - VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
289 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
245 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
246 + VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
247 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
248 }
249
292 - // Test: SetShowHeader(false) with AlwaysShowHeader(true) still suppresses header when empty.
250 TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderEvenWhenAlwaysShowHeaderTrue)
251 {
252 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
253 cap.table.SetAlwaysShowHeader(true);
254 cap.table.SetShowHeader(false);
298 -
255 cap.table.Complete();
256
301 - // SetShowHeader(false) takes precedence. Nothing should be emitted.
302 - VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
257 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
258 }
259
305 - // Test: SetShowHeader(true) is the default. Header appears before data rows.
260 TEST_METHOD(TableOutput_ShowHeader_True_IsDefaultAndEmitsHeader)
261 {
262 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
309 - // No explicit call to SetShowHeader. Default must be true.
263
311 - cap.table.OutputLine({L"my-container", L"running"});
264 + cap.table.WriteRow({L"my-container", L"running"});
265 cap.table.Complete();
266
314 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
315 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
316 - VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
267 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
268 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
269 + VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
270 }
271
319 - // Test: SetShowHeader(false) with multiple data rows emits only data rows.
272 TEST_METHOD(TableOutput_ShowHeader_False_MultipleDataRowsNoHeader)
273 {
274 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
275 cap.table.SetShowHeader(false);
276
325 - cap.table.OutputLine({L"container-a", L"running"});
326 - cap.table.OutputLine({L"container-b", L"stopped"});
277 + cap.table.WriteRow({L"container-a", L"running"});
278 + cap.table.WriteRow({L"container-b", L"stopped"});
279 cap.table.Complete();
280
329 - // Two data rows, zero header rows.
330 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
331 - VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
332 - VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
333 - // Neither line should contain the column header text.
334 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
335 - VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
281 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
282 + VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
283 + VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
284 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
285 + VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
286 }
287
338 - // Test: SetShowHeader controls whether the header row is emitted.
339 - // Covers: default (true), suppression with data rows, suppression when empty
340 - // (even with AlwaysShowHeader), and multiple data rows with no header.
288 TEST_METHOD(TableOutput_ShowHeader)
289 {
343 - // Default is true. Header appears before data rows without an explicit call.
290 {
291 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
292
347 - cap.table.OutputLine({L"my-container", L"running"});
293 + cap.table.WriteRow({L"my-container", L"running"});
294 cap.table.Complete();
295
350 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
351 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
352 - VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
296 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
297 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
298 + VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
299 }
354 -
355 - // SetShowHeader(false) suppresses the header when data rows are present.
300 {
301 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
302 cap.table.SetShowHeader(false);
303
360 - cap.table.OutputLine({L"my-container", L"running"});
304 + cap.table.WriteRow({L"my-container", L"running"});
305 cap.table.Complete();
306
363 - VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines.size());
364 - VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
365 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
307 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
308 + VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
309 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
310 }
367 -
368 - // SetShowHeader(false) suppresses the header even when AlwaysShowHeader is true and the table is empty.
311 {
312 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
313 cap.table.SetAlwaysShowHeader(true);
314 cap.table.SetShowHeader(false);
373 -
315 cap.table.Complete();
316
376 - VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines.size());
317 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
318 }
378 -
379 - // SetShowHeader(false) with multiple data rows emits only data rows.
319 {
320 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
321 cap.table.SetShowHeader(false);
322
384 - cap.table.OutputLine({L"container-a", L"running"});
385 - cap.table.OutputLine({L"container-b", L"stopped"});
323 + cap.table.WriteRow({L"container-a", L"running"});
324 + cap.table.WriteRow({L"container-b", L"stopped"});
325 cap.table.Complete();
326
388 - VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines.size());
389 - VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
390 - VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
391 - VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
392 - VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
327 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
328 + VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
329 + VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
330 + VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
331 + VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
332 }
333 }
334 +
335 + TEST_METHOD(TableOutput_RowIndent_PrependedToEveryRow)
336 + {
337 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
338 + cap.table.SetRowIndent(2);
339 +
340 + cap.table.WriteRow({L"abc", L"ok"});
341 + cap.table.Complete();
342 +
343 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
344 + VERIFY_ARE_EQUAL(std::wstring{L" NAME STATUS"}, cap.lines()[0]);
345 + VERIFY_ARE_EQUAL(std::wstring{L" abc ok"}, cap.lines()[1]);
346 + }
347 +
348 + TEST_METHOD(TableOutput_WordWrap_ShortValueProducesOneRow)
349 + {
350 + TableOutput<2>::column_config_t configs{};
351 + configs[0].MaxWidth = 10;
352 + configs[1].MaxWidth = 20;
353 + configs[1].Overflow = ColumnOverflow::Wrap;
354 +
355 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
356 + cap.table.SetShowHeader(false);
357 +
358 + cap.table.WriteRow({L"opt", L"short desc"});
359 + cap.table.Complete();
360 +
361 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
362 + VERIFY_ARE_EQUAL(std::wstring{L"opt short desc"}, cap.lines()[0]);
363 + }
364 +
365 + TEST_METHOD(TableOutput_WordWrap_WrapsAtWordBoundary)
366 + {
367 + TableOutput<2>::column_config_t configs{};
368 + configs[0].MaxWidth = 6;
369 + configs[1].MaxWidth = 10;
370 + configs[1].Overflow = ColumnOverflow::Wrap;
371 +
372 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
373 + cap.table.SetShowHeader(false);
374 +
375 + cap.table.WriteRow({L"opt", L"hello world"});
376 + cap.table.Complete();
377 +
378 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
379 + VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
380 + VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
381 + }
382 +
383 + TEST_METHOD(TableOutput_WordWrap_ContinuationRowHasBlankLeadingColumns)
384 + {
385 + TableOutput<2>::column_config_t configs{};
386 + configs[0].MaxWidth = 6;
387 + configs[1].MaxWidth = 10;
388 + configs[1].Overflow = ColumnOverflow::Wrap;
389 +
390 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
391 + cap.table.SetShowHeader(false);
392 +
393 + cap.table.WriteRow({L"opt", L"hello world"});
394 + cap.table.Complete();
395 +
396 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
397 + VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
398 + }
399 +
400 + TEST_METHOD(TableOutput_WordWrap_MultipleWrapsProduceMultipleRows)
401 + {
402 + TableOutput<2>::column_config_t configs{};
403 + configs[0].MaxWidth = 4;
404 + configs[1].MaxWidth = 10;
405 + configs[1].Overflow = ColumnOverflow::Wrap;
406 +
407 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
408 + cap.table.SetShowHeader(false);
409 +
410 + cap.table.WriteRow({L"opt", L"one two three four"});
411 + cap.table.Complete();
412 +
413 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
414 + VERIFY_ARE_EQUAL(std::wstring{L"opt one two"}, cap.lines()[0]);
415 + VERIFY_ARE_EQUAL(std::wstring{L" three four"}, cap.lines()[1]);
416 + }
417 +
418 + TEST_METHOD(TableOutput_WordWrap_HardBreakWhenNoSpaceFound)
419 + {
420 + TableOutput<2>::column_config_t configs{};
421 + configs[0].MaxWidth = 4;
422 + configs[1].MaxWidth = 6;
423 + configs[1].Overflow = ColumnOverflow::Wrap;
424 +
425 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
426 + cap.table.SetShowHeader(false);
427 +
428 + cap.table.WriteRow({L"opt", L"abcdefghij"});
429 + cap.table.Complete();
430 +
431 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
432 + VERIFY_ARE_EQUAL(std::wstring{L"opt abcdef"}, cap.lines()[0]);
433 + VERIFY_ARE_EQUAL(std::wstring{L" ghij"}, cap.lines()[1]);
434 + }
435 +
436 + TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnStillTruncates)
437 + {
438 + TableOutput<2>::column_config_t configs{};
439 + configs[0].MaxWidth = 5;
440 + // ColumnOverflow::Truncate (default) — truncates
441 + configs[1].MaxWidth = 20;
442 + configs[1].Overflow = ColumnOverflow::Wrap;
443 +
444 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
445 + cap.table.SetShowHeader(false);
446 +
447 + cap.table.WriteRow({L"toolongname", L"short desc"});
448 + cap.table.Complete();
449 +
450 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
451 + VERIFY_ARE_EQUAL(std::wstring{L"tool\u2026 short desc"}, cap.lines()[0]);
452 + }
453 +
454 + TEST_METHOD(TableOutput_WordWrap_RowIndentAppliedToAllPhysicalRows)
455 + {
456 + TableOutput<2>::column_config_t configs{};
457 + configs[0].MaxWidth = 4;
458 + configs[1].MaxWidth = 8;
459 + configs[1].Overflow = ColumnOverflow::Wrap;
460 +
461 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
462 + cap.table.SetShowHeader(false);
463 + cap.table.SetRowIndent(2);
464 +
465 + cap.table.WriteRow({L"opt", L"hello world"});
466 + cap.table.Complete();
467 +
468 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
469 + VERIFY_ARE_EQUAL(std::wstring{L" opt hello"}, cap.lines()[0]);
470 + VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
471 + }
472 +
473 + TEST_METHOD(TableOutput_WordWrap_DisabledByDefault_LongTextTruncated)
474 + {
475 + TableOutput<2>::column_config_t configs{};
476 + configs[0].MaxWidth = 4;
477 + configs[1].MaxWidth = 8;
478 + // ColumnOverflow::Truncate (default) — truncates
479 +
480 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
481 + cap.table.SetShowHeader(false);
482 +
483 + cap.table.WriteRow({L"opt", L"a very long description"});
484 + cap.table.Complete();
485 +
486 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
487 + VERIFY_ARE_EQUAL(std::wstring{L"opt a very \u2026"}, cap.lines()[0]);
488 + }
489 +
490 + TEST_METHOD(TableOutput_WordWrap_MultipleLogicalRowsEachWrapIndependently)
491 + {
492 + TableOutput<2>::column_config_t configs{};
493 + configs[0].MaxWidth = 6;
494 + configs[1].MaxWidth = 10;
495 + configs[1].Overflow = ColumnOverflow::Wrap;
496 +
497 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
498 + cap.table.SetShowHeader(false);
499 +
500 + cap.table.WriteRow({L"opt-a", L"hello world"});
501 + cap.table.WriteRow({L"opt-b", L"short"});
502 + cap.table.Complete();
503 +
504 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
505 + VERIFY_ARE_EQUAL(std::wstring{L"opt-a hello"}, cap.lines()[0]);
506 + VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
507 + VERIFY_ARE_EQUAL(std::wstring{L"opt-b short"}, cap.lines()[2]);
508 + }
509 +
510 + TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColBLonger)
511 + {
512 + TableOutput<2>::column_config_t configs{};
513 + configs[0].MaxWidth = 5;
514 + configs[0].Overflow = ColumnOverflow::Wrap;
515 + configs[1].MaxWidth = 5;
516 + configs[1].Overflow = ColumnOverflow::Wrap;
517 +
518 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
519 + cap.table.SetShowHeader(false);
520 +
521 + cap.table.WriteRow({L"ab cd ef", L"one two three"});
522 + cap.table.Complete();
523 +
524 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
525 + VERIFY_ARE_EQUAL(std::wstring{L"ab cd one"}, cap.lines()[0]);
526 + VERIFY_ARE_EQUAL(std::wstring{L"ef two"}, cap.lines()[1]);
527 + VERIFY_ARE_EQUAL(std::wstring{L" three"}, cap.lines()[2]);
528 + }
529 +
530 + TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColALonger)
531 + {
532 + TableOutput<2>::column_config_t configs{};
533 + configs[0].MaxWidth = 5;
534 + configs[0].Overflow = ColumnOverflow::Wrap;
535 + configs[1].MaxWidth = 5;
536 + configs[1].Overflow = ColumnOverflow::Wrap;
537 +
538 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
539 + cap.table.SetShowHeader(false);
540 +
541 + cap.table.WriteRow({L"one two three", L"ab cd ef"});
542 + cap.table.Complete();
543 +
544 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
545 + VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
546 + VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
547 + VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
548 + }
549 +
550 + TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsWithEqualLengths)
551 + {
552 + TableOutput<2>::column_config_t configs{};
553 + configs[0].MaxWidth = 4;
554 + configs[0].Overflow = ColumnOverflow::Wrap;
555 + configs[1].MaxWidth = 4;
556 + configs[1].Overflow = ColumnOverflow::Wrap;
557 +
558 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
559 + cap.table.SetShowHeader(false);
560 +
561 + cap.table.WriteRow({L"aa bb", L"xx yy"});
562 + cap.table.Complete();
563 +
564 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
565 + VERIFY_ARE_EQUAL(std::wstring{L"aa xx"}, cap.lines()[0]);
566 + VERIFY_ARE_EQUAL(std::wstring{L"bb yy"}, cap.lines()[1]);
567 + }
568 +
569 + TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnBetweenTwoWrappingColumns)
570 + {
571 + TableOutput<3>::column_config_t configs{};
572 + configs[0].MaxWidth = 4;
573 + // configs[0].Overflow = ColumnOverflow::Truncate (default) — truncates
574 + configs[1].MaxWidth = 5;
575 + configs[1].Overflow = ColumnOverflow::Wrap;
576 + configs[2].MaxWidth = 5;
577 + configs[2].Overflow = ColumnOverflow::Wrap;
578 +
579 + TableOutputCapture<3> cap(TableOutput<3>::header_t{L"", L"", L""}, std::move(configs));
580 + cap.table.SetShowHeader(false);
581 +
582 + cap.table.WriteRow({L"tag", L"aa bb", L"xx yy zz"});
583 + cap.table.Complete();
584 +
585 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
586 + VERIFY_ARE_EQUAL(std::wstring{L"tag aa bb xx yy"}, cap.lines()[0]);
587 + VERIFY_ARE_EQUAL(std::wstring{L" zz"}, cap.lines()[1]);
588 + }
589 +
590 + TEST_METHOD(TableOutput_WordWrap_FirstColumnLongerThanSecond)
591 + {
592 + TableOutput<2>::column_config_t configs{};
593 + configs[0].MaxWidth = 5;
594 + configs[0].Overflow = ColumnOverflow::Wrap;
595 + configs[1].MaxWidth = 5;
596 + configs[1].Overflow = ColumnOverflow::Wrap;
597 +
598 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
599 + cap.table.SetShowHeader(false);
600 +
601 + cap.table.WriteRow({L"one two three", L"ab cd ef"});
602 + cap.table.Complete();
603 +
604 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
605 + VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
606 + VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
607 + VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
608 + }
609 +
610 + TEST_METHOD(TableOutput_SetColumnConfig_WordWrapAfterConstruction)
611 + {
612 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
613 + cap.table.SetShowHeader(false);
614 + cap.table.SetColumnConfig(
615 + 1,
616 + ColumnWidthConfig{
617 + .MaxWidth = 8,
618 + .Overflow = ColumnOverflow::Wrap,
619 + });
620 +
621 + cap.table.WriteRow({L"opt", L"hello world"});
622 + cap.table.Complete();
623 +
624 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
625 + VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
626 + VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
627 + }
628 +
629 + TEST_METHOD(FormattedCell_VisibleWidth_PlainText)
630 + {
631 + FormattedCell cell{L"hello"};
632 + VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
633 + }
634 +
635 + TEST_METHOD(FormattedCell_VisibleWidth_ExcludesPlaceholders)
636 + {
637 + FormattedCell cell{L"{}hello{}", {&Format::Fg::BrightRed, &Format::Default}};
638 + VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
639 + }
640 +
641 + TEST_METHOD(FormattedCell_Render_ColorEnabled)
642 + {
643 + FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
644 + const auto result = cell.Render(true, true);
645 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
646 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
647 + }
648 +
649 + TEST_METHOD(FormattedCell_Render_ColorDisabled)
650 + {
651 + FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
652 + const auto result = cell.Render(false, false);
653 + VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
654 + }
655 +
656 + TEST_METHOD(FormattedCell_Render_VTEnabled_ColorDisabled_StripsColorSequences)
657 + {
658 + FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
659 + // VT on but color off: color sequences should be stripped
660 + const auto result = cell.Render(true, false);
661 + VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
662 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
663 + }
664 +
665 + TEST_METHOD(FormattedCell_RenderTruncated_TruncatesVisibleText)
666 + {
667 + FormattedCell cell = FormattedCell(L"hello world", Format::Fg::BrightRed);
668 + const auto result = cell.RenderTruncated(5, true, true);
669 + // Should contain truncated visible text + ellipsis + sequences
670 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
671 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
672 + }
673 +
674 + TEST_METHOD(TableOutput_FormattedCell_ColumnWidthBasedOnVisibleChars)
675 + {
676 + using namespace wsl::windows::common::vt;
677 +
678 + TableOutput<2>::column_config_t configs{};
679 + configs[0].MaxWidth = 10;
680 + configs[1].MaxWidth = 20;
681 +
682 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
683 + cap.table.SetShowHeader(false);
684 +
685 + cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
686 + cap.table.WriteRow({L"end", FormattedCell{L"world"}});
687 + cap.table.Complete();
688 +
689 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
690 + // First cell is emphasized (has ESC), second is plain
691 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
692 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
693 + VERIFY_ARE_EQUAL(std::wstring{L"end world"}, cap.lines()[1]);
694 + }
695 +
696 + TEST_METHOD(TableOutput_FormattedCell_WrapsWithSequencesOnEachChunk)
697 + {
698 + using namespace wsl::windows::common::vt;
699 +
700 + TableOutput<2>::column_config_t configs{};
701 + configs[0].MaxWidth = 6;
702 + configs[1].MaxWidth = 8;
703 + configs[1].Overflow = ColumnOverflow::Wrap;
704 +
705 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
706 + cap.table.SetShowHeader(false);
707 +
708 + cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
709 + cap.table.Complete();
710 +
711 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
712 + // Both lines should have emphasis sequences
713 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
714 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
715 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
716 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
717 + }
718 +
719 + TEST_METHOD(FormattedCell_DefaultCtor_EmptyCell)
720 + {
721 + FormattedCell cell;
722 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
723 + VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(true, true));
724 + VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(false, false));
725 + }
726 +
727 + TEST_METHOD(FormattedCell_SingleSequenceCtor_BuildsFormatWithReset)
728 + {
729 + FormattedCell cell(L"bold", Format::Bright);
730 + VERIFY_ARE_EQUAL(static_cast<size_t>(4), cell.VisibleWidth());
731 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cell.sequences.size());
732 +
733 + // Color enabled: sequences emitted
734 + const auto rendered = cell.Render(true, true);
735 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L"bold"));
736 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L'\x1b'));
737 +
738 + // Color disabled: plain text only
739 + VERIFY_ARE_EQUAL(std::wstring{L"bold"}, cell.Render(false, false));
740 + }
741 +
742 + TEST_METHOD(FormattedCell_VisibleWidth_MultiplePlaceholders)
743 + {
744 + // "{}a{}bc{}" = 3 visible chars (a, b, c), 3 placeholders
745 + FormattedCell cell{L"{}a{}bc{}", {&Format::Bright, &Format::Fg::BrightRed, &Format::Default}};
746 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), cell.VisibleWidth());
747 + }
748 +
749 + TEST_METHOD(FormattedCell_VisibleWidth_EmptyFormat)
750 + {
751 + FormattedCell cell{L""};
752 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
753 + }
754 +
755 + TEST_METHOD(FormattedCell_VisibleWidth_OnlyPlaceholders)
756 + {
757 + FormattedCell cell{L"{}{}", {&Format::Bright, &Format::Default}};
758 + VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
759 + }
760 +
761 + TEST_METHOD(FormattedCell_Render_PlainTextNoSequences)
762 + {
763 + FormattedCell cell{L"plain text"};
764 + VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(true, true));
765 + VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(false, false));
766 + }
767 +
768 + TEST_METHOD(FormattedCell_RenderTruncated_TextFitsNoTruncation)
769 + {
770 + FormattedCell cell(L"hello", Format::Fg::BrightRed);
771 + const auto result = cell.RenderTruncated(10, true, true);
772 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
773 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\u2026')); // no ellipsis
774 + }
775 +
776 + TEST_METHOD(FormattedCell_RenderTruncated_ColorDisabled_StillTruncates)
777 + {
778 + FormattedCell cell(L"hello world", Format::Fg::BrightRed);
779 + const auto result = cell.RenderTruncated(5, false, false);
780 + // Sequences stripped, but truncation still occurs
781 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
782 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
783 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
784 + }
785 +
786 + TEST_METHOD(FormattedCell_RenderTruncated_PlainText)
787 + {
788 + FormattedCell cell{L"abcdefghij"};
789 + const auto result = cell.RenderTruncated(5, false, false);
790 + VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"fghij"));
791 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
792 + }
793 +
794 + TEST_METHOD(TableOutput_FormattedCell_ColorDisabled_SequencesStripped)
795 + {
796 + using namespace wsl::windows::common::vt;
797 +
798 + TableOutput<2>::column_config_t configs{};
799 + configs[0].MaxWidth = 10;
800 + configs[1].MaxWidth = 20;
801 +
802 + // vtEnabled=false: sequences should be stripped from output
803 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
804 + cap.table.SetShowHeader(false);
805 +
806 + cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
807 + cap.table.Complete();
808 +
809 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
810 + // No ESC bytes in output
811 + VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
812 + // Visible text still present with correct padding
813 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
814 + }
815 +
816 + TEST_METHOD(TableOutput_FormattedCell_WrapColorDisabled_SequencesStripped)
817 + {
818 + using namespace wsl::windows::common::vt;
819 +
820 + TableOutput<2>::column_config_t configs{};
821 + configs[0].MaxWidth = 6;
822 + configs[1].MaxWidth = 8;
823 + configs[1].Overflow = ColumnOverflow::Wrap;
824 +
825 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
826 + cap.table.SetShowHeader(false);
827 +
828 + cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
829 + cap.table.Complete();
830 +
831 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
832 + // No ESC in either line
833 + VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
834 + VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
835 + // Text still wraps correctly
836 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
837 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
838 + }
839 +
840 + TEST_METHOD(TableOutput_FormattedCell_WrapMultipleLines_SequenceReapplied)
841 + {
842 + using namespace wsl::windows::common::vt;
843 +
844 + TableOutput<2>::column_config_t configs{};
845 + configs[0].MaxWidth = 6;
846 + configs[1].MaxWidth = 5;
847 + configs[1].Overflow = ColumnOverflow::Wrap;
848 +
849 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
850 + cap.table.SetShowHeader(false);
851 +
852 + // Text that wraps into 3 lines: "aa bb cc" at width 5 -> "aa bb" / "cc"
853 + // Actually at width 5: "aa" "bb" "cc" (word-break at spaces)
854 + cap.table.WriteRow({L"cmd", FormattedCell(L"aa bb cc dd", Format::Fg::BrightCyan)});
855 + cap.table.Complete();
856 +
857 + // Should produce multiple wrap lines, each with sequences
858 + auto lines = cap.lines();
859 + VERIFY_IS_GREATER_THAN(lines.size(), static_cast<size_t>(1));
860 + for (const auto& line : lines)
861 + {
862 + // Every line that has content should have the escape sequence reapplied
863 + if (!line.empty())
864 + {
865 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\x1b'));
866 + }
867 + }
868 + }
869 +
870 + TEST_METHOD(TableOutput_FormattedCell_HyperlinkTruncated_BothSequencesEmitted)
871 + {
872 + using namespace wsl::windows::common::vt;
873 +
874 + // Simulate a hyperlink cell: OSC 8 open + visible text + OSC 8 close.
875 + // The open/close are ConstructedSequences since they contain a URL.
876 + const ConstructedSequence hyperlinkOpen{L"\x1b]8;;https://example.com\x1b\\"};
877 + const ConstructedSequence hyperlinkClose{L"\x1b]8;;\x1b\\"};
878 +
879 + TableOutput<2>::column_config_t configs{};
880 + configs[0].MaxWidth = 6;
881 + configs[1].MaxWidth = 8; // Force truncation of "click here now" (14 chars)
882 +
883 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
884 + cap.table.SetShowHeader(false);
885 +
886 + // FormattedCell: {}visible text{} with hyperlink open/close as sequences
887 + cap.table.WriteRow({L"link", FormattedCell(L"{}click here now{}", {&hyperlinkOpen, &hyperlinkClose})});
888 + cap.table.Complete();
889 +
890 + auto lines = cap.lines();
891 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), lines.size());
892 + const auto& line = lines[0];
893 +
894 + // The hyperlink open sequence must be present (starts the clickable region)
895 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L"\x1b]8;;https://example.com\x1b\\"));
896 + // The hyperlink close sequence must also be present (terminates the clickable region)
897 + // Find the close AFTER the open
898 + auto openEnd = line.find(L"\x1b]8;;https://example.com\x1b\\");
899 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, openEnd);
900 + auto closePos = line.find(L"\x1b]8;;\x1b\\", openEnd + 1);
901 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, closePos);
902 + // Truncation occurred — full text should NOT be present
903 + VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"click here now"));
904 + // Ellipsis should be present
905 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\u2026'));
906 + }
907 +
908 + TEST_METHOD(TableOutput_WriteLine_BlankLineEmittedBetweenRows)
909 + {
910 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
911 + cap.table.SetShowHeader(false);
912 +
913 + cap.table.WriteRow({L"row-a", L"val-a"});
914 + cap.table.WriteLine();
915 + cap.table.WriteRow({L"row-b", L"val-b"});
916 + cap.table.Complete();
917 +
918 + auto lines = cap.lines();
919 + // 3 lines: data, blank, data
920 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
921 + VERIFY_IS_TRUE(lines[0].find(L"row-a") != std::wstring::npos);
922 + VERIFY_IS_TRUE(lines[1].empty());
923 + VERIFY_IS_TRUE(lines[2].find(L"row-b") != std::wstring::npos);
924 + }
925 +
926 + TEST_METHOD(TableOutput_WriteLine_SectionHeaderEmittedBetweenRows)
927 + {
928 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
929 + cap.table.SetShowHeader(false);
930 +
931 + cap.table.WriteRow({L"opt-a", L"desc-a"});
932 + cap.table.WriteLine(FormattedCell{L"Global Options:"});
933 + cap.table.WriteRow({L"opt-b", L"desc-b"});
934 + cap.table.Complete();
935 +
936 + auto lines = cap.lines();
937 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
938 + VERIFY_IS_TRUE(lines[0].find(L"opt-a") != std::wstring::npos);
939 + VERIFY_ARE_EQUAL(std::wstring{L"Global Options:"}, lines[1]);
940 + VERIFY_IS_TRUE(lines[2].find(L"opt-b") != std::wstring::npos);
941 + }
942 +
943 + TEST_METHOD(TableOutput_WriteLine_DoesNotAffectColumnWidths)
944 + {
945 + // The break text is longer than any data cell; column widths should
946 + // be driven only by data rows, not breaks.
947 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
948 + cap.table.SetShowHeader(false);
949 +
950 + cap.table.WriteRow({L"ab", L"cd"});
951 + cap.table.WriteLine(FormattedCell{L"This is a very long section header that should not widen columns"});
952 + cap.table.WriteRow({L"ef", L"gh"});
953 + cap.table.Complete();
954 +
955 + auto lines = cap.lines();
956 + VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
957 + // Both data rows should have the same width (driven by "ab"/"ef" column)
958 + VERIFY_ARE_EQUAL(lines[0].size(), lines[2].size());
959 + }
960 +
961 + TEST_METHOD(TableOutput_WriteLine_SharedColumnWidthsAcrossSections)
962 + {
963 + // Data rows in different sections share column widths because they
964 + // are sized together within a single table instance.
965 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
966 + cap.table.SetShowHeader(false);
967 +
968 + cap.table.WriteLine(FormattedCell{L"Section A:"});
969 + cap.table.WriteRow({L"short", L"x"});
970 + cap.table.WriteLine(FormattedCell{L"Section B:"});
971 + cap.table.WriteRow({L"much-longer-name", L"y"});
972 + cap.table.Complete();
973 +
974 + auto lines = cap.lines();
975 + // 4 lines: header, data, header, data
976 + VERIFY_ARE_EQUAL(static_cast<size_t>(4), lines.size());
977 +
978 + // "short" row should be padded to match "much-longer-name" column width.
979 + // Both data rows have the same total width.
980 + VERIFY_ARE_EQUAL(lines[1].size(), lines[3].size());
981 + }
982 +
983 + TEST_METHOD(TableOutput_WriteLine_FormattedCellRendersSequences)
984 + {
985 + using namespace wsl::windows::common::vt;
986 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, TableOutput<2>::column_config_t{}, true);
987 + cap.table.SetShowHeader(false);
988 +
989 + cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
990 + cap.table.WriteRow({L"name", L"desc"});
991 + cap.table.Complete();
992 +
993 + auto lines = cap.lines();
994 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
995 + // The section header should contain VT sequences (Bright + Default reset)
996 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
997 + VERIFY_IS_TRUE(lines[0].find(L"Options:") != std::wstring::npos);
998 + }
999 +
1000 + TEST_METHOD(TableOutput_WriteLine_FormattedCellStrippedWhenVTDisabled)
1001 + {
1002 + using namespace wsl::windows::common::vt;
1003 + TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
1004 + cap.table.SetShowHeader(false);
1005 +
1006 + cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
1007 + cap.table.WriteRow({L"name", L"desc"});
1008 + cap.table.Complete();
1009 +
1010 + auto lines = cap.lines();
1011 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
1012 + // VT disabled: no escape sequences, just the text
1013 + VERIFY_ARE_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
1014 + VERIFY_ARE_EQUAL(std::wstring{L"Options:"}, lines[0]);
1015 + }
1016 };
1017
397 -} // namespace WSLCTableOutputUnitTests
\ No newline at end of file
1018 +} // namespace WSLCCLITableOutputUnitTests
test/windows/wslc/WSLCCLITestHelpers.h
+51 -6
@@ -151,16 +151,61 @@ struct CaptureReporter
151 template <size_t N>
152 struct TableOutputCapture
153 {
154 - std::vector<std::wstring> lines;
154 + CaptureReporter capture;
155 wsl::windows::wslc::TableOutput<N> table;
156
157 - // Forwards constructor arguments straight to TableOutput.
158 - template <typename... Args>
159 - explicit TableOutputCapture(Args&&... args) : table(std::forward<Args>(args)...)
157 + // Header + optional config + optional VT flag.
158 + explicit TableOutputCapture(
159 + typename wsl::windows::wslc::TableOutput<N>::header_t&& header,
160 + size_t sizingBuffer = 50,
161 + size_t columnPadding = wsl::windows::wslc::TableOutput<N>::DefaultColumnPadding,
162 + bool vtEnabled = false) :
163 + capture(vtEnabled), table(capture.reporter, std::move(header), sizingBuffer, columnPadding)
164 {
161 - table.SetOutputFunction([this](const std::wstring& line) { lines.push_back(line); });
162 - // Pin the console width so shrinking tests are deterministic.
165 table.SetConsoleWidthOverride(120);
166 }
167 +
168 + // Header + column configs + optional VT flag.
169 + explicit TableOutputCapture(
170 + typename wsl::windows::wslc::TableOutput<N>::header_t&& header,
171 + typename wsl::windows::wslc::TableOutput<N>::column_config_t&& configs,
172 + bool vtEnabled = false) :
173 + capture(vtEnabled),
174 + table(capture.reporter, std::move(header), std::move(configs), 50, wsl::windows::wslc::TableOutput<N>::DefaultColumnPadding)
175 + {
176 + table.SetConsoleWidthOverride(120);
177 + }
178 +
179 + // Column definitions.
180 + explicit TableOutputCapture(typename wsl::windows::wslc::TableOutput<N>::column_def_t&& defs, bool vtEnabled = false) :
181 + capture(vtEnabled), table(capture.reporter, std::move(defs))
182 + {
183 + table.SetConsoleWidthOverride(120);
184 + }
185 +
186 + // Returns captured output split into lines.
187 + std::vector<std::wstring> lines()
188 + {
189 + auto raw = capture.captured();
190 + std::vector<std::wstring> result;
191 + size_t pos = 0;
192 + while (pos < raw.size())
193 + {
194 + auto nl = raw.find(L'\n', pos);
195 + if (nl == std::wstring::npos)
196 + {
197 + result.emplace_back(raw.substr(pos));
198 + break;
199 + }
200 + result.emplace_back(raw.substr(pos, nl - pos));
201 + pos = nl + 1;
202 + }
203 + // Remove trailing empty entry from final newline.
204 + if (!result.empty() && result.back().empty())
205 + {
206 + result.pop_back();
207 + }
208 + return result;
209 + }
210 };
211 } // namespace WSLCTestHelpers
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EAliasTests.cpp
+9 -1
@@ -65,7 +65,15 @@ class WSLCE2EAliasTests
65 const auto containerResult = RunContainerExe(L"--help");
66 containerResult.Verify({.Stderr = L"", .ExitCode = 0});
67
68 - VERIFY_ARE_EQUAL(wslcResult.Stdout.value(), containerResult.Stdout.value());
68 + // Help output should be identical except the executable name in the usage line.
69 + auto wslcOutput = wslcResult.Stdout.value();
70 + const std::wstring usageNeedle = L"Usage: wslc";
71 + const std::wstring usageReplacement = L"Usage: container";
72 + auto pos = wslcOutput.find(usageNeedle);
73 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, pos);
74 + wslcOutput.replace(pos, usageNeedle.size(), usageReplacement);
75 +
76 + VERIFY_ARE_EQUAL(wslcOutput, containerResult.Stdout.value());
77 }
78 };
79
test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
+4 -41
@@ -44,7 +44,8 @@ class WSLCE2EContainerAttachTests
44 WSLC_TEST_METHOD(WSLCE2E_Container_Attach_HelpCommand)
45 {
46 auto result = RunWslc(L"container attach --help");
47 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
47 + result.Verify({.Stderr = L"", .ExitCode = 0});
48 + VERIFY_IS_FALSE(result.Stdout.value().empty());
49 }
50
51 WSLC_TEST_METHOD(WSLCE2E_Container_Attach_TTY)
@@ -107,7 +108,8 @@ class WSLCE2EContainerAttachTests
108 WSLC_TEST_METHOD(WSLCE2E_Container_Attach_MissingContainerId)
109 {
110 auto result = RunWslc(L"container attach");
110 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
111 + result.Verify({.Stdout = L"", .ExitCode = 1});
112 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
113 }
114
115 WSLC_TEST_METHOD(WSLCE2E_Container_Attach_ContainerNotFound)
@@ -121,44 +123,5 @@ class WSLCE2EContainerAttachTests
123 private:
124 const std::wstring WslcContainerName = L"wslc-test-container";
125 const TestImage& DebianImage = DebianTestImage();
124 -
125 - std::wstring GetHelpMessage() const
126 - {
127 - std::wstringstream output;
128 - output << GetWslcHeader() //
129 - << GetDescription() //
130 - << GetUsage() //
131 - << GetAvailableCommands() //
132 - << GetAvailableOptions();
133 - return output.str();
134 - }
135 -
136 - std::wstring GetDescription() const
137 - {
138 - return L"Attaches to a container.\r\n\r\n";
139 - }
140 -
141 - std::wstring GetUsage() const
142 - {
143 - return L"Usage: wslc container attach [<options>] <container-id>\r\n\r\n";
144 - }
145 -
146 - std::wstring GetAvailableCommands() const
147 - {
148 - std::wstringstream commands;
149 - commands << L"The following arguments are available:\r\n" //
150 - << L" container-id Container ID\r\n" //
151 - << L"\r\n";
152 - return commands.str();
153 - }
154 -
155 - std::wstring GetAvailableOptions() const
156 - {
157 - std::wstringstream options;
158 - options << L"The following options are available:\r\n" //
159 - << L" -?,--help Shows help about the selected command\r\n"
160 - << L"\r\n";
161 - return options.str();
162 - }
126 };
127 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+128 -120
@@ -75,13 +75,15 @@ class WSLCE2EContainerCreateTests
75 WSLC_TEST_METHOD(WSLCE2E_Container_Create_HelpCommand)
76 {
77 auto result = RunWslc(L"container create --help");
78 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
78 + result.Verify({.Stderr = L"", .ExitCode = 0});
79 + VERIFY_IS_FALSE(result.Stdout.value().empty());
80 }
81
82 WSLC_TEST_METHOD(WSLCE2E_Container_Create_MissingImage)
83 {
84 auto result = RunWslc(L"container create --name " + WslcContainerName);
84 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
85 + result.Verify({.Stdout = L"", .ExitCode = 1});
86 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
87 }
88
89 WSLC_TEST_METHOD(WSLCE2E_Container_Create_InvalidImage)
@@ -316,75 +318,110 @@ class WSLCE2EContainerCreateTests
318 {
319 auto result =
320 RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
319 - result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
321 + result.Verify({.Stdout = L"", .ExitCode = 1});
322 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
323 + L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: <host path | "
324 + L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
325 EnsureContainerDoesNotExist(WslcContainerName);
326 }
327
328 {
329 auto result = RunWslc(
330 std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
326 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
331 + result.Verify({.Stdout = L"", .ExitCode = 1});
332 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
333 + L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: <host path "
334 + L"| named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
335 EnsureContainerDoesNotExist(WslcContainerName);
336 }
337
338 {
339 auto result = RunWslc(
340 std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
333 - result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
341 + result.Verify({.Stdout = L"", .ExitCode = 1});
342 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
343 + L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: <host path | "
344 + L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
345 EnsureContainerDoesNotExist(WslcContainerName);
346 }
347
348 {
349 auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
339 - result.Verify({.Stderr = L"Invalid volume specifications: ''. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
350 + result.Verify({.Stdout = L"", .ExitCode = 1});
351 + VERIFY_IS_TRUE(
352 + result.StderrContainsSubstring(L"Invalid volume specifications: ''. Expected format: <host path | named "
353 + L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
354 EnsureContainerDoesNotExist(WslcContainerName);
355 }
356
357 {
358 auto result =
359 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
346 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
360 + result.Verify({.Stdout = L"", .ExitCode = 1});
361 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
362 + L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: <host path | "
363 + L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
364 EnsureContainerDoesNotExist(WslcContainerName);
365 }
366
367 {
368 auto result =
369 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
353 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
370 + result.Verify({.Stdout = L"", .ExitCode = 1});
371 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
372 + L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). "
373 + L"Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
374 EnsureContainerDoesNotExist(WslcContainerName);
375 }
376
377 {
378 auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
359 - result.Verify({.Stderr = L"Invalid volume specifications: ':ro'. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
379 + result.Verify({.Stdout = L"", .ExitCode = 1});
380 + VERIFY_IS_TRUE(
381 + result.StderrContainsSubstring(L"Invalid volume specifications: ':ro'. Expected format: <host path | named "
382 + L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
383 EnsureContainerDoesNotExist(WslcContainerName);
384 }
385
386 {
387 auto result = RunWslc(
388 std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
366 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
389 + result.Verify({.Stdout = L"", .ExitCode = 1});
390 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
391 + L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: <host path "
392 + L"| named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
393 EnsureContainerDoesNotExist(WslcContainerName);
394 }
395
396 {
397 auto result = RunWslc(std::format(
398 L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
373 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
399 + result.Verify({.Stdout = L"", .ExitCode = 1});
400 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
401 + L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute "
402 + L"path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: "
403 + L"E_INVALIDARG"));
404 EnsureContainerDoesNotExist(WslcContainerName);
405 }
406
407 {
408 auto result = RunWslc(std::format(
409 L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
380 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
410 + result.Verify({.Stdout = L"", .ExitCode = 1});
411 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
412 + L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path "
413 + L"(starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: "
414 + L"E_INVALIDARG"));
415 EnsureContainerDoesNotExist(WslcContainerName);
416 }
417
418 {
419 auto result = RunWslc(std::format(
420 L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
387 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
421 + result.Verify({.Stdout = L"", .ExitCode = 1});
422 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
423 + L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected "
424 + L"format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
425 EnsureContainerDoesNotExist(WslcContainerName);
426 }
427
@@ -392,7 +429,10 @@ class WSLCE2EContainerCreateTests
429 // "::/container:ro" - host=":", container="/container". ":" is not a valid Windows path.
430 auto result = RunWslc(
431 std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
395 - result.Verify({.Stderr = L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid Windows path.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
432 + result.Verify({.Stdout = L"", .ExitCode = 1});
433 + VERIFY_IS_TRUE(
434 + result.StderrContainsSubstring(L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid "
435 + L"Windows path.\r\nError code: E_INVALIDARG"));
436 EnsureContainerDoesNotExist(WslcContainerName);
437 }
438 }
@@ -405,13 +445,19 @@ class WSLCE2EContainerCreateTests
445 {
446 auto result = RunWslc(
447 std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
408 - result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
448 + result.Verify({.Stdout = L"", .ExitCode = 1});
449 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
450 + L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). "
451 + L"Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
452 EnsureContainerDoesNotExist(WslcContainerName);
453 }
454
455 {
456 auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
414 - result.Verify({.Stderr = L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
457 + result.Verify({.Stdout = L"", .ExitCode = 1});
458 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
459 + L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: <host path | named "
460 + L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
461 EnsureContainerDoesNotExist(WslcContainerName);
462 }
463
@@ -419,14 +465,20 @@ class WSLCE2EContainerCreateTests
465 // "::" splits as host=":", container="". Container path empty check fires first.
466 auto result =
467 RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
422 - result.Verify({.Stderr = L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
468 + result.Verify({.Stdout = L"", .ExitCode = 1});
469 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
470 + L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: <host path | named "
471 + L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
472 EnsureContainerDoesNotExist(WslcContainerName);
473 }
474
475 {
476 auto result =
477 RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
429 - result.Verify({.Stderr = L"Invalid volume specifications: 'e2e_test'. Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
478 + result.Verify({.Stdout = L"", .ExitCode = 1});
479 + VERIFY_IS_TRUE(
480 + result.StderrContainsSubstring(L"Invalid volume specifications: 'e2e_test'. Expected format: <host path | named "
481 + L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
482 EnsureContainerDoesNotExist(WslcContainerName);
483 }
484 }
@@ -613,14 +665,18 @@ class WSLCE2EContainerCreateTests
665 {
666 auto result =
667 RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
616 - result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
668 + result.Verify({.Stdout = L"", .ExitCode = 1});
669 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
670 + L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
671 }
672
673 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
674 {
675 auto result =
676 RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
623 - result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
677 + result.Verify({.Stdout = L"", .ExitCode = 1});
678 + VERIFY_IS_TRUE(
679 + result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
680 }
681
682 WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
@@ -781,7 +837,8 @@ class WSLCE2EContainerCreateTests
837 {
838 auto result =
839 RunWslc(std::format(L"container create --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
784 - result.Verify({.Stderr = L"Invalid stop-timeout argument value: abc\r\n", .ExitCode = 1});
840 + result.Verify({.Stdout = L"", .ExitCode = 1});
841 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-timeout argument value: abc"));
842 VerifyContainerIsNotListed(WslcContainerName);
843 }
844
@@ -809,14 +866,18 @@ class WSLCE2EContainerCreateTests
866 {
867 auto result =
868 RunWslc(std::format(L"container create --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
812 - result.Verify({.Stderr = L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
869 + result.Verify({.Stdout = L"", .ExitCode = 1});
870 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
871 + L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
872 VerifyContainerIsNotListed(WslcContainerName);
873 }
874
875 {
876 auto result =
877 RunWslc(std::format(L"container create --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
819 - result.Verify({.Stderr = L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
878 + result.Verify({.Stdout = L"", .ExitCode = 1});
879 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
880 + L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
881 VerifyContainerIsNotListed(WslcContainerName);
882 }
883 }
@@ -885,29 +946,32 @@ class WSLCE2EContainerCreateTests
946 {
947 auto result = RunWslc(std::format(
948 L"container create --health-interval notaduration --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
888 - result.Verify(
889 - {.Stderr = L"Invalid health-interval argument value: 'notaduration'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
949 + result.Verify({.Stdout = L"", .ExitCode = 1});
950 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-interval argument value"));
951 VerifyContainerIsNotListed(WslcContainerName);
952 }
953
954 {
955 auto result =
956 RunWslc(std::format(L"container create --health-retries abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
896 - result.Verify({.Stderr = L"Invalid health-retries argument value: abc\r\n", .ExitCode = 1});
957 + result.Verify({.Stdout = L"", .ExitCode = 1});
958 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-retries argument value"));
959 VerifyContainerIsNotListed(WslcContainerName);
960 }
961
962 {
963 auto result = RunWslc(std::format(
964 LR"(container create --no-healthcheck --health-cmd "exit 0" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
903 - result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
965 + result.Verify({.Stdout = L"", .ExitCode = 1});
966 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"cannot be combined with other health check options"));
967 VerifyContainerIsNotListed(WslcContainerName);
968 }
969
970 {
971 auto result = RunWslc(std::format(
972 L"container create --no-healthcheck --health-interval 5s --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
910 - result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
973 + result.Verify({.Stdout = L"", .ExitCode = 1});
974 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"cannot be combined with other health check options"));
975 VerifyContainerIsNotListed(WslcContainerName);
976 }
977 }
@@ -917,21 +981,26 @@ class WSLCE2EContainerCreateTests
981 {
982 auto result = RunWslc(
983 std::format(L"container create --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
920 - result.Verify({.Stderr = L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9).\r\n", .ExitCode = 1});
984 + result.Verify({.Stdout = L"", .ExitCode = 1});
985 + VERIFY_IS_TRUE(
986 + result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
987 + L"(Example: SIGKILL, kill, or 9)."));
988 VerifyContainerIsNotListed(WslcContainerName);
989 }
990
991 {
992 auto result =
993 RunWslc(std::format(L"container create --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
927 - result.Verify({.Stderr = L"Invalid stop-signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
994 + result.Verify({.Stdout = L"", .ExitCode = 1});
995 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
996 VerifyContainerIsNotListed(WslcContainerName);
997 }
998
999 {
1000 auto result =
1001 RunWslc(std::format(L"container create --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
934 - result.Verify({.Stderr = L"Invalid stop-signal value: 99 is out of valid range (1-31).\r\n", .ExitCode = 1});
1002 + result.Verify({.Stdout = L"", .ExitCode = 1});
1003 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
1004 VerifyContainerIsNotListed(WslcContainerName);
1005 }
1006 }
@@ -949,7 +1018,8 @@ class WSLCE2EContainerCreateTests
1018 {
1019 auto result =
1020 RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
952 - result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
1021 + result.Verify({.Stdout = L"", .ExitCode = 1});
1022 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
1023 VerifyContainerIsNotListed(WslcContainerName);
1024 }
1025
@@ -957,7 +1027,8 @@ class WSLCE2EContainerCreateTests
1027 {
1028 auto result = RunWslc(std::format(
1029 L"container create --name {} --network bridge --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
960 - result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
1030 + result.Verify({.Stdout = L"", .ExitCode = 1});
1031 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
1032 VerifyContainerIsNotListed(WslcContainerName);
1033 }
1034
@@ -978,7 +1049,8 @@ class WSLCE2EContainerCreateTests
1049 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_EmptyValue_Rejected)
1050 {
1051 auto result = RunWslc(std::format(L"container create --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
981 - result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
1052 + result.Verify({.Stdout = L"", .ExitCode = 1});
1053 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
1054 VerifyContainerIsNotListed(WslcContainerName);
1055 }
1056
@@ -1035,7 +1107,10 @@ class WSLCE2EContainerCreateTests
1107 L"container create --network bridge --network bridge --network-alias db --name {} {} true",
1108 WslcContainerName,
1109 DebianImage.NameAndTag()));
1038 - result.Verify({.Stderr = L"Network aliases cannot be specified when multiple networks are requested. Use a single --network argument.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
1110 + result.Verify({.Stdout = L"", .ExitCode = 1});
1111 + VERIFY_IS_TRUE(
1112 + result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
1113 + L"single --network argument.\r\nError code: E_INVALIDARG"));
1114 VerifyContainerIsNotListed(WslcContainerName);
1115 }
1116
@@ -1043,7 +1118,9 @@ class WSLCE2EContainerCreateTests
1118 {
1119 auto result =
1120 RunWslc(std::format(L"container create --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1046 - result.Verify({.Stderr = L"Invalid network-alias value: network alias cannot be empty or whitespace\r\n", .ExitCode = 1});
1121 + result.Verify({.Stdout = L"", .ExitCode = 1});
1122 + VERIFY_IS_TRUE(
1123 + result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
1124 VerifyContainerIsNotListed(WslcContainerName);
1125 }
1126
@@ -1059,7 +1136,9 @@ class WSLCE2EContainerCreateTests
1136 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus_Invalid)
1137 {
1138 auto result = RunWslc(std::format(L"container create --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1062 - result.Verify({.Stderr = L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)\r\n", .ExitCode = 1});
1139 + result.Verify({.Stdout = L"", .ExitCode = 1});
1140 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1141 + L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
1142 EnsureContainerDoesNotExist(WslcContainerName);
1143 }
1144
@@ -1078,7 +1157,9 @@ class WSLCE2EContainerCreateTests
1157 {
1158 auto result =
1159 RunWslc(std::format(L"container create --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1081 - result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
1160 + result.Verify({.Stdout = L"", .ExitCode = 1});
1161 + VERIFY_IS_TRUE(
1162 + result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1163 EnsureContainerDoesNotExist(WslcContainerName);
1164 }
1165
@@ -1109,8 +1190,9 @@ class WSLCE2EContainerCreateTests
1190 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit_Invalid)
1191 {
1192 auto result = RunWslc(std::format(L"container create --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1112 - result.Verify(
1113 - {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)\r\n", .ExitCode = 1});
1193 + result.Verify({.Stdout = L"", .ExitCode = 1});
1194 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1195 + L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)"));
1196 EnsureContainerDoesNotExist(WslcContainerName);
1197 }
1198
@@ -1144,8 +1226,9 @@ class WSLCE2EContainerCreateTests
1226 {
1227 auto result = RunWslc(std::format(
1228 L"container create --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
1147 - result.Verify(
1148 - {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
1229 + result.Verify({.Stdout = L"", .ExitCode = 1});
1230 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1231 + L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
1232 EnsureContainerDoesNotExist(WslcContainerName);
1233 }
1234
@@ -1155,7 +1238,9 @@ class WSLCE2EContainerCreateTests
1238
1239 auto result = RunWslc(std::format(
1240 L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
1158 - result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
1241 + result.Verify({.Stdout = L"", .ExitCode = 1});
1242 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1243 + L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
1244 EnsureContainerDoesNotExist(WslcContainerName);
1245 }
1246
@@ -1325,82 +1410,5 @@ private:
1410 // Test volume files
1411 std::filesystem::path VolumeTestFile1;
1412 std::filesystem::path VolumeTestFile2;
1328 -
1329 - std::wstring GetHelpMessage() const
1330 - {
1331 - std::wstringstream output;
1332 - output << GetWslcHeader() //
1333 - << GetDescription() //
1334 - << GetUsage() //
1335 - << GetAvailableCommands() //
1336 - << GetAvailableOptions();
1337 - return output.str();
1338 - }
1339 -
1340 - std::wstring GetDescription() const
1341 - {
1342 - return Localization::WSLCCLI_ContainerCreateLongDesc() + L"\r\n\r\n";
1343 - }
1344 -
1345 - std::wstring GetUsage() const
1346 - {
1347 - return L"Usage: wslc container create [<options>] <image> [<command>] [<arguments>...]\r\n\r\n";
1348 - }
1349 -
1350 - std::wstring GetAvailableCommands() const
1351 - {
1352 - std::wstringstream commands;
1353 - commands << L"The following arguments are available:\r\n"
1354 - << L" image Image name\r\n"
1355 - << L" command The command to run\r\n"
1356 - << L" arguments Arguments to pass to container's init process\r\n\r\n";
1357 - return commands.str();
1358 - }
1359 -
1360 - std::wstring GetAvailableOptions() const
1361 - {
1362 - std::wstringstream options;
1363 - options
1364 - << L"The following options are available:\r\n" //
1365 - << L" --cidfile Write the container ID to the provided path\r\n"
1366 - << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1367 - << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1368 - << L" --dns-option Set DNS options\r\n"
1369 - << L" --dns-search Set DNS search domains\r\n"
1370 - << L" --domainname Container domain name\r\n"
1371 - << L" --entrypoint Specifies the container init process executable\r\n"
1372 - << L" -e,--env Key=Value pairs for environment variables\r\n"
1373 - << L" --env-file File containing key=value pairs of env variables\r\n"
1374 - << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1375 - << L" --health-cmd Command to run to check container health\r\n"
1376 - << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
1377 - << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
1378 - << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
1379 - L"1m30s)\r\n"
1380 - << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
1381 - << L" -h,--hostname Container host name\r\n"
1382 - << L" -i,--interactive Attach to stdin and keep it open\r\n"
1383 - << L" -l,--label Set metadata on an object\r\n"
1384 - << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1385 - << L" --name Name of the container\r\n"
1386 - << L" --network Connect a container to a network\r\n"
1387 - << L" --network-alias Add a network-scoped alias for the container\r\n"
1388 - << L" --no-healthcheck Disable any container-specified health check\r\n"
1389 - << L" -p,--publish Publish a port from a container to host\r\n"
1390 - << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1391 - << L" --rm Remove the container after it stops\r\n"
1392 - << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1393 - << L" --stop-signal Signal to stop the container\r\n"
1394 - << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1395 - << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1396 - << L" -t,--tty Open a TTY with the container process.\r\n"
1397 - << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1398 - << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1399 - << L" -v,--volume Bind mount a volume to the container\r\n"
1400 - << L" -w,--workdir Working directory inside the container\r\n"
1401 - << L" -?,--help Shows help about the selected command\r\n"
1402 - << L"\r\n";
1403 - return options.str();
1404 - }
1413 };
1414 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
+12 -54
@@ -61,13 +61,15 @@ class WSLCE2EContainerExecTests
61 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_HelpCommand)
62 {
63 auto result = RunWslc(L"container exec --help");
64 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
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");
70 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
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,7 +78,8 @@ class WSLCE2EContainerExecTests
78 result.Verify({.Stderr = L"", .ExitCode = 0});
79
80 result = RunWslc(std::format(L"container exec {}", WslcContainerName));
79 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'command'\r\n", .ExitCode = 1});
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)
@@ -322,8 +325,9 @@ class WSLCE2EContainerExecTests
325 result.Verify({.Stderr = L"", .ExitCode = 0});
326
327 result = RunWslc(std::format(L"container exec --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName));
325 - result.Verify(
326 - {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
328 + result.Verify({.Stdout = L"", .ExitCode = 1});
329 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
330 + L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
331 }
332
333 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MultipleFiles)
@@ -352,7 +356,9 @@ class WSLCE2EContainerExecTests
356 result.Verify({.Stderr = L"", .ExitCode = 0});
357
358 result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
355 - result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
359 + result.Verify({.Stdout = L"", .ExitCode = 1});
360 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
361 + L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
362 }
363
364 WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_DuplicateKeys_Precedence)
@@ -500,53 +506,5 @@ private:
506 // Test environment variable files
507 std::filesystem::path EnvTestFile1;
508 std::filesystem::path EnvTestFile2;
503 -
504 - std::wstring GetHelpMessage() const
505 - {
506 - std::wstringstream output;
507 - output << GetWslcHeader() //
508 - << GetDescription() //
509 - << GetUsage() //
510 - << GetAvailableCommands() //
511 - << GetAvailableOptions();
512 - return output.str();
513 - }
514 -
515 - std::wstring GetDescription() const
516 - {
517 - return L"Executes a command in a running container.\r\n\r\n";
518 - }
519 -
520 - std::wstring GetUsage() const
521 - {
522 - return L"Usage: wslc container exec [<options>] <container-id> <command> [<arguments>...]\r\n\r\n";
523 - }
524 -
525 - std::wstring GetAvailableCommands() const
526 - {
527 - std::wstringstream commands;
528 - commands << L"The following arguments are available:\r\n"
529 - << L" container-id Container ID\r\n"
530 - << L" command The command to run\r\n"
531 - << L" arguments Arguments to pass to the command being executed inside the container\r\n"
532 - << L"\r\n";
533 - return commands.str();
534 - }
535 -
536 - std::wstring GetAvailableOptions() const
537 - {
538 - std::wstringstream options;
539 - options << L"The following options are available:\r\n"
540 - << L" -d,--detach Run container in detached mode\r\n"
541 - << L" -e,--env Key=Value pairs for environment variables\r\n"
542 - << L" --env-file File containing key=value pairs of env variables\r\n"
543 - << L" -i,--interactive Attach to stdin and keep it open\r\n"
544 - << L" -t,--tty Open a TTY with the container process.\r\n"
545 - << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
546 - << L" -w,--workdir Working directory inside the container\r\n"
547 - << L" -?,--help Shows help about the selected command\r\n"
548 - << L"\r\n";
549 - return options.str();
550 - }
509 };
510 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp
+4 -42
@@ -54,13 +54,15 @@ class WSLCE2EContainerExportTests
54 WSLC_TEST_METHOD(WSLCE2E_Container_Export_HelpCommand)
55 {
56 auto result = RunWslc(L"container export --help");
57 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
57 + result.Verify({.Stderr = L"", .ExitCode = 0});
58 + VERIFY_IS_FALSE(result.Stdout.value().empty());
59 }
60
61 WSLC_TEST_METHOD(WSLCE2E_Container_Export_MissingContainerId)
62 {
63 const auto result = RunWslc(std::format(L"container export --output \"{}\"", ExportPath.wstring()));
63 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
64 + result.Verify({.Stdout = L"", .ExitCode = 1});
65 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
66 }
67
68 WSLC_TEST_METHOD(WSLCE2E_Container_Export_ContainerNotFound)
@@ -103,45 +105,5 @@ private:
105 const TestImage& DebianImage = DebianTestImage();
106
107 std::filesystem::path ExportPath{};
106 -
107 - std::wstring GetHelpMessage() const
108 - {
109 - std::wstringstream output;
110 - output << GetWslcHeader() //
111 - << GetDescription() //
112 - << GetUsage() //
113 - << GetAvailableCommands() //
114 - << GetAvailableOptions();
115 - return output.str();
116 - }
117 -
118 - std::wstring GetDescription() const
119 - {
120 - return Localization::WSLCCLI_ContainerExportLongDesc() + L"\r\n\r\n";
121 - }
122 -
123 - std::wstring GetUsage() const
124 - {
125 - return L"Usage: wslc container export [<options>] <container-id>\r\n\r\n";
126 - }
127 -
128 - std::wstring GetAvailableCommands() const
129 - {
130 - std::wstringstream commands;
131 - commands << L"The following arguments are available:\r\n" //
132 - << L" container-id Container ID\r\n" //
133 - << L"\r\n";
134 - return commands.str();
135 - }
136 -
137 - std::wstring GetAvailableOptions() const
138 - {
139 - std::wstringstream options;
140 - options << L"The following options are available:\r\n" //
141 - << L" -o,--output Write to a file, instead of STDOUT\r\n" //
142 - << L" -?,--help Shows help about the selected command\r\n" //
143 - << L"\r\n";
144 - return options.str();
145 - }
108 };
109 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp
+4 -41
@@ -49,13 +49,15 @@ class WSLCE2EContainerInspectTests
49 WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_HelpCommand)
50 {
51 auto result = RunWslc(L"container inspect --help");
52 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
52 + result.Verify({.Stderr = L"", .ExitCode = 0});
53 + VERIFY_IS_FALSE(result.Stdout.value().empty());
54 }
55
56 WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_MissingContainerId)
57 {
58 auto result = RunWslc(L"container inspect");
58 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
59 + result.Verify({.Stdout = L"", .ExitCode = 1});
60 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
61 }
62
63 WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_ContainerNotFound)
@@ -116,44 +118,5 @@ private:
118 const std::wstring TestContainerName1 = L"wslc-e2e-container-inspect-1";
119 const std::wstring TestContainerName2 = L"wslc-e2e-container-inspect-2";
120 const TestImage& DebianImage = DebianTestImage();
119 -
120 - std::wstring GetHelpMessage() const
121 - {
122 - std::wstringstream output;
123 - output << GetWslcHeader() //
124 - << GetDescription() //
125 - << GetUsage() //
126 - << GetAvailableCommands() //
127 - << GetAvailableOptions();
128 - return output.str();
129 - }
130 -
131 - std::wstring GetDescription() const
132 - {
133 - return Localization::WSLCCLI_ContainerInspectLongDesc() + L"\r\n\r\n";
134 - }
135 -
136 - std::wstring GetUsage() const
137 - {
138 - return L"Usage: wslc container inspect [<options>] <container-id>\r\n\r\n";
139 - }
140 -
141 - std::wstring GetAvailableCommands() const
142 - {
143 - std::wstringstream commands;
144 - commands << L"The following arguments are available:\r\n" //
145 - << L" container-id Container ID\r\n" //
146 - << L"\r\n";
147 - return commands.str();
148 - }
149 -
150 - std::wstring GetAvailableOptions() const
151 - {
152 - std::wstringstream options;
153 - options << L"The following options are available:\r\n" //
154 - << L" -?,--help Shows help about the selected command\r\n" //
155 - << L"\r\n";
156 - return options.str();
157 - }
121 };
122 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp
+6 -41
@@ -47,7 +47,8 @@ class WSLCE2EContainerKillTests
47 WSLC_TEST_METHOD(WSLCE2E_Container_Kill_HelpCommand)
48 {
49 auto result = RunWslc(L"container kill --help");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
50 + result.Verify({.Stderr = L"", .ExitCode = 0});
51 + VERIFY_IS_FALSE(result.Stdout.value().empty());
52 }
53
54 WSLC_TEST_METHOD(WSLCE2E_Container_Kill_KillsRunningContainer)
@@ -105,12 +106,14 @@ class WSLCE2EContainerKillTests
106
107 {
108 result = RunWslc(std::format(L"container kill {} -s 0", WslcContainerName));
108 - result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
109 + result.Verify({.Stdout = L"", .ExitCode = 1});
110 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 0 is out of valid range (1-31)."));
111 }
112
113 {
114 result = RunWslc(std::format(L"container kill {} -s 32", WslcContainerName));
113 - result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
115 + result.Verify({.Stdout = L"", .ExitCode = 1});
116 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 32 is out of valid range (1-31)."));
117 }
118 }
119
@@ -145,43 +148,5 @@ private:
148 const std::wstring WslcContainerName = L"wslc-test-container";
149 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
150 const TestImage& DebianImage = DebianTestImage();
148 -
149 - std::wstring GetHelpMessage() const
150 - {
151 - std::wstringstream output;
152 - output << GetWslcHeader() //
153 - << GetDescription() //
154 - << GetUsage() //
155 - << GetAvailableCommands() //
156 - << GetAvailableOptions();
157 - return output.str();
158 - }
159 -
160 - std::wstring GetDescription() const
161 - {
162 - return Localization::WSLCCLI_ContainerKillLongDesc() + L"\r\n\r\n";
163 - }
164 -
165 - std::wstring GetUsage() const
166 - {
167 - return L"Usage: wslc container kill [<options>] <container-id>\r\n\r\n";
168 - }
169 -
170 - std::wstring GetAvailableCommands() const
171 - {
172 - std::wstringstream commands;
173 - commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
174 - return commands.str();
175 - }
176 -
177 - std::wstring GetAvailableOptions() const
178 - {
179 - std::wstringstream options;
180 - options << L"The following options are available:\r\n"
181 - << L" -s,--signal Signal to send\r\n"
182 - << L" -?,--help Shows help about the selected command\r\n"
183 - << L"\r\n";
184 - return options.str();
185 - }
151 };
152 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
+7 -45
@@ -51,7 +51,8 @@ class WSLCE2EContainerListTests
51 WSLC_TEST_METHOD(WSLCE2E_Container_List_HelpCommand)
52 {
53 auto result = RunWslc(L"container list --help");
54 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
54 + result.Verify({.Stderr = L"", .ExitCode = 0});
55 + VERIFY_IS_FALSE(result.Stdout.value().empty());
56 }
57
58 WSLC_TEST_METHOD(WSLCE2E_Container_List_AllOption)
@@ -157,7 +158,9 @@ class WSLCE2EContainerListTests
158 WSLC_TEST_METHOD(WSLCE2E_Container_List_InvalidFormatOption)
159 {
160 const auto result = RunWslc(L"container list --format invalid");
160 - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
161 + result.Verify({.Stdout = L"", .ExitCode = 1});
162 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
163 + L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
164 }
165
166 WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat)
@@ -213,7 +216,8 @@ class WSLCE2EContainerListTests
216 {
217 // Filter values must be of the form key=value; bare keys are rejected by the CLI.
218 const auto result = RunWslc(L"container list --filter status");
216 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"status") + L"\r\n", .ExitCode = 1});
219 + result.Verify({.Stdout = L"", .ExitCode = 1});
220 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status")));
221 }
222
223 WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue)
@@ -489,47 +493,5 @@ private:
493 const std::wstring WslcContainerName = L"wslc-test-container";
494 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
495 const TestImage& DebianImage = DebianTestImage();
492 -
493 - std::wstring GetHelpMessage() const
494 - {
495 - std::wstringstream output;
496 - output << GetWslcHeader() //
497 - << GetDescription() //
498 - << GetUsage() //
499 - << GetAvailableCommandAliases() //
500 - << GetAvailableOptions();
501 - return output.str();
502 - }
503 -
504 - std::wstring GetDescription() const
505 - {
506 - return Localization::WSLCCLI_ContainerListLongDesc() + L"\r\n\r\n";
507 - }
508 -
509 - std::wstring GetUsage() const
510 - {
511 - return L"Usage: wslc container list [<options>]\r\n\r\n";
512 - }
513 -
514 - std::wstring GetAvailableCommandAliases() const
515 - {
516 - return L"The following command aliases are available: ls ps\r\n\r\n";
517 - }
518 -
519 - std::wstring GetAvailableOptions() const
520 - {
521 - std::wstringstream options;
522 - options << L"The following options are available:\r\n"
523 - << L" -a,--all Show all regardless of state.\r\n"
524 - << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
525 - << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
526 - << L" -n,--last " << Localization::WSLCCLI_LastArgDescription() << L"\r\n"
527 - << L" -l,--latest " << Localization::WSLCCLI_LatestArgDescription() << L"\r\n"
528 - << L" --no-trunc Do not truncate output\r\n"
529 - << L" -q,--quiet Outputs the container IDs only\r\n"
530 - << L" -?,--help Shows help about the selected command\r\n"
531 - << L"\r\n";
532 - return options.str();
533 - }
496 };
497 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
+2 -30
@@ -45,7 +45,8 @@ class WSLCE2EContainerPruneTests
45 WSLC_TEST_METHOD(WSLCE2E_Container_Prune_HelpCommand)
46 {
47 const auto result = RunWslc(L"container prune --help");
48 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
48 + result.Verify({.Stderr = L"", .ExitCode = 0});
49 + VERIFY_IS_FALSE(result.Stdout.value().empty());
50 }
51
52 WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers)
@@ -123,34 +124,5 @@ class WSLCE2EContainerPruneTests
124
125 private:
126 const TestImage& DebianImage = DebianTestImage();
126 -
127 - std::wstring GetHelpMessage() const
128 - {
129 - std::wstringstream output;
130 - output << GetWslcHeader() //
131 - << GetDescription() //
132 - << GetUsage() //
133 - << GetAvailableOptions();
134 - return output.str();
135 - }
136 -
137 - std::wstring GetDescription() const
138 - {
139 - return Localization::WSLCCLI_ContainerPruneLongDesc() + L"\r\n\r\n";
140 - }
141 -
142 - std::wstring GetUsage() const
143 - {
144 - return L"Usage: wslc container prune [<options>]\r\n\r\n";
145 - }
146 -
147 - std::wstring GetAvailableOptions() const
148 - {
149 - std::wstringstream options;
150 - options << L"The following options are available:\r\n"
151 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
152 - << L"\r\n";
153 - return options.str();
154 - }
127 };
128 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
+2 -45
@@ -47,7 +47,8 @@ class WSLCE2EContainerRemoveTests
47 WSLC_TEST_METHOD(WSLCE2E_Container_Remove_HelpCommand)
48 {
49 auto result = RunWslc(L"container remove --help");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
50 + result.Verify({.Stderr = L"", .ExitCode = 0});
51 + VERIFY_IS_FALSE(result.Stdout.value().empty());
52 }
53
54 WSLC_TEST_METHOD(WSLCE2E_Container_Remove_NotFound)
@@ -160,49 +161,5 @@ private:
161 const std::wstring WslcContainerName = L"wslc-test-container";
162 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
163 const TestImage& DebianImage = DebianTestImage();
163 -
164 - std::wstring GetHelpMessage() const
165 - {
166 - std::wstringstream output;
167 - output << GetWslcHeader() //
168 - << GetDescription() //
169 - << GetUsage() //
170 - << GetAvailableCommandAliases() //
171 - << GetAvailableCommands() //
172 - << GetAvailableOptions();
173 - return output.str();
174 - }
175 -
176 - std::wstring GetDescription() const
177 - {
178 - return Localization::WSLCCLI_ContainerRemoveLongDesc() + L"\r\n\r\n";
179 - }
180 -
181 - std::wstring GetUsage() const
182 - {
183 - return L"Usage: wslc container remove [<options>] <container-id>\r\n\r\n";
184 - }
185 -
186 - std::wstring GetAvailableCommandAliases() const
187 - {
188 - return L"The following command aliases are available: delete rm\r\n\r\n";
189 - }
190 -
191 - std::wstring GetAvailableCommands() const
192 - {
193 - std::wstringstream commands;
194 - commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
195 - return commands.str();
196 - }
197 -
198 - std::wstring GetAvailableOptions() const
199 - {
200 - std::wstringstream options;
201 - options << L"The following options are available:\r\n" //
202 - << L" -f,--force Delete containers even if they are running\r\n"
203 - << L" -?,--help Shows help about the selected command\r\n"
204 - << L"\r\n";
205 - return options.str();
206 - }
164 };
165 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+52 -101
@@ -76,7 +76,8 @@ class WSLCE2EContainerRunTests
76 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HelpCommand)
77 {
78 auto result = RunWslc(L"container run --help");
79 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
79 + result.Verify({.Stderr = L"", .ExitCode = 0});
80 + VERIFY_IS_FALSE(result.Stdout.value().empty());
81 }
82
83 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Container_With_Command)
@@ -292,8 +293,9 @@ class WSLCE2EContainerRunTests
293
294 auto result = RunWslc(std::format(
295 L"container run --rm --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
295 - result.Verify(
296 - {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
296 + result.Verify({.Stdout = L"", .ExitCode = 1});
297 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
298 + L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
299 }
300
301 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_InvalidContent)
@@ -307,7 +309,9 @@ class WSLCE2EContainerRunTests
309 WslcContainerName,
310 EscapePath(EnvTestFile1.wstring()),
311 DebianImage.NameAndTag()));
310 - result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
312 + result.Verify({.Stdout = L"", .ExitCode = 1});
313 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
314 + L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
315 }
316
317 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_DuplicateKeys_Precedence)
@@ -802,13 +806,17 @@ class WSLCE2EContainerRunTests
806 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_RelativePath_Fails)
807 {
808 auto result = RunWslc(std::format(L"container run --rm --tmpfs wslc-tmpfs {}", DebianImage.NameAndTag()));
805 - result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
809 + result.Verify({.Stdout = L"", .ExitCode = 1});
810 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
811 + L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
812 }
813
814 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_EmptyDestination_Fails)
815 {
816 auto result = RunWslc(std::format(L"container run --rm --tmpfs :size=64k {}", DebianImage.NameAndTag()));
811 - result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
817 + result.Verify({.Stdout = L"", .ExitCode = 1});
818 + VERIFY_IS_TRUE(
819 + result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
820 }
821
822 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir)
@@ -867,7 +875,8 @@ class WSLCE2EContainerRunTests
875 {
876 auto result =
877 RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
870 - result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
878 + result.Verify({.Stdout = L"", .ExitCode = 1});
879 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
880 VerifyContainerIsNotListed(WslcContainerName);
881 }
882
@@ -890,7 +899,8 @@ class WSLCE2EContainerRunTests
899 {
900 auto result =
901 RunWslc(std::format(L"container run --rm --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
893 - result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
902 + result.Verify({.Stdout = L"", .ExitCode = 1});
903 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
904 }
905
906 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_NonexistentNetwork_Rejected)
@@ -943,14 +953,19 @@ class WSLCE2EContainerRunTests
953 L"container run --rm --network bridge --network bridge --network-alias db --name {} {} true",
954 WslcContainerName,
955 DebianImage.NameAndTag()));
946 - result.Verify({.Stderr = L"Network aliases cannot be specified when multiple networks are requested. Use a single --network argument.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
956 + result.Verify({.Stdout = L"", .ExitCode = 1});
957 + VERIFY_IS_TRUE(
958 + result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
959 + L"single --network argument.\r\nError code: E_INVALIDARG"));
960 }
961
962 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_EmptyValue_Rejected)
963 {
964 auto result = RunWslc(
965 std::format(L"container run --rm --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
953 - result.Verify({.Stderr = L"Invalid network-alias value: network alias cannot be empty or whitespace\r\n", .ExitCode = 1});
966 + result.Verify({.Stdout = L"", .ExitCode = 1});
967 + VERIFY_IS_TRUE(
968 + result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
969 }
970
971 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
@@ -1072,7 +1087,8 @@ class WSLCE2EContainerRunTests
1087 {
1088 auto result =
1089 RunWslc(std::format(L"container run --rm --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1075 - result.Verify({.Stderr = L"Invalid stop-timeout argument value: abc\r\n", .ExitCode = 1});
1090 + result.Verify({.Stdout = L"", .ExitCode = 1});
1091 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-timeout argument value: abc"));
1092 EnsureContainerDoesNotExist(WslcContainerName);
1093 }
1094
@@ -1104,14 +1120,18 @@ class WSLCE2EContainerRunTests
1120 {
1121 auto result =
1122 RunWslc(std::format(L"container run --rm --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1107 - result.Verify({.Stderr = L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
1123 + result.Verify({.Stdout = L"", .ExitCode = 1});
1124 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1125 + L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1126 EnsureContainerDoesNotExist(WslcContainerName);
1127 }
1128
1129 {
1130 auto result =
1131 RunWslc(std::format(L"container run --rm --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1114 - result.Verify({.Stderr = L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
1132 + result.Verify({.Stdout = L"", .ExitCode = 1});
1133 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1134 + L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
1135 EnsureContainerDoesNotExist(WslcContainerName);
1136 }
1137 }
@@ -1158,7 +1178,8 @@ class WSLCE2EContainerRunTests
1178 {
1179 auto result = RunWslc(
1180 std::format(L"container run --rm --health-timeout invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1161 - result.Verify({.Stderr = L"Invalid health-timeout argument value: 'invalid'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
1181 + result.Verify({.Stdout = L"", .ExitCode = 1});
1182 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid health-timeout argument value"));
1183 EnsureContainerDoesNotExist(WslcContainerName);
1184 }
1185
@@ -1259,7 +1280,9 @@ class WSLCE2EContainerRunTests
1280 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
1281 {
1282 auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1262 - result.Verify({.Stderr = L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)\r\n", .ExitCode = 1});
1283 + result.Verify({.Stdout = L"", .ExitCode = 1});
1284 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1285 + L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
1286 EnsureContainerDoesNotExist(WslcContainerName);
1287 }
1288
@@ -1267,7 +1290,9 @@ class WSLCE2EContainerRunTests
1290 {
1291 auto result =
1292 RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1270 - result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
1293 + result.Verify({.Stdout = L"", .ExitCode = 1});
1294 + VERIFY_IS_TRUE(
1295 + result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
1296 EnsureContainerDoesNotExist(WslcContainerName);
1297 }
1298
@@ -1275,8 +1300,9 @@ class WSLCE2EContainerRunTests
1300 {
1301 auto result =
1302 RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1278 - result.Verify(
1279 - {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)\r\n", .ExitCode = 1});
1303 + result.Verify({.Stdout = L"", .ExitCode = 1});
1304 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1305 + L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)"));
1306 EnsureContainerDoesNotExist(WslcContainerName);
1307 }
1308
@@ -1285,21 +1311,26 @@ class WSLCE2EContainerRunTests
1311 {
1312 auto result = RunWslc(
1313 std::format(L"container run --rm --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1288 - result.Verify({.Stderr = L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9).\r\n", .ExitCode = 1});
1314 + result.Verify({.Stdout = L"", .ExitCode = 1});
1315 + VERIFY_IS_TRUE(
1316 + result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
1317 + L"(Example: SIGKILL, kill, or 9)."));
1318 EnsureContainerDoesNotExist(WslcContainerName);
1319 }
1320
1321 {
1322 auto result =
1323 RunWslc(std::format(L"container run --rm --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1295 - result.Verify({.Stderr = L"Invalid stop-signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
1324 + result.Verify({.Stdout = L"", .ExitCode = 1});
1325 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
1326 EnsureContainerDoesNotExist(WslcContainerName);
1327 }
1328
1329 {
1330 auto result =
1331 RunWslc(std::format(L"container run --rm --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1302 - result.Verify({.Stderr = L"Invalid stop-signal value: 99 is out of valid range (1-31).\r\n", .ExitCode = 1});
1332 + result.Verify({.Stdout = L"", .ExitCode = 1});
1333 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
1334 EnsureContainerDoesNotExist(WslcContainerName);
1335 }
1336 }
@@ -1333,85 +1364,5 @@ private:
1364
1365 // Test user-defined network
1366 const std::wstring TestNetworkName = L"wslc-test-network";
1336 -
1337 - std::wstring GetHelpMessage() const
1338 - {
1339 - std::wstringstream output;
1340 - output << GetWslcHeader() //
1341 - << GetDescription() //
1342 - << GetUsage() //
1343 - << GetAvailableCommands() //
1344 - << GetAvailableOptions();
1345 - return output.str();
1346 - }
1347 -
1348 - std::wstring GetDescription() const
1349 - {
1350 - return L"Runs a container. By default, the container is started in the foreground; use --detach to run in the "
1351 - L"background.\r\n\r\n";
1352 - }
1353 -
1354 - std::wstring GetUsage() const
1355 - {
1356 - return L"Usage: wslc container run [<options>] <image> [<command>] [<arguments>...]\r\n\r\n";
1357 - }
1358 -
1359 - std::wstring GetAvailableCommands() const
1360 - {
1361 - std::wstringstream commands;
1362 - commands << L"The following arguments are available:\r\n"
1363 - << L" image Image name\r\n"
1364 - << L" command The command to run\r\n"
1365 - << L" arguments Arguments to pass to container's init process\r\n"
1366 - << L"\r\n";
1367 - return commands.str();
1368 - }
1369 -
1370 - std::wstring GetAvailableOptions() const
1371 - {
1372 - std::wstringstream options;
1373 - options
1374 - << L"The following options are available:\r\n"
1375 - << L" --cidfile Write the container ID to the provided path\r\n"
1376 - << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1377 - << L" -d,--detach Run container in detached mode\r\n"
1378 - << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1379 - << L" --dns-option Set DNS options\r\n"
1380 - << L" --dns-search Set DNS search domains\r\n"
1381 - << L" --domainname Container domain name\r\n"
1382 - << L" --entrypoint Specifies the container init process executable\r\n"
1383 - << L" -e,--env Key=Value pairs for environment variables\r\n"
1384 - << L" --env-file File containing key=value pairs of env variables\r\n"
1385 - << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1386 - << L" --health-cmd Command to run to check container health\r\n"
1387 - << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
1388 - << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
1389 - << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
1390 - L"1m30s)\r\n"
1391 - << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
1392 - << L" -h,--hostname Container host name\r\n"
1393 - << L" -i,--interactive Attach to stdin and keep it open\r\n"
1394 - << L" -l,--label Set metadata on an object\r\n"
1395 - << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1396 - << L" --name Name of the container\r\n"
1397 - << L" --network Connect a container to a network\r\n"
1398 - << L" --network-alias Add a network-scoped alias for the container\r\n"
1399 - << L" --no-healthcheck Disable any container-specified health check\r\n"
1400 - << L" -p,--publish Publish a port from a container to host\r\n"
1401 - << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1402 - << L" --rm Remove the container after it stops\r\n"
1403 - << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1404 - << L" --stop-signal Signal to stop the container\r\n"
1405 - << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1406 - << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1407 - << L" -t,--tty Open a TTY with the container process.\r\n"
1408 - << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1409 - << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1410 - << L" -v,--volume Bind mount a volume to the container\r\n"
1411 - << L" -w,--workdir Working directory inside the container\r\n"
1412 - << L" -?,--help Shows help about the selected command\r\n"
1413 - << L"\r\n";
1414 - return options.str();
1415 - }
1367 };
1368 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
+15 -46
@@ -47,7 +47,8 @@ class WSLCE2EContainerStopTests
47 WSLC_TEST_METHOD(WSLCE2E_Container_Stop_HelpCommand)
48 {
49 auto result = RunWslc(L"container stop --help");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
50 + result.Verify({.Stderr = L"", .ExitCode = 0});
51 + VERIFY_IS_FALSE(result.Stdout.value().empty());
52 }
53
54 WSLC_TEST_METHOD(WSLCE2E_Container_Stop_InvalidSignal)
@@ -57,12 +58,14 @@ class WSLCE2EContainerStopTests
58
59 {
60 result = RunWslc(std::format(L"container stop {} -s 0 -t 0", WslcContainerName));
60 - result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
61 + result.Verify({.Stdout = L"", .ExitCode = 1});
62 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 0 is out of valid range (1-31)."));
63 }
64
65 {
66 result = RunWslc(std::format(L"container stop {} -s 32 -t 0", WslcContainerName));
65 - result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
67 + result.Verify({.Stdout = L"", .ExitCode = 1});
68 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 32 is out of valid range (1-31)."));
69 }
70 }
71
@@ -192,7 +195,9 @@ class WSLCE2EContainerStopTests
195
196 // Try to stop with an invalid signal name
197 result = RunWslc(std::format(L"container stop {} -s SIGINVALID -t 0", containerId));
195 - result.Verify({.Stderr = L"Invalid signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9).\r\n", .ExitCode = 1});
198 + result.Verify({.Stdout = L"", .ExitCode = 1});
199 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
200 + L"Invalid signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9)."));
201
202 // Verify container is still running after failed stop request
203 VerifyContainerIsListed(containerId, L"running");
@@ -212,7 +217,8 @@ class WSLCE2EContainerStopTests
217 {
218 // Invalid integer
219 result = RunWslc(std::format(L"container stop {} -t abc", containerId));
215 - result.Verify({.Stderr = L"Invalid time argument value: abc\r\n", .ExitCode = 1});
220 + result.Verify({.Stdout = L"", .ExitCode = 1});
221 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: abc"));
222
223 // Should still be running after failed stop
224 VerifyContainerIsListed(containerId, L"running");
@@ -221,7 +227,8 @@ class WSLCE2EContainerStopTests
227 {
228 // Another invalid integer shape
229 result = RunWslc(std::format(L"container stop {} -t 1.5", containerId));
224 - result.Verify({.Stderr = L"Invalid time argument value: 1.5\r\n", .ExitCode = 1});
230 + result.Verify({.Stdout = L"", .ExitCode = 1});
231 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 1.5"));
232
233 // Should still be running after failed stop
234 VerifyContainerIsListed(containerId, L"running");
@@ -230,7 +237,8 @@ class WSLCE2EContainerStopTests
237 {
238 // Invalid integer prefixed
239 result = RunWslc(std::format(L"container stop {} -t 9abc", containerId));
233 - result.Verify({.Stderr = L"Invalid time argument value: 9abc\r\n", .ExitCode = 1});
240 + result.Verify({.Stdout = L"", .ExitCode = 1});
241 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 9abc"));
242
243 // Should still be running after failed stop
244 VerifyContainerIsListed(containerId, L"running");
@@ -241,44 +249,5 @@ private:
249 const std::wstring WslcContainerName = L"wslc-test-container";
250 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
251 const TestImage& DebianImage = DebianTestImage();
244 -
245 - std::wstring GetHelpMessage() const
246 - {
247 - std::wstringstream output;
248 - output << GetWslcHeader() //
249 - << GetDescription() //
250 - << GetUsage() //
251 - << GetAvailableCommands() //
252 - << GetAvailableOptions();
253 - return output.str();
254 - }
255 -
256 - std::wstring GetDescription() const
257 - {
258 - return Localization::WSLCCLI_ContainerStopLongDesc() + L"\r\n\r\n";
259 - }
260 -
261 - std::wstring GetUsage() const
262 - {
263 - return L"Usage: wslc container stop [<options>] [<container-id>]\r\n\r\n";
264 - }
265 -
266 - std::wstring GetAvailableCommands() const
267 - {
268 - std::wstringstream commands;
269 - commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
270 - return commands.str();
271 - }
272 -
273 - std::wstring GetAvailableOptions() const
274 - {
275 - std::wstringstream options;
276 - options << L"The following options are available:\r\n"
277 - << L" -s,--signal Signal to send\r\n"
278 - << L" -t,--time Time in seconds to wait before executing (default 5)\r\n"
279 - << L" -?,--help Shows help about the selected command\r\n"
280 - << L"\r\n";
281 - return options.str();
282 - }
252 };
253 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+6 -68
@@ -36,78 +36,16 @@ class WSLCE2EContainerTests
36
37 WSLC_TEST_METHOD(WSLCE2E_Container_HelpCommand)
38 {
39 - RunWslc(L"container --help").Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
39 + auto result = RunWslc(L"container --help");
40 + result.Verify({.Stderr = L"", .ExitCode = 0});
41 + VERIFY_IS_FALSE(result.Stdout.value().empty());
42 }
43
44 WSLC_TEST_METHOD(WSLCE2E_Container_InvalidCommand_DisplaysErrorMessage)
45 {
44 - RunWslc(L"container INVALID_CMD").Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
45 - }
46 -
47 -private:
48 - std::wstring GetHelpMessage() const
49 - {
50 - std::wstringstream output;
51 - output << GetWslcHeader() //
52 - << GetDescription() //
53 - << GetUsage() //
54 - << GetAvailableCommands() //
55 - << GetAvailableOptions();
56 - return output.str();
57 - }
58 -
59 - std::wstring GetDescription() const
60 - {
61 - return Localization::WSLCCLI_ContainerCommandLongDesc() + L"\r\n\r\n";
62 - }
63 -
64 - std::wstring GetUsage() const
65 - {
66 - return L"Usage: wslc container [<command>] [<options>]\r\n\r\n";
67 - }
68 -
69 - std::wstring GetAvailableCommands() const
70 - {
71 - std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
72 - {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
73 - {L"cp", Localization::WSLCCLI_ContainerCpDesc()},
74 - {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
75 - {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
76 - {L"export", Localization::WSLCCLI_ContainerExportDesc()},
77 - {L"inspect", Localization::WSLCCLI_ContainerInspectDesc()},
78 - {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
79 - {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
80 - {L"list", Localization::WSLCCLI_ContainerListDesc()},
81 - {L"prune", Localization::WSLCCLI_ContainerPruneDesc()},
82 - {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
83 - {L"run", Localization::WSLCCLI_ContainerRunDesc()},
84 - {L"start", Localization::WSLCCLI_ContainerStartDesc()},
85 - {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
86 - {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
87 - };
88 -
89 - size_t maxLen = 0;
90 - for (const auto& [name, _] : entries)
91 - {
92 - maxLen = (std::max)(maxLen, name.size());
93 - }
94 -
95 - std::wstringstream commands;
96 - commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
97 - for (const auto& [name, desc] : entries)
98 - {
99 - commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
100 - }
101 - commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
102 - return commands.str();
103 - }
104 -
105 - std::wstring GetAvailableOptions() const
106 - {
107 - std::wstringstream options;
108 - options << L"The following options are available:\r\n" //
109 - << L" -?,--help Shows help about the selected command\r\n\r\n";
110 - return options.str();
46 + auto result = RunWslc(L"container INVALID_CMD");
47 + result.Verify({.Stdout = L"", .ExitCode = 1});
48 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
49 }
50 };
51 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+39 -87
@@ -58,12 +58,49 @@ class WSLCE2EGlobalTests
58
59 WSLC_TEST_METHOD(WSLCE2E_HelpCommand)
60 {
61 - RunWslcAndVerify(L"--help", {.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
61 + auto result = RunWslc(L"--help");
62 + result.Verify({.Stderr = L"", .ExitCode = 0});
63 + VERIFY_IS_FALSE(result.Stdout.value().empty());
64 }
65
66 WSLC_TEST_METHOD(WSLCE2E_InvalidCommand_DisplaysErrorMessage)
67 {
66 - RunWslcAndVerify(L"INVALID_CMD", {.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
68 + auto result = RunWslc(L"INVALID_CMD");
69 + result.Verify({.Stdout = L"", .ExitCode = 1});
70 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
71 + }
72 +
73 + WSLC_TEST_METHOD(WSLCE2E_Help_RoutesToStdout)
74 + {
75 + auto result = RunWslc(L"--help");
76 + result.Verify({.Stderr = L"", .ExitCode = 0});
77 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Usage: wslc"));
78 + }
79 +
80 + WSLC_TEST_METHOD(WSLCE2E_Help_ErrorRoutesToStderr)
81 + {
82 + // Help on error must land on stderr; stdout must remain empty.
83 + auto result = RunWslc(L"INVALID_CMD");
84 + VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
85 + VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
86 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
87 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc"));
88 + }
89 +
90 + WSLC_TEST_METHOD(WSLCE2E_Help_NoColorWhenRedirected)
91 + {
92 + // Captured via anonymous pipe; Reporter must suppress VT escape sequences.
93 + auto result = RunWslc(L"--help");
94 + result.Verify({.Stderr = L"", .ExitCode = 0});
95 + VERIFY_ARE_EQUAL(std::wstring::npos, result.Stdout.value().find(L'\x1b'));
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_Help_ColorOnTerminal)
99 + {
100 + // Pseudo console reports VT support; Reporter should emit SGR sequences.
101 + auto session = RunWslcInteractive(L"--help", ElevationType::Elevated, PseudoConsole{120, 30});
102 + session.WaitForExit();
103 + VERIFY_IS_TRUE(session.GetStdoutData().find('\x1b') != std::string::npos);
104 }
105
106 WSLC_TEST_METHOD(WSLCE2E_VersionCommand)
@@ -536,94 +573,9 @@ class WSLCE2EGlobalTests
573 }
574
575 private:
539 - std::wstring GetHelpMessage() const
540 - {
541 - std::wstringstream output;
542 - output << GetWslcHeader() //
543 - << GetDescription() //
544 - << GetUsage() //
545 - << GetAvailableCommands() //
546 - << GetAvailableOptions();
547 - return output.str();
548 - }
549 -
576 std::wstring GetVersionMessage() const
577 {
578 return std::format(L"wslc {}\r\n", WSL_PACKAGE_VERSION);
579 }
554 -
555 - std::wstring GetDescription() const
556 - {
557 - return L"WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL "
558 - L"containers from the command line.\r\n\r\n";
559 - }
560 -
561 - std::wstring GetUsage() const
562 - {
563 - return L"Usage: wslc [<command>] [<options>]\r\n\r\n";
564 - }
565 -
566 - std::wstring GetAvailableCommands() const
567 - {
568 - std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
569 - {L"container", Localization::WSLCCLI_ContainerCommandDesc()},
570 - {L"image", Localization::WSLCCLI_ImageCommandDesc()},
571 - {L"network", Localization::WSLCCLI_NetworkCommandDesc()},
572 - {L"registry", Localization::WSLCCLI_RegistryCommandDesc()},
573 - {L"settings", Localization::WSLCCLI_SettingsCommandDesc()},
574 - {L"system", Localization::WSLCCLI_SystemCommandDesc()},
575 - {L"volume", Localization::WSLCCLI_VolumeCommandDesc()},
576 - {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
577 - {L"build", Localization::WSLCCLI_ImageBuildDesc()},
578 - {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
579 - {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
580 - {L"export", Localization::WSLCCLI_ContainerExportDesc()},
581 - {L"images", Localization::WSLCCLI_ImageListDesc()},
582 - {L"import", Localization::WSLCCLI_ImageImportDesc()},
583 - {L"inspect", Localization::WSLCCLI_InspectDesc()},
584 - {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
585 - {L"list", Localization::WSLCCLI_ContainerListDesc()},
586 - {L"load", Localization::WSLCCLI_ImageLoadDesc()},
587 - {L"login", Localization::WSLCCLI_LoginDesc()},
588 - {L"logout", Localization::WSLCCLI_LogoutDesc()},
589 - {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
590 - {L"pull", Localization::WSLCCLI_ImagePullDesc()},
591 - {L"push", Localization::WSLCCLI_ImagePushDesc()},
592 - {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
593 - {L"rmi", Localization::WSLCCLI_ImageRemoveDesc()},
594 - {L"run", Localization::WSLCCLI_ContainerRunDesc()},
595 - {L"save", Localization::WSLCCLI_ImageSaveDesc()},
596 - {L"start", Localization::WSLCCLI_ContainerStartDesc()},
597 - {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
598 - {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
599 - {L"tag", Localization::WSLCCLI_ImageTagDesc()},
600 - {L"version", Localization::WSLCCLI_VersionDesc()},
601 - };
602 -
603 - size_t maxLen = 0;
604 - for (const auto& [name, _] : entries)
605 - {
606 - maxLen = (std::max)(maxLen, name.size());
607 - }
608 -
609 - std::wstringstream commands;
610 - commands << Localization::WSLCCLI_AvailableCommands() << L"\r\n";
611 - for (const auto& [name, desc] : entries)
612 - {
613 - commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
614 - }
615 - commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
616 - return commands.str();
617 - }
618 -
619 - std::wstring GetAvailableOptions() const
620 - {
621 - std::wstringstream options;
622 - options << L"The following options are available:\r\n"
623 - << L" -v,--version Show version information for this tool\r\n"
624 - << L" -?,--help Shows help about the selected command\r\n"
625 - << L"\r\n";
626 - return options.str();
627 - }
580 };
581 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
+4 -49
@@ -44,7 +44,8 @@ class WSLCE2EImageDeleteTests
44 WSLC_TEST_METHOD(WSLCE2E_Image_Delete_HelpCommand)
45 {
46 auto result = RunWslc(L"image delete --help");
47 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
47 + result.Verify({.Stderr = L"", .ExitCode = 0});
48 + VERIFY_IS_FALSE(result.Stdout.value().empty());
49 }
50
51 WSLC_TEST_METHOD(WSLCE2E_Image_Delete_ImageNotFound)
@@ -57,7 +58,8 @@ class WSLCE2EImageDeleteTests
58 WSLC_TEST_METHOD(WSLCE2E_Image_Delete_MissingImageName)
59 {
60 auto result = RunWslc(L"image delete");
60 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
61 + result.Verify({.Stdout = L"", .ExitCode = 1});
62 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
63 }
64
65 WSLC_TEST_METHOD(WSLCE2E_Image_Delete_UnusedImage_Success)
@@ -150,52 +152,5 @@ private:
152 const TestImage& AlpineImage = AlpineTestImage();
153 const TestImage& InvalidImage = InvalidTestImage();
154 const TestImage NoPruneTaggedImage{L"wslc-test-noprune", L"alias", L""};
153 -
154 - std::wstring GetHelpMessage() const
155 - {
156 - std::wstringstream output;
157 - output << GetWslcHeader() //
158 - << GetDescription() //
159 - << GetUsage() //
160 - << GetAvailableCommandAliases() //
161 - << GetAvailableCommands() //
162 - << GetAvailableOptions();
163 - return output.str();
164 - }
165 -
166 - std::wstring GetDescription() const
167 - {
168 - return Localization::WSLCCLI_ImageRemoveLongDesc() + L"\r\n\r\n";
169 - }
170 -
171 - std::wstring GetUsage() const
172 - {
173 - return L"Usage: wslc image remove [<options>] <image>\r\n\r\n";
174 - }
175 -
176 - std::wstring GetAvailableCommandAliases() const
177 - {
178 - return L"The following command aliases are available: delete rm\r\n\r\n";
179 - }
180 -
181 - std::wstring GetAvailableCommands() const
182 - {
183 - std::wstringstream commands;
184 - commands << L"The following arguments are available:\r\n" //
185 - << L" image Image name\r\n" //
186 - << L"\r\n";
187 - return commands.str();
188 - }
189 -
190 - std::wstring GetAvailableOptions() const
191 - {
192 - std::wstringstream options;
193 - options << L"The following options are available:\r\n" //
194 - << L" -f,--force Delete images even if they are being used\r\n" //
195 - << L" --no-prune Do not delete untagged parents\r\n" //
196 - << L" -?,--help Shows help about the selected command\r\n" //
197 - << L"\r\n";
198 - return options.str();
199 - }
155 };
156 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
+2 -3
@@ -57,9 +57,8 @@ class WSLCE2EImageImportTests
57 WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile)
58 {
59 const auto result = RunWslc(L"image import");
60 - result.Verify({.ExitCode = 1});
61 - VERIFY_IS_TRUE(result.Stderr.has_value());
62 - VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'file'") != std::wstring::npos);
60 + result.Verify({.Stdout = L"", .ExitCode = 1});
61 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'file'"));
62 }
63
64 WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success)
test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
+4 -41
@@ -40,13 +40,15 @@ class WSLCE2EImageInspectTests
40 WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_HelpCommand)
41 {
42 auto result = RunWslc(L"image inspect --help");
43 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
43 + result.Verify({.Stderr = L"", .ExitCode = 0});
44 + VERIFY_IS_FALSE(result.Stdout.value().empty());
45 }
46
47 WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_MissingImageName)
48 {
49 auto result = RunWslc(L"image inspect");
49 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
50 + result.Verify({.Stdout = L"", .ExitCode = 1});
51 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
52 }
53
54 WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_ImageNotFound)
@@ -126,44 +128,5 @@ private:
128 const TestImage& DebianImage = DebianTestImage();
129 const TestImage& InvalidImage = InvalidTestImage();
130 const TestImage BuiltExposeImage{L"wslc-e2e-inspect-config-extras", L"latest", L""};
129 -
130 - std::wstring GetHelpMessage() const
131 - {
132 - std::wstringstream output;
133 - output << GetWslcHeader() //
134 - << GetDescription() //
135 - << GetUsage() //
136 - << GetAvailableCommands() //
137 - << GetAvailableOptions();
138 - return output.str();
139 - }
140 -
141 - std::wstring GetDescription() const
142 - {
143 - return Localization::WSLCCLI_ImageInspectLongDesc() + L"\r\n\r\n";
144 - }
145 -
146 - std::wstring GetUsage() const
147 - {
148 - return L"Usage: wslc image inspect [<options>] <image>\r\n\r\n";
149 - }
150 -
151 - std::wstring GetAvailableCommands() const
152 - {
153 - std::wstringstream commands;
154 - commands << L"The following arguments are available:\r\n" //
155 - << L" image Image name\r\n" //
156 - << L"\r\n";
157 - return commands.str();
158 - }
159 -
160 - std::wstring GetAvailableOptions() const
161 - {
162 - std::wstringstream options;
163 - options << L"The following options are available:\r\n" //
164 - << L" -?,--help Shows help about the selected command\r\n" //
165 - << L"\r\n";
166 - return options.str();
167 - }
131 };
132 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
+7 -43
@@ -43,7 +43,8 @@ class WSLCE2EImageListTests
43 WSLC_TEST_METHOD(WSLCE2E_Image_List_HelpCommand)
44 {
45 const auto result = RunWslc(L"image list --help");
46 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
46 + result.Verify({.Stderr = L"", .ExitCode = 0});
47 + VERIFY_IS_FALSE(result.Stdout.value().empty());
48 }
49
50 WSLC_TEST_METHOD(WSLCE2E_Image_List_DisplayLoadedImage)
@@ -117,7 +118,9 @@ class WSLCE2EImageListTests
118 WSLC_TEST_METHOD(WSLCE2E_Image_List_InvalidFormatOption)
119 {
120 const auto result = RunWslc(L"image list --format invalid");
120 - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
121 + result.Verify({.Stdout = L"", .ExitCode = 1});
122 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
123 + L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
124 }
125
126 WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat)
@@ -167,7 +170,8 @@ class WSLCE2EImageListTests
170 {
171 // Filter values must be of the form key=value; bare keys are rejected by the CLI.
172 const auto result = RunWslc(L"image list --filter dangling");
170 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"dangling") + L"\r\n", .ExitCode = 1});
173 + result.Verify({.Stdout = L"", .ExitCode = 1});
174 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling")));
175 }
176
177 WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_InvalidKey)
@@ -315,45 +319,5 @@ class WSLCE2EImageListTests
319 private:
320 const TestImage& DebianImage = DebianTestImage();
321 const TestImage& AlpineImage = AlpineTestImage();
318 -
319 - std::wstring GetHelpMessage() const
320 - {
321 - std::wstringstream output;
322 - output << GetWslcHeader() //
323 - << GetDescription() //
324 - << GetUsage() //
325 - << GetAvailableCommandAliases() //
326 - << GetAvailableOptions();
327 - return output.str();
328 - }
329 -
330 - std::wstring GetDescription() const
331 - {
332 - return Localization::WSLCCLI_ImageListLongDesc() + L"\r\n\r\n";
333 - }
334 -
335 - std::wstring GetUsage() const
336 - {
337 - return L"Usage: wslc image list [<options>]\r\n\r\n";
338 - }
339 -
340 - std::wstring GetAvailableCommandAliases() const
341 - {
342 - return L"The following command aliases are available: ls\r\n\r\n";
343 - }
344 -
345 - std::wstring GetAvailableOptions() const
346 - {
347 - std::wstringstream options;
348 - options << L"The following options are available:\r\n"
349 - << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
350 - << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
351 - << L" --no-trunc Do not truncate output\r\n"
352 - << L" -q,--quiet Outputs the container IDs only\r\n"
353 - << L" --verbose Output verbose details\r\n"
354 - << L" -?,--help Shows help about the selected command\r\n"
355 - << L"\r\n";
356 - return options.str();
357 - }
322 };
323 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
+4 -33
@@ -38,7 +38,8 @@ class WSLCE2EImagePruneTests
38 WSLC_TEST_METHOD(WSLCE2E_Image_Prune_HelpCommand)
39 {
40 const auto result = RunWslc(L"image prune --help");
41 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
41 + result.Verify({.Stderr = L"", .ExitCode = 0});
42 + VERIFY_IS_FALSE(result.Stdout.value().empty());
43 }
44
45 WSLC_TEST_METHOD(WSLCE2E_Image_Prune_NoDanglingImages)
@@ -114,7 +115,8 @@ class WSLCE2EImagePruneTests
115 {
116 // Filter values must be of the form key=value; bare keys are rejected by the CLI.
117 const auto result = RunWslc(L"image prune --filter label");
117 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1});
118 + result.Verify({.Stdout = L"", .ExitCode = 1});
119 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
120 }
121
122 WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey)
@@ -187,36 +189,5 @@ private:
189
190 VERIFY_FAIL(std::format(L"Expected stdout to contain '{}'", substring).c_str());
191 }
190 -
191 - std::wstring GetHelpMessage() const
192 - {
193 - std::wstringstream output;
194 - output << GetWslcHeader() //
195 - << GetDescription() //
196 - << GetUsage() //
197 - << GetAvailableOptions();
198 - return output.str();
199 - }
200 -
201 - std::wstring GetDescription() const
202 - {
203 - return Localization::WSLCCLI_ImagePruneLongDesc() + L"\r\n\r\n";
204 - }
205 -
206 - std::wstring GetUsage() const
207 - {
208 - return L"Usage: wslc image prune [<options>]\r\n\r\n";
209 - }
210 -
211 - std::wstring GetAvailableOptions() const
212 - {
213 - std::wstringstream options;
214 - options << L"The following options are available:\r\n"
215 - << L" -a,--all " << Localization::WSLCCLI_ImagePruneAllArgDescription() << L"\r\n"
216 - << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
217 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
218 - << L"\r\n";
219 - return options.str();
220 - }
192 };
193 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
+7 -44
@@ -46,13 +46,15 @@ class WSLCE2EImageSaveTests
46 WSLC_TEST_METHOD(WSLCE2E_Image_Save_HelpCommand)
47 {
48 auto result = RunWslc(L"image save --help");
49 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
49 + result.Verify({.Stderr = L"", .ExitCode = 0});
50 + VERIFY_IS_FALSE(result.Stdout.value().empty());
51 }
52
53 WSLC_TEST_METHOD(WSLCE2E_Image_Save_MissingImageName)
54 {
55 const auto result = RunWslc(std::format(L"image save --output \"{}\"", SavedArchivePath.wstring()));
55 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
56 + result.Verify({.Stdout = L"", .ExitCode = 1});
57 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
58 }
59
60 WSLC_TEST_METHOD(WSLCE2E_Image_Save_ImageNotFound)
@@ -108,8 +110,9 @@ class WSLCE2EImageSaveTests
110 SKIP_TEST_UNSTABLE();
111
112 const auto result = RunWslcAndRedirectToFile(std::format(L"image save {}", DebianImage.NameAndTag()));
111 - result.Verify(
112 - {.Stderr = L"Cannot write image to terminal. Use the -o flag or redirect stdout.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
113 + result.Verify({.Stdout = L"", .ExitCode = 1});
114 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
115 + L"Cannot write image to terminal. Use the -o flag or redirect stdout.\r\nError code: E_INVALIDARG"));
116 }
117
118 WSLC_TEST_METHOD(WSLCE2E_Image_Save_ToStdout_Load)
@@ -183,45 +186,5 @@ private:
186 const TestImage& InvalidImage = InvalidTestImage();
187
188 std::filesystem::path SavedArchivePath{};
186 -
187 - std::wstring GetHelpMessage() const
188 - {
189 - std::wstringstream output;
190 - output << GetWslcHeader() //
191 - << GetDescription() //
192 - << GetUsage() //
193 - << GetAvailableCommands() //
194 - << GetAvailableOptions();
195 - return output.str();
196 - }
197 -
198 - std::wstring GetDescription() const
199 - {
200 - return Localization::WSLCCLI_ImageSaveLongDesc() + L"\r\n\r\n";
201 - }
202 -
203 - std::wstring GetUsage() const
204 - {
205 - return L"Usage: wslc image save [<options>] <image>\r\n\r\n";
206 - }
207 -
208 - std::wstring GetAvailableCommands() const
209 - {
210 - std::wstringstream commands;
211 - commands << L"The following arguments are available:\r\n" //
212 - << L" image Image name\r\n" //
213 - << L"\r\n";
214 - return commands.str();
215 - }
216 -
217 - std::wstring GetAvailableOptions() const
218 - {
219 - std::wstringstream options;
220 - options << L"The following options are available:\r\n" //
221 - << L" -o,--output Path for the saved image\r\n" //
222 - << L" -?,--help Shows help about the selected command\r\n" //
223 - << L"\r\n";
224 - return options.str();
225 - }
189 };
190 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp
+6 -43
@@ -43,19 +43,22 @@ class WSLCE2EImageTagTests
43 WSLC_TEST_METHOD(WSLCE2E_Image_Tag_HelpCommand)
44 {
45 auto result = RunWslc(L"image tag --help");
46 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
46 + result.Verify({.Stderr = L"", .ExitCode = 0});
47 + VERIFY_IS_FALSE(result.Stdout.value().empty());
48 }
49
50 WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingSourceAndTarget)
51 {
52 auto result = RunWslc(L"image tag");
52 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'source'\r\n", .ExitCode = 1});
53 + result.Verify({.Stdout = L"", .ExitCode = 1});
54 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'source'"));
55 }
56
57 WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingTarget)
58 {
59 auto result = RunWslc(std::format(L"image tag {}", DebianImage.NameAndTag()));
58 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'target'\r\n", .ExitCode = 1});
60 + result.Verify({.Stdout = L"", .ExitCode = 1});
61 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'target'"));
62 }
63
64 WSLC_TEST_METHOD(WSLCE2E_Image_Tag_SourceImageNotFound)
@@ -167,45 +170,5 @@ private:
170 const TestImage& AlpineImage = AlpineTestImage();
171 const TestImage& InvalidImage = InvalidTestImage();
172 const TestImage DebianTaggedImage{L"debian", L"e2e-new-tag"};
170 -
171 - std::wstring GetHelpMessage() const
172 - {
173 - std::wstringstream output;
174 - output << GetWslcHeader() //
175 - << GetDescription() //
176 - << GetUsage() //
177 - << GetAvailableCommands() //
178 - << GetAvailableOptions();
179 - return output.str();
180 - }
181 -
182 - std::wstring GetDescription() const
183 - {
184 - return wsl::shared::Localization::WSLCCLI_ImageTagLongDesc() + L"\r\n\r\n";
185 - }
186 -
187 - std::wstring GetUsage() const
188 - {
189 - return L"Usage: wslc image tag [<options>] <source> <target>\r\n\r\n";
190 - }
191 -
192 - std::wstring GetAvailableCommands() const
193 - {
194 - std::wstringstream commands;
195 - commands << L"The following arguments are available:\r\n" //
196 - << L" source Current or existing image reference in the image-name[:tag] format\r\n" //
197 - << L" target New image reference in the image-name[:tag] format\r\n" //
198 - << L"\r\n";
199 - return commands.str();
200 - }
201 -
202 - std::wstring GetAvailableOptions() const
203 - {
204 - std::wstringstream options;
205 - options << L"The following options are available:\r\n" //
206 - << L" -?,--help Shows help about the selected command\r\n" //
207 - << L"\r\n";
208 - return options.str();
209 - }
173 };
174 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EImageTests.cpp
+6 -66
@@ -27,82 +27,22 @@ class WSLCE2EImageTests
27 WSLC_TEST_METHOD(WSLCE2E_Image_HelpCommand)
28 {
29 auto result = RunWslc(L"image --help");
30 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
30 + result.Verify({.Stderr = L"", .ExitCode = 0});
31 + VERIFY_IS_FALSE(result.Stdout.value().empty());
32 }
33
34 WSLC_TEST_METHOD(WSLCE2E_Image_NoSubcommand_ShowsHelp)
35 {
36 auto result = RunWslc(L"image");
36 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
37 + result.Verify({.Stderr = L"", .ExitCode = 0});
38 + VERIFY_IS_FALSE(result.Stdout.value().empty());
39 }
40
41 WSLC_TEST_METHOD(WSLCE2E_Image_InvalidCommand_DisplaysErrorMessage)
42 {
43 auto result = RunWslc(L"image INVALID_CMD");
42 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
43 - }
44 -
45 -private:
46 - std::wstring GetHelpMessage() const
47 - {
48 - std::wstringstream output;
49 - output << GetWslcHeader() //
50 - << GetDescription() //
51 - << GetUsage() //
52 - << GetAvailableCommands() //
53 - << GetAvailableOptions();
54 - return output.str();
55 - }
56 -
57 - std::wstring GetDescription() const
58 - {
59 - return Localization::WSLCCLI_ImageCommandLongDesc() + L"\r\n\r\n";
60 - }
61 -
62 - std::wstring GetUsage() const
63 - {
64 - return L"Usage: wslc image [<command>] [<options>]\r\n\r\n";
65 - }
66 -
67 - std::wstring GetAvailableCommands() const
68 - {
69 - std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
70 - {L"build", Localization::WSLCCLI_ImageBuildDesc()},
71 - {L"remove", Localization::WSLCCLI_ImageRemoveDesc()},
72 - {L"inspect", Localization::WSLCCLI_ImageInspectDesc()},
73 - {L"list", Localization::WSLCCLI_ImageListDesc()},
74 - {L"load", Localization::WSLCCLI_ImageLoadDesc()},
75 - {L"import", Localization::WSLCCLI_ImageImportDesc()},
76 - {L"prune", Localization::WSLCCLI_ImagePruneDesc()},
77 - {L"pull", Localization::WSLCCLI_ImagePullDesc()},
78 - {L"push", Localization::WSLCCLI_ImagePushDesc()},
79 - {L"save", Localization::WSLCCLI_ImageSaveDesc()},
80 - {L"tag", Localization::WSLCCLI_ImageTagDesc()},
81 - };
82 -
83 - size_t maxLen = 0;
84 - for (const auto& [name, _] : entries)
85 - {
86 - maxLen = (std::max)(maxLen, name.size());
87 - }
88 -
89 - std::wstringstream commands;
90 - commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
91 - for (const auto& [name, desc] : entries)
92 - {
93 - commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
94 - }
95 - commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n" << L"\r\n";
96 - return commands.str();
97 - }
98 -
99 - std::wstring GetAvailableOptions() const
100 - {
101 - std::wstringstream options;
102 - options << L"The following options are available:\r\n"
103 - << L" -?,--help Shows help about the selected command\r\n"
104 - << L"\r\n";
105 - return options.str();
44 + result.Verify({.Stdout = L"", .ExitCode = 1});
45 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
46 }
47 };
48 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCE2EInspectTests.cpp
+8 -42
@@ -53,13 +53,15 @@ class WSLCE2EInspectTests
53 WSLC_TEST_METHOD(WSLCE2E_Inspect_HelpCommand)
54 {
55 auto result = RunWslc(L"inspect --help");
56 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
56 + result.Verify({.Stderr = L"", .ExitCode = 0});
57 + VERIFY_IS_FALSE(result.Stdout.value().empty());
58 }
59
60 WSLC_TEST_METHOD(WSLCE2E_Inspect_MissingObjectId)
61 {
62 auto result = RunWslc(L"inspect");
62 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'object-id'\r\n", .ExitCode = 1});
63 + result.Verify({.Stdout = L"", .ExitCode = 1});
64 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'object-id'"));
65 }
66
67 WSLC_TEST_METHOD(WSLCE2E_Inspect_ObjectNotFound)
@@ -290,7 +292,10 @@ class WSLCE2EInspectTests
292 WSLC_TEST_METHOD(WSLCE2E_Inspect_InvalidTypeValue)
293 {
294 auto result = RunWslc(std::format(L"inspect --type invalid {}", DebianImage.NameAndTag()));
293 - result.Verify({.Stderr = L"Invalid type value: invalid is not a recognized inspect type. Supported inspect types are: image, container, network, volume.\r\n", .ExitCode = 1});
295 + result.Verify({.Stdout = L"", .ExitCode = 1});
296 + VERIFY_IS_TRUE(
297 + result.StderrContainsSubstring(L"Invalid type value: invalid is not a recognized inspect type. Supported inspect "
298 + L"types are: image, container, network, volume."));
299 }
300
301 WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidFormatError)
@@ -313,44 +318,5 @@ private:
318 const TestImage& InvalidImage = InvalidTestImage();
319 const std::wstring WslcVolumeName = L"wslc-inspect-test-volume";
320 const std::wstring WslcNetworkName = L"wslc-inspect-test-network";
316 - std::wstring GetHelpMessage() const
317 - {
318 - std::wstringstream output;
319 - output << GetWslcHeader() //
320 - << GetDescription() //
321 - << GetUsage() //
322 - << GetAvailableCommands() //
323 - << GetAvailableOptions();
324 - return output.str();
325 - }
326 -
327 - std::wstring GetDescription() const
328 - {
329 - return Localization::WSLCCLI_InspectLongDesc() + L"\r\n\r\n";
330 - }
331 -
332 - std::wstring GetUsage() const
333 - {
334 - return L"Usage: wslc inspect [<options>] <object-id>\r\n\r\n";
335 - }
336 -
337 - std::wstring GetAvailableCommands() const
338 - {
339 - std::wstringstream commands;
340 - commands << L"The following arguments are available:\r\n" //
341 - << L" object-id Name or Id of any object type\r\n" //
342 - << L"\r\n";
343 - return commands.str();
344 - }
345 -
346 - std::wstring GetAvailableOptions() const
347 - {
348 - std::wstringstream options;
349 - options << L"The following options are available:\r\n" //
350 - << L" -t,--type Type of the object to inspect\r\n" //
351 - << L" -?,--help Shows help about the selected command\r\n" //
352 - << L"\r\n";
353 - return options.str();
354 - }
321 };
322 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp
+9 -50
@@ -38,13 +38,15 @@ class WSLCE2ENetworkCreateTests
38 WSLC_TEST_METHOD(WSLCE2E_Network_Create_HelpCommand)
39 {
40 auto result = RunWslc(L"network create --help");
41 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
41 + result.Verify({.Stderr = L"", .ExitCode = 0});
42 + VERIFY_IS_FALSE(result.Stdout.value().empty());
43 }
44
45 WSLC_TEST_METHOD(WSLCE2E_Network_Create_MissingName)
46 {
47 auto result = RunWslc(L"network create");
47 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
48 + result.Verify({.Stdout = L"", .ExitCode = 1});
49 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'"));
50 }
51
52 WSLC_TEST_METHOD(WSLCE2E_Network_Create_DefaultDriver_Success)
@@ -85,7 +87,8 @@ class WSLCE2ENetworkCreateTests
87 WSLC_TEST_METHOD(WSLCE2E_Network_Create_EmptyLabelKey_Fail)
88 {
89 auto result = RunWslc(std::format(L"network create --driver bridge --label =foo {}", TestNetworkName));
88 - result.Verify({.Stdout = L"", .Stderr = L"Label key cannot be empty\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
90 + result.Verify({.Stdout = L"", .ExitCode = 1});
91 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Label key cannot be empty\r\nError code: E_INVALIDARG"));
92
93 VerifyNetworkIsNotListed(TestNetworkName);
94 }
@@ -93,8 +96,9 @@ class WSLCE2ENetworkCreateTests
96 WSLC_TEST_METHOD(WSLCE2E_Network_Create_InvalidDriver_Fail)
97 {
98 auto result = RunWslc(std::format(L"network create --driver invalid_driver {}", TestNetworkName));
96 - result.Verify(
97 - {.Stdout = L"", .Stderr = std::format(L"Unsupported network driver: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n"), .ExitCode = 1});
99 + result.Verify({.Stdout = L"", .ExitCode = 1});
100 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
101 + std::format(L"Unsupported network driver: 'invalid_driver'\r\nError code: E_INVALIDARG")));
102
103 VerifyNetworkIsNotListed(TestNetworkName);
104 }
@@ -181,50 +185,5 @@ class WSLCE2ENetworkCreateTests
185
186 private:
187 const std::wstring TestNetworkName = L"wslc-e2e-network-create";
184 -
185 - std::wstring GetHelpMessage() const
186 - {
187 - std::wstringstream output;
188 - output << GetWslcHeader() //
189 - << GetDescription() //
190 - << GetUsage() //
191 - << GetAvailableCommands() //
192 - << GetAvailableOptions();
193 - return output.str();
194 - }
195 -
196 - std::wstring GetDescription() const
197 - {
198 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkCreateLongDesc());
199 - }
200 -
201 - std::wstring GetUsage() const
202 - {
203 - return L"Usage: wslc network create [<options>] <network-name>\r\n\r\n";
204 - }
205 -
206 - std::wstring GetAvailableCommands() const
207 - {
208 - std::wstringstream commands;
209 - commands << L"The following arguments are available:\r\n" //
210 - << L" network-name Network name\r\n" //
211 - << L"\r\n";
212 - return commands.str();
213 - }
214 -
215 - std::wstring GetAvailableOptions() const
216 - {
217 - std::wstringstream options;
218 - options << L"The following options are available:\r\n" //
219 - << L" -d,--driver Specify network driver name (default: bridge)\r\n" //
220 - << L" -o,--opt Set driver specific options\r\n" //
221 - << L" -l,--label Network metadata setting\r\n" //
222 - << L" --gateway IPv4 or IPv6 gateway for the subnet\r\n" //
223 - << L" --internal Restrict external access to the network\r\n" //
224 - << L" --subnet Subnet in CIDR format that represents a network segment\r\n" //
225 - << L" -?,--help Shows help about the selected command\r\n" //
226 - << L"\r\n";
227 - return options.str();
228 - }
188 };
189 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp
+4 -41
@@ -41,13 +41,15 @@ class WSLCE2ENetworkInspectTests
41 WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_HelpCommand)
42 {
43 auto result = RunWslc(L"network inspect --help");
44 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + result.Verify({.Stderr = L"", .ExitCode = 0});
45 + VERIFY_IS_FALSE(result.Stdout.value().empty());
46 }
47
48 WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_MissingNetworkName)
49 {
50 auto result = RunWslc(L"network inspect");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
51 + result.Verify({.Stdout = L"", .ExitCode = 1});
52 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'"));
53 }
54
55 WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_Success)
@@ -113,44 +115,5 @@ class WSLCE2ENetworkInspectTests
115 private:
116 const std::wstring TestNetworkName1 = L"wslc-e2e-network-inspect-1";
117 const std::wstring TestNetworkName2 = L"wslc-e2e-network-inspect-2";
116 -
117 - std::wstring GetHelpMessage() const
118 - {
119 - std::wstringstream output;
120 - output << GetWslcHeader() //
121 - << GetDescription() //
122 - << GetUsage() //
123 - << GetAvailableCommands() //
124 - << GetAvailableOptions();
125 - return output.str();
126 - }
127 -
128 - std::wstring GetDescription() const
129 - {
130 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkInspectLongDesc());
131 - }
132 -
133 - std::wstring GetUsage() const
134 - {
135 - return L"Usage: wslc network inspect [<options>] <network-name>\r\n\r\n";
136 - }
137 -
138 - std::wstring GetAvailableCommands() const
139 - {
140 - std::wstringstream commands;
141 - commands << L"The following arguments are available:\r\n" //
142 - << L" network-name Network name\r\n" //
143 - << L"\r\n";
144 - return commands.str();
145 - }
146 -
147 - std::wstring GetAvailableOptions() const
148 - {
149 - std::wstringstream options;
150 - options << L"The following options are available:\r\n" //
151 - << L" -?,--help Shows help about the selected command\r\n" //
152 - << L"\r\n";
153 - return options.str();
154 - }
118 };
119 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp
+5 -39
@@ -41,13 +41,16 @@ class WSLCE2ENetworkListTests
41 WSLC_TEST_METHOD(WSLCE2E_Network_List_HelpCommand)
42 {
43 auto result = RunWslc(L"network list --help");
44 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + result.Verify({.Stderr = L"", .ExitCode = 0});
45 + VERIFY_IS_FALSE(result.Stdout.value().empty());
46 }
47
48 WSLC_TEST_METHOD(WSLCE2E_Network_List_InvalidFormatOption)
49 {
50 auto result = RunWslc(L"network list --format invalid");
50 - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
51 + result.Verify({.Stdout = L"", .ExitCode = 1});
52 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
53 + L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
54 }
55
56 WSLC_TEST_METHOD(WSLCE2E_Network_List_QuietOption_OutputsNamesOnly)
@@ -92,42 +95,5 @@ class WSLCE2ENetworkListTests
95 private:
96 const std::wstring TestNetworkName = L"wslc-e2e-network-list";
97 const std::wstring TestNetworkName2 = L"wslc-e2e-network-list-2";
95 -
96 - std::wstring GetHelpMessage() const
97 - {
98 - std::wstringstream output;
99 - output << GetWslcHeader() //
100 - << GetDescription() //
101 - << GetUsage() //
102 - << GetAvailableCommandAliases() //
103 - << GetAvailableOptions();
104 - return output.str();
105 - }
106 -
107 - std::wstring GetDescription() const
108 - {
109 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkListLongDesc());
110 - }
111 -
112 - std::wstring GetUsage() const
113 - {
114 - return L"Usage: wslc network list [<options>]\r\n\r\n";
115 - }
116 -
117 - std::wstring GetAvailableCommandAliases() const
118 - {
119 - return L"The following command aliases are available: ls\r\n\r\n";
120 - }
121 -
122 - std::wstring GetAvailableOptions() const
123 - {
124 - std::wstringstream options;
125 - options << L"The following options are available:\r\n" //
126 - << L" --format Output formatting (json or table) (Default: table)\r\n" //
127 - << L" -q,--quiet Outputs the network names only\r\n" //
128 - << L" -?,--help Shows help about the selected command\r\n" //
129 - << L"\r\n";
130 - return options.str();
131 - }
98 };
99 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
+6 -33
@@ -46,7 +46,8 @@ class WSLCE2ENetworkPruneTests
46 WSLC_TEST_METHOD(WSLCE2E_Network_Prune_HelpCommand)
47 {
48 const auto result = RunWslc(L"network prune --help");
49 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
49 + result.Verify({.Stderr = L"", .ExitCode = 0});
50 + VERIFY_IS_FALSE(result.Stdout.value().empty());
51 }
52
53 WSLC_TEST_METHOD(WSLCE2E_Network_Prune_NoNetworks)
@@ -197,13 +198,15 @@ class WSLCE2ENetworkPruneTests
198 WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_MalformedValue)
199 {
200 const auto result = RunWslc(L"network prune --filter label");
200 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1});
201 + result.Verify({.Stdout = L"", .ExitCode = 1});
202 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
203 }
204
205 WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey)
206 {
207 const auto result = RunWslc(L"network prune --filter color=red");
206 - result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
208 + result.Verify({.Stdout = L"", .ExitCode = 1});
209 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG"));
210 }
211
212 private:
@@ -218,35 +221,5 @@ private:
221 EnsureNetworkDoesNotExist(TestNetworkName);
222 EnsureNetworkDoesNotExist(TestNetworkName2);
223 }
221 -
222 - std::wstring GetHelpMessage() const
223 - {
224 - std::wstringstream output;
225 - output << GetWslcHeader() //
226 - << GetDescription() //
227 - << GetUsage() //
228 - << GetAvailableOptions();
229 - return output.str();
230 - }
231 -
232 - std::wstring GetDescription() const
233 - {
234 - return Localization::WSLCCLI_NetworkPruneLongDesc() + L"\r\n\r\n";
235 - }
236 -
237 - std::wstring GetUsage() const
238 - {
239 - return L"Usage: wslc network prune [<options>]\r\n\r\n";
240 - }
241 -
242 - std::wstring GetAvailableOptions() const
243 - {
244 - std::wstringstream options;
245 - options << L"The following options are available:\r\n"
246 - << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
247 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
248 - << L"\r\n";
249 - return options.str();
250 - }
224 };
225 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp
+4 -48
@@ -40,13 +40,15 @@ class WSLCE2ENetworkRemoveTests
40 WSLC_TEST_METHOD(WSLCE2E_Network_Remove_HelpCommand)
41 {
42 auto result = RunWslc(L"network remove --help");
43 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
43 + result.Verify({.Stderr = L"", .ExitCode = 0});
44 + VERIFY_IS_FALSE(result.Stdout.value().empty());
45 }
46
47 WSLC_TEST_METHOD(WSLCE2E_Network_Remove_MissingNetworkName)
48 {
49 auto result = RunWslc(L"network remove");
49 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
50 + result.Verify({.Stdout = L"", .ExitCode = 1});
51 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'"));
52 }
53
54 WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Valid)
@@ -132,51 +134,5 @@ class WSLCE2ENetworkRemoveTests
134 private:
135 const std::wstring TestNetworkName = L"wslc-e2e-network-remove";
136 const std::wstring TestNetworkName2 = L"wslc-e2e-network-remove-2";
135 -
136 - std::wstring GetHelpMessage() const
137 - {
138 - std::wstringstream output;
139 - output << GetWslcHeader() //
140 - << GetDescription() //
141 - << GetUsage() //
142 - << GetAvailableCommandAliases() //
143 - << GetAvailableCommands() //
144 - << GetAvailableOptions();
145 - return output.str();
146 - }
147 -
148 - std::wstring GetDescription() const
149 - {
150 - return Localization::WSLCCLI_NetworkRemoveLongDesc() + L"\r\n\r\n";
151 - }
152 -
153 - std::wstring GetUsage() const
154 - {
155 - return L"Usage: wslc network remove [<options>] <network-name>\r\n\r\n";
156 - }
157 -
158 - std::wstring GetAvailableCommandAliases() const
159 - {
160 - return L"The following command aliases are available: delete rm\r\n\r\n";
161 - }
162 -
163 - std::wstring GetAvailableCommands() const
164 - {
165 - std::wstringstream commands;
166 - commands << L"The following arguments are available:\r\n" //
167 - << L" network-name Network name\r\n" //
168 - << L"\r\n";
169 - return commands.str();
170 - }
171 -
172 - std::wstring GetAvailableOptions() const
173 - {
174 - std::wstringstream options;
175 - options << L"The following options are available:\r\n" //
176 - << L" -f,--force Do not error if the network does not exist\r\n" //
177 - << L" -?,--help Shows help about the selected command\r\n" //
178 - << L"\r\n";
179 - return options.str();
180 - }
137 };
138 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
+18 -108
@@ -48,55 +48,64 @@ class WSLCE2ENetworkTests
48 WSLC_TEST_METHOD(WSLCE2E_Network_HelpCommand)
49 {
50 auto result = RunWslc(L"network --help");
51 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
51 + result.Verify({.Stderr = L"", .ExitCode = 0});
52 + VERIFY_IS_FALSE(result.Stdout.value().empty());
53 }
54
55 WSLC_TEST_METHOD(WSLCE2E_Network_NoSubcommand_ShowsHelp)
56 {
57 auto result = RunWslc(L"network");
57 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
58 + result.Verify({.Stderr = L"", .ExitCode = 0});
59 + VERIFY_IS_FALSE(result.Stdout.value().empty());
60 }
61
62 WSLC_TEST_METHOD(WSLCE2E_Network_InvalidCommand_DisplaysErrorMessage)
63 {
64 auto result = RunWslc(L"network INVALID_CMD");
63 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
65 + result.Verify({.Stdout = L"", .ExitCode = 1});
66 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
67 }
68
69 WSLC_TEST_METHOD(WSLCE2E_Network_Connect_HelpCommand)
70 {
71 auto result = RunWslc(L"network connect --help");
69 - result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"", .ExitCode = 0});
72 + result.Verify({.Stderr = L"", .ExitCode = 0});
73 + VERIFY_IS_FALSE(result.Stdout.value().empty());
74 }
75
76 WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_HelpCommand)
77 {
78 auto result = RunWslc(L"network disconnect --help");
75 - result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"", .ExitCode = 0});
79 + result.Verify({.Stderr = L"", .ExitCode = 0});
80 + VERIFY_IS_FALSE(result.Stdout.value().empty());
81 }
82
83 WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingNetworkName)
84 {
85 auto result = RunWslc(L"network connect");
81 - result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
86 + result.Verify({.Stdout = L"", .ExitCode = 1});
87 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'"));
88 }
89
90 WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingContainerId)
91 {
92 auto result = RunWslc(std::format(L"network connect {}", TestNetworkName));
87 - result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
93 + result.Verify({.Stdout = L"", .ExitCode = 1});
94 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
95 }
96
97 WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingNetworkName)
98 {
99 auto result = RunWslc(L"network disconnect");
93 - result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
100 + result.Verify({.Stdout = L"", .ExitCode = 1});
101 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'"));
102 }
103
104 WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingContainerId)
105 {
106 auto result = RunWslc(std::format(L"network disconnect {}", TestNetworkName));
99 - result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
107 + result.Verify({.Stdout = L"", .ExitCode = 1});
108 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
109 }
110
111 WSLC_TEST_METHOD(WSLCE2E_Network_Connect_Valid)
@@ -211,104 +220,5 @@ private:
220 const std::wstring WslcContainerName = L"wslc-e2e-network-connect-container";
221 const std::wstring TestNetworkName = L"wslc-e2e-network-connect";
222 const TestImage& DebianImage = DebianTestImage();
214 -
215 - std::wstring GetHelpMessage() const
216 - {
217 - std::wstringstream output;
218 - output << GetWslcHeader() //
219 - << GetDescription() //
220 - << GetUsage() //
221 - << GetAvailableCommands() //
222 - << GetAvailableOptions();
223 - return output.str();
224 - }
225 -
226 - std::wstring GetDescription() const
227 - {
228 - return Localization::WSLCCLI_NetworkCommandLongDesc() + L"\r\n\r\n";
229 - }
230 -
231 - std::wstring GetUsage() const
232 - {
233 - return L"Usage: wslc network [<command>] [<options>]\r\n\r\n";
234 - }
235 -
236 - std::wstring GetAvailableCommands() const
237 - {
238 - std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
239 - {L"create", Localization::WSLCCLI_NetworkCreateDesc()},
240 - {L"remove", Localization::WSLCCLI_NetworkRemoveDesc()},
241 - {L"inspect", Localization::WSLCCLI_NetworkInspectDesc()},
242 - {L"list", Localization::WSLCCLI_NetworkListDesc()},
243 - {L"prune", Localization::WSLCCLI_NetworkPruneDesc()},
244 - {L"connect", Localization::WSLCCLI_NetworkConnectDesc()},
245 - {L"disconnect", Localization::WSLCCLI_NetworkDisconnectDesc()},
246 - };
247 -
248 - size_t maxLen = 0;
249 - for (const auto& [name, _] : entries)
250 - {
251 - maxLen = (std::max)(maxLen, name.size());
252 - }
253 -
254 - std::wstringstream commands;
255 - commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
256 - for (const auto& [name, desc] : entries)
257 - {
258 - commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
259 - }
260 - commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
261 - return commands.str();
262 - }
263 -
264 - std::wstring GetAvailableOptions() const
265 - {
266 - std::wstringstream options;
267 - options << L"The following options are available:\r\n"
268 - << L" -?,--help Shows help about the selected command\r\n"
269 - << L"\r\n";
270 - return options.str();
271 - }
272 -
273 - std::wstring GetConnectHelpMessage() const
274 - {
275 - std::wstringstream output;
276 - output << GetWslcHeader() //
277 - << Localization::WSLCCLI_NetworkConnectLongDesc() + L"\r\n\r\n" //
278 - << L"Usage: wslc network connect [<options>] <network-name> <container-id>\r\n\r\n" //
279 - << GetSubcommandArguments() //
280 - << GetSubcommandOptions();
281 - return output.str();
282 - }
283 -
284 - std::wstring GetDisconnectHelpMessage() const
285 - {
286 - std::wstringstream output;
287 - output << GetWslcHeader() //
288 - << Localization::WSLCCLI_NetworkDisconnectLongDesc() + L"\r\n\r\n" //
289 - << L"Usage: wslc network disconnect [<options>] <network-name> <container-id>\r\n\r\n" //
290 - << GetSubcommandArguments() //
291 - << GetSubcommandOptions();
292 - return output.str();
293 - }
294 -
295 - std::wstring GetSubcommandArguments() const
296 - {
297 - std::wstringstream args;
298 - args << L"The following arguments are available:\r\n" //
299 - << L" network-name Network name\r\n" //
300 - << L" container-id Container ID\r\n" //
301 - << L"\r\n";
302 - return args.str();
303 - }
304 -
305 - std::wstring GetSubcommandOptions() const
306 - {
307 - std::wstringstream options;
308 - options << L"The following options are available:\r\n" //
309 - << L" -?,--help Shows help about the selected command\r\n" //
310 - << L"\r\n";
311 - return options.str();
312 - }
223 };
224 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp
+8 -81
@@ -28,25 +28,29 @@ class WSLCE2EPushPullTests
28 WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand)
29 {
30 auto result = RunWslc(L"image push --help");
31 - result.Verify({.Stdout = GetPushHelpMessage(), .Stderr = L"", .ExitCode = 0});
31 + result.Verify({.Stderr = L"", .ExitCode = 0});
32 + VERIFY_IS_FALSE(result.Stdout.value().empty());
33 }
34
35 WSLC_TEST_METHOD(WSLCE2E_Image_Push_RootAlias)
36 {
37 auto result = RunWslc(L"push --help");
37 - result.Verify({.Stdout = GetPushRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
38 + result.Verify({.Stderr = L"", .ExitCode = 0});
39 + VERIFY_IS_FALSE(result.Stdout.value().empty());
40 }
41
42 WSLC_TEST_METHOD(WSLCE2E_Image_Pull_HelpCommand)
43 {
44 auto result = RunWslc(L"image pull --help");
43 - result.Verify({.Stdout = GetPullHelpMessage(), .Stderr = L"", .ExitCode = 0});
45 + result.Verify({.Stderr = L"", .ExitCode = 0});
46 + VERIFY_IS_FALSE(result.Stdout.value().empty());
47 }
48
49 WSLC_TEST_METHOD(WSLCE2E_Image_Pull_RootAlias)
50 {
51 auto result = RunWslc(L"pull --help");
49 - result.Verify({.Stdout = GetPullRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
52 + result.Verify({.Stderr = L"", .ExitCode = 0});
53 + VERIFY_IS_FALSE(result.Stdout.value().empty());
54 }
55
56 WSLC_TEST_METHOD(WSLCE2E_Image_PushPull)
@@ -100,82 +104,5 @@ class WSLCE2EPushPullTests
104 L"access to the resource is denied\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n";
105 result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
106 }
103 -
104 -private:
105 - std::wstring GetPushHelpMessage() const
106 - {
107 - std::wstringstream output;
108 - output << GetWslcHeader() << GetPushDescription() << GetPushUsage() << GetAvailableArguments() << GetAvailableOptions();
109 - return output.str();
110 - }
111 -
112 - std::wstring GetPushRootAliasHelpMessage() const
113 - {
114 - std::wstringstream output;
115 - output << GetWslcHeader() << GetPushDescription() << GetPushRootUsage() << GetAvailableArguments() << GetAvailableOptions();
116 - return output.str();
117 - }
118 -
119 - std::wstring GetPullHelpMessage() const
120 - {
121 - std::wstringstream output;
122 - output << GetWslcHeader() << GetPullDescription() << GetPullUsage() << GetAvailableArguments() << GetAvailableOptions();
123 - return output.str();
124 - }
125 -
126 - std::wstring GetPullRootAliasHelpMessage() const
127 - {
128 - std::wstringstream output;
129 - output << GetWslcHeader() << GetPullDescription() << GetPullRootUsage() << GetAvailableArguments() << GetAvailableOptions();
130 - return output.str();
131 - }
132 -
133 - std::wstring GetPushDescription() const
134 - {
135 - return Localization::WSLCCLI_ImagePushLongDesc() + L"\r\n\r\n";
136 - }
137 -
138 - std::wstring GetPullDescription() const
139 - {
140 - return Localization::WSLCCLI_ImagePullLongDesc() + L"\r\n\r\n";
141 - }
142 -
143 - std::wstring GetPushUsage() const
144 - {
145 - return L"Usage: wslc image push [<options>] <image>\r\n\r\n";
146 - }
147 -
148 - std::wstring GetPushRootUsage() const
149 - {
150 - return L"Usage: wslc push [<options>] <image>\r\n\r\n";
151 - }
152 -
153 - std::wstring GetPullUsage() const
154 - {
155 - return L"Usage: wslc image pull [<options>] <image>\r\n\r\n";
156 - }
157 -
158 - std::wstring GetPullRootUsage() const
159 - {
160 - return L"Usage: wslc pull [<options>] <image>\r\n\r\n";
161 - }
162 -
163 - std::wstring GetAvailableArguments() const
164 - {
165 - std::wstringstream args;
166 - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
167 - << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n"
168 - << L"\r\n";
169 - return args.str();
170 - }
171 -
172 - std::wstring GetAvailableOptions() const
173 - {
174 - std::wstringstream options;
175 - options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
176 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
177 - << L"\r\n";
178 - return options.str();
179 - }
107 };
108 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp
+4 -77
@@ -108,13 +108,15 @@ class WSLCE2ERegistryTests
108 WSLC_TEST_METHOD(WSLCE2E_Registry_Login_HelpCommand)
109 {
110 auto result = RunWslc(L"registry login --help");
111 - result.Verify({.Stdout = GetLoginHelpMessage(), .Stderr = L"", .ExitCode = 0});
111 + result.Verify({.Stderr = L"", .ExitCode = 0});
112 + VERIFY_IS_FALSE(result.Stdout.value().empty());
113 }
114
115 WSLC_TEST_METHOD(WSLCE2E_Registry_Logout_HelpCommand)
116 {
117 auto result = RunWslc(L"registry logout --help");
117 - result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0});
118 + result.Verify({.Stderr = L"", .ExitCode = 0});
119 + VERIFY_IS_FALSE(result.Stdout.value().empty());
120 }
121
122 WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordAndStdinMutuallyExclusive)
@@ -212,80 +214,5 @@ class WSLCE2ERegistryTests
214 }
215 }
216 }
215 -
216 -private:
217 - std::wstring GetLoginHelpMessage() const
218 - {
219 - std::wstringstream output;
220 - output << GetWslcHeader() << GetLoginDescription() << GetLoginUsage() << GetLoginAvailableArguments() << GetLoginAvailableOptions();
221 - return output.str();
222 - }
223 -
224 - std::wstring GetLogoutHelpMessage() const
225 - {
226 - std::wstringstream output;
227 - output << GetWslcHeader() << GetLogoutDescription() << GetLogoutUsage() << GetLogoutAvailableArguments()
228 - << GetLogoutAvailableOptions();
229 - return output.str();
230 - }
231 -
232 - std::wstring GetLoginDescription() const
233 - {
234 - return Localization::WSLCCLI_LoginLongDesc() + L"\r\n\r\n";
235 - }
236 -
237 - std::wstring GetLogoutDescription() const
238 - {
239 - return Localization::WSLCCLI_LogoutLongDesc() + L"\r\n\r\n";
240 - }
241 -
242 - std::wstring GetLoginUsage() const
243 - {
244 - return L"Usage: wslc registry login [<options>] [<server>]\r\n\r\n";
245 - }
246 -
247 - std::wstring GetLogoutUsage() const
248 - {
249 - return L"Usage: wslc registry logout [<options>] [<server>]\r\n\r\n";
250 - }
251 -
252 - std::wstring GetLoginAvailableArguments() const
253 - {
254 - std::wstringstream args;
255 - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
256 - << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
257 - << L"\r\n";
258 - return args.str();
259 - }
260 -
261 - std::wstring GetLogoutAvailableArguments() const
262 - {
263 - std::wstringstream args;
264 - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
265 - << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
266 - << L"\r\n";
267 - return args.str();
268 - }
269 -
270 - std::wstring GetLoginAvailableOptions() const
271 - {
272 - std::wstringstream options;
273 - options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
274 - << L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n"
275 - << L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n"
276 - << L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n"
277 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
278 - << L"\r\n";
279 - return options.str();
280 - }
281 -
282 - std::wstring GetLogoutAvailableOptions() const
283 - {
284 - std::wstringstream options;
285 - options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
286 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
287 - << L"\r\n";
288 - return options.str();
289 - }
217 };
218 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp
+6 -45
@@ -40,7 +40,8 @@ class WSLCE2EVolumeCreateTests
40 WSLC_TEST_METHOD(WSLCE2E_Volume_Create_HelpCommand)
41 {
42 auto result = RunWslc(L"volume create --help");
43 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
43 + result.Verify({.Stderr = L"", .ExitCode = 0});
44 + VERIFY_IS_FALSE(result.Stdout.value().empty());
45 }
46
47 WSLC_TEST_METHOD(WSLCE2E_Volume_Create_EmptyName)
@@ -82,7 +83,8 @@ class WSLCE2EVolumeCreateTests
83 WSLC_TEST_METHOD(WSLCE2E_Volume_Create_Vhd_MissingOpts_Fail)
84 {
85 auto result = RunWslc(std::format(L"volume create --driver vhd {}", TestVolumeName));
85 - result.Verify({.Stderr = L"Missing required option: 'SizeBytes'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
86 + result.Verify({.Stdout = L"", .ExitCode = 1});
87 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Missing required option: 'SizeBytes'\r\nError code: E_INVALIDARG"));
88
89 VerifyVolumeIsNotListed(TestVolumeName);
90 }
@@ -91,7 +93,8 @@ class WSLCE2EVolumeCreateTests
93 {
94 auto result =
95 RunWslc(std::format(L"volume create --driver invalid_driver --opt SizeBytes={} {}", DefaultVolumeSizeBytes, TestVolumeName));
94 - result.Verify({.Stdout = L"", .Stderr = L"Unsupported volume type: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
96 + result.Verify({.Stdout = L"", .ExitCode = 1});
97 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unsupported volume type: 'invalid_driver'\r\nError code: E_INVALIDARG"));
98
99 VerifyVolumeIsNotListed(TestVolumeName);
100 }
@@ -111,47 +114,5 @@ class WSLCE2EVolumeCreateTests
114 private:
115 const std::wstring TestVolumeName = L"wslc-e2e-volume-create";
116 const int DefaultVolumeSizeBytes = 3 * 1024 * 1024;
114 -
115 - std::wstring GetHelpMessage() const
116 - {
117 - std::wstringstream output;
118 - output << GetWslcHeader() //
119 - << GetDescription() //
120 - << GetUsage() //
121 - << GetAvailableCommands() //
122 - << GetAvailableOptions();
123 - return output.str();
124 - }
125 -
126 - std::wstring GetDescription() const
127 - {
128 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeCreateLongDesc());
129 - }
130 -
131 - std::wstring GetUsage() const
132 - {
133 - return L"Usage: wslc volume create [<options>] [<volume-name>]\r\n\r\n";
134 - }
135 -
136 - std::wstring GetAvailableCommands() const
137 - {
138 - std::wstringstream commands;
139 - commands << L"The following arguments are available:\r\n" //
140 - << L" volume-name Volume name\r\n" //
141 - << L"\r\n";
142 - return commands.str();
143 - }
144 -
145 - std::wstring GetAvailableOptions() const
146 - {
147 - std::wstringstream options;
148 - options << L"The following options are available:\r\n" //
149 - << L" -d,--driver Specify volume driver name, e.g. 'guest' or 'vhd' (default: guest)\r\n" //
150 - << L" -o,--opt Set driver specific options\r\n" //
151 - << L" -l,--label Set metadata on an object\r\n" //
152 - << L" -?,--help Shows help about the selected command\r\n" //
153 - << L"\r\n";
154 - return options.str();
155 - }
117 };
118 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp
+4 -41
@@ -41,13 +41,15 @@ class WSLCE2EVolumeInspectTests
41 WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_HelpCommand)
42 {
43 auto result = RunWslc(L"volume inspect --help");
44 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
44 + result.Verify({.Stderr = L"", .ExitCode = 0});
45 + VERIFY_IS_FALSE(result.Stdout.value().empty());
46 }
47
48 WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_MissingVolumeName)
49 {
50 auto result = RunWslc(L"volume inspect");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1});
51 + result.Verify({.Stdout = L"", .ExitCode = 1});
52 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'volume-name'"));
53 }
54
55 WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_Success)
@@ -121,44 +123,5 @@ class WSLCE2EVolumeInspectTests
123 private:
124 const std::wstring TestVolumeName1 = L"wslc-e2e-volume-inspect-1";
125 const std::wstring TestVolumeName2 = L"wslc-e2e-volume-inspect-2";
124 -
125 - std::wstring GetHelpMessage() const
126 - {
127 - std::wstringstream output;
128 - output << GetWslcHeader() //
129 - << GetDescription() //
130 - << GetUsage() //
131 - << GetAvailableCommands() //
132 - << GetAvailableOptions();
133 - return output.str();
134 - }
135 -
136 - std::wstring GetDescription() const
137 - {
138 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeInspectLongDesc());
139 - }
140 -
141 - std::wstring GetUsage() const
142 - {
143 - return L"Usage: wslc volume inspect [<options>] <volume-name>\r\n\r\n";
144 - }
145 -
146 - std::wstring GetAvailableCommands() const
147 - {
148 - std::wstringstream commands;
149 - commands << L"The following arguments are available:\r\n" //
150 - << L" volume-name Volume name\r\n" //
151 - << L"\r\n";
152 - return commands.str();
153 - }
154 -
155 - std::wstring GetAvailableOptions() const
156 - {
157 - std::wstringstream options;
158 - options << L"The following options are available:\r\n" //
159 - << L" -?,--help Shows help about the selected command\r\n" //
160 - << L"\r\n";
161 - return options.str();
162 - }
126 };
127 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp
+5 -39
@@ -43,13 +43,16 @@ class WSLCE2EVolumeListTests
43 WSLC_TEST_METHOD(WSLCE2E_Volume_List_HelpCommand)
44 {
45 auto result = RunWslc(L"volume list --help");
46 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
46 + result.Verify({.Stderr = L"", .ExitCode = 0});
47 + VERIFY_IS_FALSE(result.Stdout.value().empty());
48 }
49
50 WSLC_TEST_METHOD(WSLCE2E_Volume_List_InvalidFormatOption)
51 {
52 auto result = RunWslc(L"volume list --format invalid");
52 - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
53 + result.Verify({.Stdout = L"", .ExitCode = 1});
54 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
55 + L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
56 }
57
58 WSLC_TEST_METHOD(WSLCE2E_Volume_List_QuietOption_OutputsNamesOnly)
@@ -94,42 +97,5 @@ class WSLCE2EVolumeListTests
97 private:
98 const std::wstring TestVolumeName = L"wslc-e2e-volume-list";
99 const std::wstring TestVolumeName2 = L"wslc-e2e-volume-list-2";
97 -
98 - std::wstring GetHelpMessage() const
99 - {
100 - std::wstringstream output;
101 - output << GetWslcHeader() //
102 - << GetDescription() //
103 - << GetUsage() //
104 - << GetAvailableCommandAliases() //
105 - << GetAvailableOptions();
106 - return output.str();
107 - }
108 -
109 - std::wstring GetDescription() const
110 - {
111 - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeListLongDesc());
112 - }
113 -
114 - std::wstring GetUsage() const
115 - {
116 - return L"Usage: wslc volume list [<options>]\r\n\r\n";
117 - }
118 -
119 - std::wstring GetAvailableCommandAliases() const
120 - {
121 - return L"The following command aliases are available: ls\r\n\r\n";
122 - }
123 -
124 - std::wstring GetAvailableOptions() const
125 - {
126 - std::wstringstream options;
127 - options << L"The following options are available:\r\n" //
128 - << L" --format Output formatting (json or table) (Default: table)\r\n"
129 - << L" -q,--quiet Outputs the volume names only\r\n" //
130 - << L" -?,--help Shows help about the selected command\r\n" //
131 - << L"\r\n";
132 - return options.str();
133 - }
100 };
101 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
+6 -34
@@ -46,7 +46,8 @@ class WSLCE2EVolumePruneTests
46 WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_HelpCommand)
47 {
48 const auto result = RunWslc(L"volume prune --help");
49 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
49 + result.Verify({.Stderr = L"", .ExitCode = 0});
50 + VERIFY_IS_FALSE(result.Stdout.value().empty());
51 }
52
53 WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoVolumes)
@@ -216,13 +217,15 @@ class WSLCE2EVolumePruneTests
217 WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_MalformedValue)
218 {
219 const auto result = RunWslc(L"volume prune --filter label");
219 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1});
220 + result.Verify({.Stdout = L"", .ExitCode = 1});
221 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
222 }
223
224 WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey)
225 {
226 const auto result = RunWslc(L"volume prune --filter color=red");
225 - result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
227 + result.Verify({.Stdout = L"", .ExitCode = 1});
228 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG"));
229 }
230
231 private:
@@ -237,36 +240,5 @@ private:
240 EnsureVolumeDoesNotExist(TestVolumeName);
241 EnsureVolumeDoesNotExist(TestVolumeName2);
242 }
240 -
241 - std::wstring GetHelpMessage() const
242 - {
243 - std::wstringstream output;
244 - output << GetWslcHeader() //
245 - << GetDescription() //
246 - << GetUsage() //
247 - << GetAvailableOptions();
248 - return output.str();
249 - }
250 -
251 - std::wstring GetDescription() const
252 - {
253 - return Localization::WSLCCLI_VolumePruneLongDesc() + L"\r\n\r\n";
254 - }
255 -
256 - std::wstring GetUsage() const
257 - {
258 - return L"Usage: wslc volume prune [<options>]\r\n\r\n";
259 - }
260 -
261 - std::wstring GetAvailableOptions() const
262 - {
263 - std::wstringstream options;
264 - options << L"The following options are available:\r\n"
265 - << L" -a,--all " << Localization::WSLCCLI_VolumePruneAllArgDescription() << L"\r\n"
266 - << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
267 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
268 - << L"\r\n";
269 - return options.str();
270 - }
243 };
244 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp
+4 -48
@@ -49,13 +49,15 @@ class WSLCE2EVolumeRemoveTests
49 WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_HelpCommand)
50 {
51 auto result = RunWslc(L"volume remove --help");
52 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
52 + result.Verify({.Stderr = L"", .ExitCode = 0});
53 + VERIFY_IS_FALSE(result.Stdout.value().empty());
54 }
55
56 WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_MissingVolumeName)
57 {
58 auto result = RunWslc(L"volume remove");
58 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1});
59 + result.Verify({.Stdout = L"", .ExitCode = 1});
60 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'volume-name'"));
61 }
62
63 WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Valid)
@@ -191,51 +193,5 @@ private:
193 const TestImage& DebianImage = DebianTestImage();
194 const std::wstring TestVolumeName = L"wslc-e2e-volume-remove";
195 const std::wstring TestVolumeName2 = L"wslc-e2e-volume-remove-2";
194 -
195 - std::wstring GetHelpMessage() const
196 - {
197 - std::wstringstream output;
198 - output << GetWslcHeader() //
199 - << GetDescription() //
200 - << GetUsage() //
201 - << GetAvailableCommandAliases() //
202 - << GetAvailableCommands() //
203 - << GetAvailableOptions();
204 - return output.str();
205 - }
206 -
207 - std::wstring GetDescription() const
208 - {
209 - return Localization::WSLCCLI_VolumeRemoveLongDesc() + L"\r\n\r\n";
210 - }
211 -
212 - std::wstring GetUsage() const
213 - {
214 - return L"Usage: wslc volume remove [<options>] <volume-name>\r\n\r\n";
215 - }
216 -
217 - std::wstring GetAvailableCommandAliases() const
218 - {
219 - return L"The following command aliases are available: delete rm\r\n\r\n";
220 - }
221 -
222 - std::wstring GetAvailableCommands() const
223 - {
224 - std::wstringstream commands;
225 - commands << L"The following arguments are available:\r\n" //
226 - << L" volume-name Volume name\r\n" //
227 - << L"\r\n";
228 - return commands.str();
229 - }
230 -
231 - std::wstring GetAvailableOptions() const
232 - {
233 - std::wstringstream options;
234 - options << L"The following options are available:\r\n" //
235 - << L" -f,--force Do not error if the volume does not exist\r\n" //
236 - << L" -?,--help Shows help about the selected command\r\n" //
237 - << L"\r\n";
238 - return options.str();
239 - }
196 };
197 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp
+6 -60
@@ -27,76 +27,22 @@ class WSLCE2EVolumeTests
27 WSLC_TEST_METHOD(WSLCE2E_Volume_HelpCommand)
28 {
29 auto result = RunWslc(L"volume --help");
30 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
30 + result.Verify({.Stderr = L"", .ExitCode = 0});
31 + VERIFY_IS_FALSE(result.Stdout.value().empty());
32 }
33
34 WSLC_TEST_METHOD(WSLCE2E_Volume_NoSubcommand_ShowsHelp)
35 {
36 auto result = RunWslc(L"volume");
36 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
37 + result.Verify({.Stderr = L"", .ExitCode = 0});
38 + VERIFY_IS_FALSE(result.Stdout.value().empty());
39 }
40
41 WSLC_TEST_METHOD(WSLCE2E_Volume_InvalidCommand_DisplaysErrorMessage)
42 {
43 auto result = RunWslc(L"volume INVALID_CMD");
42 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
43 - }
44 -
45 -private:
46 - std::wstring GetHelpMessage() const
47 - {
48 - std::wstringstream output;
49 - output << GetWslcHeader() //
50 - << GetDescription() //
51 - << GetUsage() //
52 - << GetAvailableCommands() //
53 - << GetAvailableOptions();
54 - return output.str();
55 - }
56 -
57 - std::wstring GetDescription() const
58 - {
59 - return Localization::WSLCCLI_VolumeCommandLongDesc() + L"\r\n\r\n";
60 - }
61 -
62 - std::wstring GetUsage() const
63 - {
64 - return L"Usage: wslc volume [<command>] [<options>]\r\n\r\n";
65 - }
66 -
67 - std::wstring GetAvailableCommands() const
68 - {
69 - std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
70 - {L"create", Localization::WSLCCLI_VolumeCreateDesc()},
71 - {L"remove", Localization::WSLCCLI_VolumeRemoveDesc()},
72 - {L"inspect", Localization::WSLCCLI_VolumeInspectDesc()},
73 - {L"list", Localization::WSLCCLI_VolumeListDesc()},
74 - {L"prune", Localization::WSLCCLI_VolumePruneDesc()},
75 - };
76 -
77 - size_t maxLen = 0;
78 - for (const auto& [name, _] : entries)
79 - {
80 - maxLen = (std::max)(maxLen, name.size());
81 - }
82 -
83 - std::wstringstream commands;
84 - commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
85 - for (const auto& [name, desc] : entries)
86 - {
87 - commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
88 - }
89 - commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
90 - return commands.str();
91 - }
92 -
93 - std::wstring GetAvailableOptions() const
94 - {
95 - std::wstringstream options;
96 - options << L"The following options are available:\r\n"
97 - << L" -?,--help Shows help about the selected command\r\n"
98 - << L"\r\n";
99 - return options.str();
44 + result.Verify({.Stdout = L"", .ExitCode = 1});
45 + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
46 }
47 };
48 } // namespace WSLCE2ETests
\ No newline at end of file
test/windows/wslc/e2e/WSLCExecutor.cpp
+6 -9
@@ -147,6 +147,12 @@ bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring)
147 return Stdout.value().find(substring) != std::wstring::npos;
148 }
149
150 +bool WSLCExecutionResult::StderrContainsSubstring(const std::wstring& substring) const
151 +{
152 + VERIFY_IS_TRUE(Stderr.has_value());
153 + return Stderr.value().find(substring) != std::wstring::npos;
154 +}
155 +
156 WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType, HANDLE stdinHandle)
157 {
158 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
@@ -278,15 +284,6 @@ void WaitForContainerOutput(const std::wstring& containerName, std::string_view
284 WaitForOutput(wil::unique_handle{parentStdoutRead.release()}, expected, timeout);
285 }
286
281 -std::wstring GetWslcHeader()
282 -{
283 - std::wstringstream header;
284 - header << L"Copyright (c) Microsoft Corporation. All rights reserved.\r\n"
285 - << L"For privacy information about this product please visit https://aka.ms/privacy.\r\n"
286 - << L"\r\n";
287 - return header.str();
288 -}
289 -
287 WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType, std::optional<PseudoConsole> pseudoConsole)
288 {
289 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
test/windows/wslc/e2e/WSLCExecutor.h
+1 -1
@@ -46,6 +46,7 @@ struct WSLCExecutionResult
46 std::wstring GetStdoutOneLine() const;
47 bool StdoutContainsLine(const std::wstring& expectedLine) const;
48 bool StdoutContainsSubstring(const std::wstring& substring) const;
49 + bool StderrContainsSubstring(const std::wstring& substring) const;
50 };
51
52 struct PseudoConsole
@@ -135,7 +136,6 @@ WSLCExecutionResult RunWslcWithStdinFile(
136 const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType = ElevationType::Elevated);
137 void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated);
138
138 -std::wstring GetWslcHeader();
139 WSLCInteractiveSession RunWslcInteractive(
140 const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, std::optional<PseudoConsole> pseudoConsole = std::nullopt);
141