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