diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index 3ca27cf4ad..a1808c7182 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2195,6 +2195,21 @@ Usage:
For more details on a specific command, pass it the help argument.
+
+ Commands:
+
+
+ Options:
+
+
+ Global Options:
+
+
+ Aliases:
+
+
+ Arguments:
+
Argument name was not recognized for the current command: '{}'
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/windows/wslc/commands/ContainerCommand.cpp b/src/windows/wslc/commands/ContainerCommand.cpp
index 9d5002f3f1..d115b5ca13 100644
--- a/src/windows/wslc/commands/ContainerCommand.cpp
+++ b/src/windows/wslc/commands/ContainerCommand.cpp
@@ -56,6 +56,6 @@ std::wstring ContainerCommand::LongDescription() const
void ContainerCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/ImageCommand.cpp b/src/windows/wslc/commands/ImageCommand.cpp
index 99507f695d..6b4ce97db4 100644
--- a/src/windows/wslc/commands/ImageCommand.cpp
+++ b/src/windows/wslc/commands/ImageCommand.cpp
@@ -53,6 +53,6 @@ std::wstring ImageCommand::LongDescription() const
void ImageCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
-} // namespace wsl::windows::wslc
\ No newline at end of file
+} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/NetworkCommand.cpp b/src/windows/wslc/commands/NetworkCommand.cpp
index 94146efa38..89bd3c063b 100644
--- a/src/windows/wslc/commands/NetworkCommand.cpp
+++ b/src/windows/wslc/commands/NetworkCommand.cpp
@@ -47,6 +47,6 @@ std::wstring NetworkCommand::LongDescription() const
void NetworkCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp
index 30cfb8e755..9c16513472 100644
--- a/src/windows/wslc/commands/RegistryCommand.cpp
+++ b/src/windows/wslc/commands/RegistryCommand.cpp
@@ -85,7 +85,7 @@ std::wstring RegistryCommand::LongDescription() const
void RegistryCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
// Registry Login Command
diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp
index a972e996c9..b134c4d459 100644
--- a/src/windows/wslc/commands/RootCommand.cpp
+++ b/src/windows/wslc/commands/RootCommand.cpp
@@ -109,6 +109,6 @@ void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
return;
}
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/SessionCommand.cpp b/src/windows/wslc/commands/SessionCommand.cpp
index 02af727357..43dec52e54 100644
--- a/src/windows/wslc/commands/SessionCommand.cpp
+++ b/src/windows/wslc/commands/SessionCommand.cpp
@@ -48,6 +48,6 @@ std::wstring SessionCommand::LongDescription() const
void SessionCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/SystemCommand.cpp b/src/windows/wslc/commands/SystemCommand.cpp
index b073573bd6..ed58665e2e 100644
--- a/src/windows/wslc/commands/SystemCommand.cpp
+++ b/src/windows/wslc/commands/SystemCommand.cpp
@@ -43,6 +43,6 @@ std::wstring SystemCommand::LongDescription() const
void SystemCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/VolumeCommand.cpp b/src/windows/wslc/commands/VolumeCommand.cpp
index dd3ded2980..f6e00ef299 100644
--- a/src/windows/wslc/commands/VolumeCommand.cpp
+++ b/src/windows/wslc/commands/VolumeCommand.cpp
@@ -47,6 +47,6 @@ std::wstring VolumeCommand::LongDescription() const
void VolumeCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/core/Command.cpp b/src/windows/wslc/core/Command.cpp
index 5e320ee4d3..e37c029e6c 100644
--- a/src/windows/wslc/core/Command.cpp
+++ b/src/windows/wslc/core/Command.cpp
@@ -15,13 +15,18 @@ Module Name:
#include "Command.h"
#include "Invocation.h"
#include "ArgumentParser.h"
+#include "RootCommand.h"
+#include "TableOutput.h"
using namespace wsl::shared;
using namespace wsl::windows::common::wslutil;
+using namespace wsl::windows::common::vt;
using namespace wsl::windows::wslc::execution;
namespace wsl::windows::wslc {
+std::wstring s_ExecutableName = L"wslc";
+
Command::Command(std::wstring_view name, std::vector&& aliases, const std::wstring& parent) :
m_name(name), m_aliases(std::move(aliases))
{
@@ -38,33 +43,32 @@ Command::Command(std::wstring_view name, std::vector&& aliase
}
}
-// This is the header applied before every help output.
-// It is separate in case we need to show it in other contexts, such as error messages, or
-// during specific command executions.
-void Command::OutputIntroHeader() const
+void Command::OutputHelp(Reporter& reporter, const CommandException* exception) const
{
- std::wostringstream infoOut;
- infoOut << Localization::WSLCCLI_CopyrightHeader() << std::endl;
- PrintMessage(infoOut.str(), stdout);
-}
+ constexpr size_t c_helpRowIndent = 2;
+ constexpr size_t c_helpColumnPadding = 2;
+ const auto helpLevel = exception ? Reporter::Level::Info : Reporter::Level::Output;
-void Command::OutputHelp(const CommandException* exception) const
-{
- // Header
- OutputIntroHeader();
+ // Emphasis sequences for help output.
+ static const auto& HelpHeadingEmphasis = Format::Bright;
+ static const auto& HelpCommandEmphasis = Format::Bright;
+ static const auto& HelpArgumentEmphasis = Format::Bright;
+ static const auto& HelpMetaEmphasis = Format::Dim;
+ static const auto& HelpPlaceholderEmphasis = Format::Fg::BrightCyan;
+
+ // Copyright header (dimmed)
+ reporter.Write(helpLevel, L"{}{}{}\n\n", HelpMetaEmphasis, Localization::WSLCCLI_CopyrightHeader(), Format::Default);
// Error if given
if (exception)
{
- PrintMessage(exception->Message(), stderr);
+ reporter.Error(L"{}\n\n", exception->Message());
}
// Description
- std::wostringstream infoOut;
- infoOut << LongDescription() << std::endl << std::endl;
+ reporter.Write(helpLevel, L"{}\n\n", LongDescription());
- // Example usage for this command
- // First create the command chain for output
+ // Build command chain from full name (replace ParentSplitChar with spaces, strip root).
std::wstring commandChain = FullName();
size_t firstSplit = commandChain.find_first_of(ParentSplitChar);
if (firstSplit == std::wstring::npos)
@@ -83,37 +87,23 @@ void Command::OutputHelp(const CommandException* exception) const
}
}
- // Usage follows the Microsoft convention:
- // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
-
- // Output the command preamble and command chain
- infoOut << Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
-
auto commandAliases = Aliases();
auto commands = GetCommands();
auto arguments = GetAllArguments();
- // Separate arguments by Kind
std::vector standardArgs;
std::vector positionalArgs;
std::vector forwardArgs;
- bool requiredPositionalArgsExist = false;
for (const auto& arg : arguments)
{
switch (arg.Kind())
{
case Kind::Flag:
- standardArgs.emplace_back(arg);
- break;
case Kind::Value:
standardArgs.emplace_back(arg);
break;
case Kind::Positional:
positionalArgs.emplace_back(arg);
- if (arg.Required())
- {
- requiredPositionalArgsExist = true;
- }
break;
case Kind::Forward:
forwardArgs.emplace_back(arg);
@@ -121,154 +111,244 @@ void Command::OutputHelp(const CommandException* exception) const
}
}
- bool hasArguments = !positionalArgs.empty();
- bool hasOptions = !standardArgs.empty();
- bool hasForwardArgs = !forwardArgs.empty();
+ const bool hasArguments = !positionalArgs.empty();
+ const bool hasOptions = !standardArgs.empty();
+ const bool hasForwardArgs = !forwardArgs.empty();
- // Output the command token, made optional if arguments are present.
- if (!commands.empty())
+ // Global options from the root command, shown on every command's help.
+ auto globalArgs = RootCommand().GetGlobalArguments();
+
+ // Build usage line with Write calls for each segment.
{
- infoOut << ' ';
+ std::wstring usageText = Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
+ reporter.Write(helpLevel, L"{}{}{}", HelpHeadingEmphasis, usageText, Format::Default);
- if (!arguments.empty())
+ if (!commands.empty())
{
- infoOut << L'[';
+ if (!arguments.empty())
+ {
+ reporter.Write(helpLevel, L" {}[{}", HelpMetaEmphasis, Format::Default);
+ }
+ else
+ {
+ reporter.Write(helpLevel, L" ");
+ }
+ reporter.Write(
+ helpLevel,
+ L"{}<{}{}{}{}{}>{}",
+ HelpMetaEmphasis,
+ Format::Default,
+ HelpPlaceholderEmphasis,
+ Localization::WSLCCLI_Command(),
+ Format::Default,
+ HelpMetaEmphasis,
+ Format::Default);
+ if (!arguments.empty())
+ {
+ reporter.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
+ }
}
- infoOut << L'<' << Localization::WSLCCLI_Command() << L'>';
-
- if (!arguments.empty())
+ if (hasOptions)
{
- infoOut << L']';
+ reporter.Write(
+ helpLevel,
+ L" {}[<{}{}{}{}{}>]{}",
+ HelpMetaEmphasis,
+ Format::Default,
+ HelpPlaceholderEmphasis,
+ Localization::WSLCCLI_Options(),
+ Format::Default,
+ HelpMetaEmphasis,
+ Format::Default);
}
- }
-
- // For WSLC format of command []
-
- // Add options to the usage if there are options present.
- if (hasOptions)
- {
- infoOut << L" [<" << Localization::WSLCCLI_Options() << L">]";
- }
-
- // Add arguments to the usage if there are arguments present. Positional come after
- // options and may be optional or required.
- for (const auto& arg : positionalArgs)
- {
- infoOut << L' ';
- if (!arg.Required())
+ for (const auto& arg : positionalArgs)
{
- infoOut << L'[';
+ reporter.Write(helpLevel, L" ");
+ if (!arg.Required())
+ {
+ reporter.Write(helpLevel, L"{}[{}", HelpMetaEmphasis, Format::Default);
+ }
+ reporter.Write(
+ helpLevel, L"{}<{}{}{}{}{}>{}", HelpMetaEmphasis, Format::Default, HelpPlaceholderEmphasis, arg.Name(), Format::Default, HelpMetaEmphasis, Format::Default);
+ if (arg.Limit() > 1)
+ {
+ reporter.Write(helpLevel, L"{}...{}", HelpMetaEmphasis, Format::Default);
+ }
+ if (!arg.Required())
+ {
+ reporter.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
+ }
}
- infoOut << L'<' << arg.Name() << L'>';
-
- if (arg.Limit() > 1)
+ if (hasForwardArgs)
{
- infoOut << L"...";
+ reporter.Write(
+ helpLevel,
+ L" {}[<{}{}{}{}{}>...]{}",
+ HelpMetaEmphasis,
+ Format::Default,
+ HelpPlaceholderEmphasis,
+ forwardArgs.front().Name(),
+ Format::Default,
+ HelpMetaEmphasis,
+ Format::Default);
}
- if (!arg.Required())
- {
- infoOut << L']';
- }
+ reporter.Write(helpLevel, L"\n\n");
}
- if (hasForwardArgs)
- {
- // Assume only one forward arg is present, as multiple forwards would be
- // ambiguous in usage. Revisit if this becomes a scenario.
- infoOut << L" [<" << forwardArgs.front().Name() << L">...]";
- }
-
- infoOut << std::endl << std::endl;
-
if (!commandAliases.empty())
{
- infoOut << Localization::WSLCCLI_AvailableCommandAliases() << L' ';
- infoOut << string::Join(commandAliases, L' ');
- infoOut << std::endl << std::endl;
- }
+ reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingAliases(), Format::Default);
- if (!commands.empty())
- {
- if (Name() == FullName())
- {
- infoOut << Localization::WSLCCLI_AvailableCommands() << std::endl;
- }
- else
+ std::wstring aliasLine;
+ for (size_t i = 0; i < commandAliases.size(); ++i)
{
- infoOut << Localization::WSLCCLI_AvailableSubcommands() << std::endl;
+ if (i != 0)
+ {
+ aliasLine += L", ";
+ }
+ aliasLine += commandAliases[i];
}
+ reporter.Write(helpLevel, L"{}{}\n\n", std::wstring(c_helpRowIndent, L' '), aliasLine);
+ }
- size_t maxCommandNameLength = 0;
- for (const auto& command : commands)
- {
- maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length());
- }
+ // Col0: name/command
+ // Col1: description (word-wraps at computed column width)
+ const auto MakeHelpTable = [&reporter, helpLevel]() -> TableOutput<2> {
+ TableOutput<2> table{reporter, {L"", L""}, 50, c_helpColumnPadding, helpLevel};
+ table.SetShowHeader(false);
+ table.SetRowIndent(c_helpRowIndent);
+ table.SetColumnConfig(
+ 1,
+ ColumnWidthConfig{
+ .MinWidth = ColumnWidthConfig::NoLimit,
+ .MaxWidth = ColumnWidthConfig::NoLimit,
+ .Overflow = ColumnOverflow::Wrap,
+ });
+ return table;
+ };
+ if (!commands.empty())
+ {
+ reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingCommands(), Format::Default);
+
+ auto table = MakeHelpTable();
for (const auto& command : commands)
{
- size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2;
- infoOut << L" " << command->Name() << std::wstring(fillChars, L' ') << command->ShortDescription() << std::endl;
+ table.WriteRow({
+ FormattedCell(command->Name(), HelpCommandEmphasis),
+ FormattedCell(command->ShortDescription()),
+ });
}
+ table.Complete();
- infoOut << std::endl << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L']' << std::endl;
+ reporter.Write(helpLevel, L"\n{} [{}]\n", Localization::WSLCCLI_HelpForDetails(), WSLC_CLI_HELP_ARG_STRING);
}
if (!arguments.empty())
{
if (!commands.empty())
{
- infoOut << std::endl;
+ reporter.Write(helpLevel, L"\n");
}
- size_t maxArgNameLength = 0;
- for (const auto& arg : arguments)
+ // Arguments table: positional and forward args, name (emphasized) | description
+ if (hasArguments || hasForwardArgs)
{
- auto argLength = arg.GetUsageString().length();
- maxArgNameLength = std::max(maxArgNameLength, argLength);
- }
+ reporter.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingArguments(), Format::Default);
- if (hasArguments)
- {
- infoOut << Localization::WSLCCLI_AvailableArguments() << std::endl;
+ auto table = MakeHelpTable();
for (const auto& arg : positionalArgs)
{
- size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
- infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
+ table.WriteRow({
+ FormattedCell(arg.Name(), HelpArgumentEmphasis),
+ FormattedCell(arg.Description()),
+ });
}
- }
- if (hasForwardArgs)
- {
for (const auto& arg : forwardArgs)
{
- size_t fillChars = (maxArgNameLength - arg.Name().length()) + 2;
- infoOut << L" " << arg.Name() << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
+ table.WriteRow({
+ FormattedCell(arg.Name(), HelpArgumentEmphasis),
+ FormattedCell(arg.Description()),
+ });
}
+
+ table.Complete();
}
+ }
- if (hasOptions)
+ // Col0: short alias (e.g. "-f")
+ // Col1: long name (e.g. "--force")
+ // Col2: description (word-wraps at computed column width)
+ const auto MakeOptionsTable = [&reporter, helpLevel]() -> TableOutput<3> {
+ TableOutput<3> table{reporter, {L"", L"", L""}, {}, 50, c_helpColumnPadding, helpLevel};
+ table.SetShowHeader(false);
+ table.SetRowIndent(c_helpRowIndent);
+ table.SetColumnConfig(
+ 2,
+ ColumnWidthConfig{
+ .MinWidth = ColumnWidthConfig::NoLimit,
+ .MaxWidth = ColumnWidthConfig::NoLimit,
+ .Overflow = ColumnOverflow::Wrap,
+ });
+ return table;
+ };
+
+ // Options table: alias (emphasized) | long name (emphasized) | description
+ // Global options are appended to the same table so column widths are shared.
+ if (hasOptions || !globalArgs.empty())
+ {
+ if (hasArguments || hasForwardArgs)
{
- if (hasArguments || hasForwardArgs)
+ reporter.Write(helpLevel, L"\n");
+ }
+ else if (!commands.empty() && arguments.empty())
+ {
+ reporter.Write(helpLevel, L"\n");
+ }
+
+ auto table = MakeOptionsTable();
+
+ const auto AddOptionRows = [&table](const std::vector& args) {
+ for (const auto& arg : args)
{
- infoOut << std::endl;
+ FormattedCell aliasCell{L""};
+ if (!arg.Alias().empty())
+ {
+ aliasCell = FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Alias(), HelpArgumentEmphasis);
+ }
+
+ table.WriteRow({
+ std::move(aliasCell),
+ FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Name(), HelpArgumentEmphasis),
+ FormattedCell(arg.Description()),
+ });
}
+ };
- infoOut << Localization::WSLCCLI_AvailableOptions() << std::endl;
- for (const auto& arg : standardArgs)
+ if (hasOptions)
+ {
+ table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingOptions(), HelpHeadingEmphasis));
+ AddOptionRows(standardArgs);
+ }
+
+ if (!globalArgs.empty())
+ {
+ if (hasOptions)
{
- auto usage = arg.GetUsageString();
- size_t fillChars = (maxArgNameLength - usage.length()) + 2;
- infoOut << L" " << usage << std::wstring(fillChars, ' ') << arg.Description() << std::endl;
+ table.WriteLine();
}
+ table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingGlobalOptions(), HelpHeadingEmphasis));
+ AddOptionRows(globalArgs);
}
- }
- PrintMessage(infoOut.str(), stdout);
+ table.Complete();
+ }
}
std::unique_ptr Command::FindSubCommand(Invocation& inv) const
@@ -375,7 +455,7 @@ void Command::Execute(CLIExecutionContext& context) const
// If Help was part of the validated argument set, we will output help instead of executing.
if (context.Args.Contains(ArgType::Help))
{
- OutputHelp();
+ OutputHelp(context.Reporter);
}
else
{
diff --git a/src/windows/wslc/core/Command.h b/src/windows/wslc/core/Command.h
index ed2d38d21b..bb9531d24f 100644
--- a/src/windows/wslc/core/Command.h
+++ b/src/windows/wslc/core/Command.h
@@ -18,6 +18,7 @@ Module Name:
#include "CLIExecutionContext.h"
#include "Invocation.h"
#include "ArgumentParser.h"
+#include "Reporter.h"
#include
#include
@@ -30,7 +31,8 @@ using namespace wsl::windows::wslc::argument;
namespace wsl::windows::wslc {
-constexpr std::wstring_view s_ExecutableName = L"wslc";
+// The executable name shown in usage/help output, set from argv[0] at startup.
+extern std::wstring s_ExecutableName;
struct Command
{
@@ -99,8 +101,7 @@ struct Command
virtual std::wstring ShortDescription() const = 0;
virtual std::wstring LongDescription() const = 0;
- void OutputIntroHeader() const;
- void OutputHelp(const CommandException* exception = nullptr) const;
+ void OutputHelp(Reporter& reporter, const CommandException* exception = nullptr) const;
std::unique_ptr FindSubCommand(Invocation& inv) const;
diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp
index fb21b5b9be..4e9519340e 100644
--- a/src/windows/wslc/core/Main.cpp
+++ b/src/windows/wslc/core/Main.cpp
@@ -36,6 +36,23 @@ try
wslutil::ConfigureCrt();
wslutil::InitializeWil();
+ // Extract the executable name from argv[0] for use in help/usage output.
+ if (argc > 0 && argv[0])
+ {
+ std::wstring_view exe{argv[0]};
+ auto lastSlash = exe.find_last_of(L"\\/");
+ if (lastSlash != std::wstring_view::npos)
+ {
+ exe = exe.substr(lastSlash + 1);
+ }
+ auto dot = exe.rfind(L'.');
+ if (dot != std::wstring_view::npos)
+ {
+ exe = exe.substr(0, dot);
+ }
+ s_ExecutableName = exe;
+ }
+
WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild);
auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); });
@@ -121,7 +138,7 @@ try
catch (const CommandException& ce)
{
// Input failure: show help alongside the error so the user can correct it.
- command->OutputHelp(&ce);
+ command->OutputHelp(context.Reporter, &ce);
return 1;
}
catch (...)
diff --git a/src/windows/wslc/core/TableOutput.h b/src/windows/wslc/core/TableOutput.h
index a84725d845..49bca6d409 100644
--- a/src/windows/wslc/core/TableOutput.h
+++ b/src/windows/wslc/core/TableOutput.h
@@ -8,119 +8,263 @@ Module Name:
Abstract:
- Header file for outputting data in a table format.
+ Structured table output for the WSLC CLI. Cells are either plain text or
+ format-string + Sequence args. Sequences are zero display width; the table
+ measures visible width by counting non-placeholder characters. At render
+ time, sequences are emitted or stripped based on Reporter color state.
--*/
#pragma once
#include
#include
-#include
-#include
-#include
+#include
+#include
#include
#include
+#include
#include
-#include
+#include "Reporter.h"
+#include "VTSupport.h"
namespace wsl::windows::wslc {
-namespace detail {
- // This function outputs a table line.
- inline void PrintTableLine(const std::wstring& line, FILE* stream)
+using namespace wsl::windows::common::vt;
+
+// A table cell: either plain text or a format string with Sequence placeholders.
+// Every {} in the format string corresponds to a Sequence (zero display width).
+// Visible width is the count of non-placeholder characters in the format string.
+struct FormattedCell
+{
+ std::wstring fmt;
+ std::vector sequences;
+
+ // Default constructor — empty cell.
+ FormattedCell() = default;
+
+ // Implicit from wstring — plain text cell (no formatting).
+ FormattedCell(std::wstring text) : fmt(std::move(text))
{
- ::wsl::windows::common::wslutil::PrintMessage(line, stream);
}
-} // namespace detail
-// Helper function to get display width of a string
-// For now, uses simple length (can be enhanced with proper Unicode width calculation)
-inline size_t GetStringColumnWidth(const wchar_t* str)
-{
- if (!str)
+ // Implicit from wstring_view.
+ FormattedCell(std::wstring_view text) : fmt(text)
{
- return 0;
}
- return wcslen(str);
-}
-// Helper function to trim string to a specific column width
-inline std::wstring TrimStringToColumnWidth(const wchar_t* str, size_t maxWidth, size_t& actualWidth)
-{
- if (!str)
+ // Implicit from literal.
+ FormattedCell(const wchar_t* text) : fmt(text)
{
- actualWidth = 0;
- return L"";
}
- size_t len = wcslen(str);
- if (len <= maxWidth)
+ // Formatted cell: format string with Sequence placeholders.
+ FormattedCell(std::wstring format, std::initializer_list seqs) : fmt(std::move(format)), sequences(seqs)
{
- actualWidth = len;
- return std::wstring(str);
}
- actualWidth = maxWidth;
- return std::wstring(str, maxWidth);
-}
+ // Single-sequence cell: wraps text with the sequence and a trailing reset.
+ FormattedCell(std::wstring_view text, const Sequence& seq) : sequences({&seq, &Format::Default})
+ {
+ fmt.reserve(4 + text.size());
+ fmt += L"{}";
+ fmt += text;
+ fmt += L"{}";
+ }
+
+ // Visible width: count characters that are not part of {} placeholders.
+ size_t VisibleWidth() const
+ {
+ size_t width = 0;
+ for (size_t i = 0; i < fmt.size(); ++i)
+ {
+ if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
+ {
+ ++i; // skip the pair
+ }
+ else
+ {
+ ++width;
+ }
+ }
+ return width;
+ }
+
+ // Renders the cell with or without sequences.
+ // When vtEnabled is false, all {} placeholders are skipped (no VT output).
+ // When vtEnabled is true but colorEnabled is false, only non-color sequences are emitted.
+ // When both are true, all sequences are emitted.
+ std::wstring Render(bool vtEnabled, bool colorEnabled) const
+ {
+ if (sequences.empty())
+ {
+ return fmt; // plain text, no placeholders
+ }
+
+ std::wstring result;
+ result.reserve(fmt.size() + (vtEnabled ? sequences.size() * 8 : 0));
+ size_t seqIdx = 0;
+
+ for (size_t i = 0; i < fmt.size(); ++i)
+ {
+ if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
+ {
+ if (vtEnabled && seqIdx < sequences.size() && (colorEnabled || !sequences[seqIdx]->IsColor()))
+ {
+ result.append(sequences[seqIdx]->Get());
+ }
+ ++seqIdx;
+ ++i; // skip the pair
+ }
+ else
+ {
+ result += fmt[i];
+ }
+ }
+
+ return result;
+ }
+
+ // Renders with visible text truncated to maxWidth characters, appending ellipsis.
+ // Sequences after the truncation point are still emitted (for resets).
+ std::wstring RenderTruncated(size_t maxWidth, bool vtEnabled, bool colorEnabled) const
+ {
+ if (sequences.empty())
+ {
+ // Plain text: simple truncation.
+ if (fmt.size() <= maxWidth)
+ {
+ return fmt;
+ }
+ return fmt.substr(0, maxWidth > 0 ? maxWidth - 1 : 0) + L"\u2026";
+ }
+
+ std::wstring result;
+ result.reserve(fmt.size());
+ size_t seqIdx = 0;
+ size_t visibleChars = 0;
+ bool truncated = (maxWidth == 0);
+ const size_t truncateAt = maxWidth > 1 ? maxWidth - 1 : 0;
+
+ for (size_t i = 0; i < fmt.size(); ++i)
+ {
+ if (i + 1 < fmt.size() && fmt[i] == L'{' && fmt[i + 1] == L'}')
+ {
+ // Always emit sequences (they're invisible); they handle resets after truncation.
+ if (vtEnabled && seqIdx < sequences.size() && (colorEnabled || !sequences[seqIdx]->IsColor()))
+ {
+ result.append(sequences[seqIdx]->Get());
+ }
+ ++seqIdx;
+ ++i;
+ }
+ else if (!truncated)
+ {
+ if (visibleChars < truncateAt)
+ {
+ result += fmt[i];
+ ++visibleChars;
+ }
+ else
+ {
+ result += L'\u2026';
+ truncated = true;
+ }
+ }
+ // After truncation, skip remaining visible chars but continue to emit sequences.
+ }
+
+ return result;
+ }
+};
+
+// Controls how a column handles content that exceeds its available width.
+enum class ColumnOverflow
+{
+ // Truncates content with an ellipsis at MaxWidth; column width is fixed and does not
+ // participate in the shrink loop.
+ Truncate,
+
+ // Participates in the shrink loop: reduced largest-first down to MinWidth, then truncated.
+ // PreferredShrink=true marks this as a higher-priority shrink target.
+ Shrink,
+
+ // Wraps long values across multiple physical rows; width is remaining space after other columns.
+ Wrap,
+};
-// Column width configuration options
struct ColumnWidthConfig
{
static constexpr size_t NoLimit = 0;
- size_t MinWidth = NoLimit; // Minimum column width (NoLimit = use header width)
- size_t MaxWidth = NoLimit; // Maximum column width (NoLimit = unlimited)
- bool PreferredShrink = true; // Should this column shrink first when space is limited?
+ size_t MinWidth = NoLimit; // Minimum visible width (NoLimit = header width).
+ size_t MaxWidth = NoLimit; // Maximum visible width cap (NoLimit = unlimited).
+ ColumnOverflow Overflow = ColumnOverflow::Truncate;
+ bool PreferredShrink = true; // Prioritizes this column in the shrink loop.
};
-// Column definition with name and configuration
struct ColumnDefinition
{
std::wstring Name;
ColumnWidthConfig Config;
};
-// Enables output data in a table format.
-// TODO: Improve for use with sparse data.
template
struct TableOutput
{
static_assert(FieldCount > 0, "TableOutput requires at least one column");
using header_t = std::array;
- using line_t = std::array;
+ using line_t = std::array;
using column_config_t = std::array;
using column_def_t = std::array;
- using OutputFn = std::function;
static constexpr size_t DefaultColumnPadding = 3; // Docker-like spacing between columns
- // For redirected console the receiver controls the width. This should be a large value but not
- // too large. A few thousand should be reasonable and prevents potential arithmetic issues later.
+ // Generous fallback used when the destination is redirected (no real console width).
+ // The wrap pass is skipped in that case so the receiver controls its own width.
static constexpr size_t DefaultRedirectedConsoleWidth = 2000;
- // Constructor with default behavior (no column limits)
- TableOutput(header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
- m_sizingBuffer(sizingBuffer), m_limitColumnWidths(false), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
+ TableOutput(Reporter& reporter, header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding, Reporter::Level level = Reporter::Level::Output) :
+ m_reporter(reporter),
+ m_outputLevel(level),
+ m_vtEnabled(reporter.IsVTEnabled(level)),
+ m_colorEnabled(reporter.IsColorEnabled(level)),
+ m_sizingBuffer(sizingBuffer),
+ m_columnPadding(columnPadding)
{
InitializeColumns(std::move(header));
}
- // Constructor with column width configuration (legacy)
- TableOutput(header_t&& header, column_config_t&& config, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
+ TableOutput(
+ Reporter& reporter,
+ header_t&& header,
+ column_config_t&& config,
+ size_t sizingBuffer = 50,
+ size_t columnPadding = DefaultColumnPadding,
+ Reporter::Level level = Reporter::Level::Output) :
+ m_reporter(reporter),
+ m_outputLevel(level),
+ m_vtEnabled(reporter.IsVTEnabled(level)),
+ m_colorEnabled(reporter.IsColorEnabled(level)),
m_sizingBuffer(sizingBuffer),
- m_limitColumnWidths(true),
m_columnPadding(columnPadding),
- m_columnConfigs(std::move(config)),
- m_outputFn(DefaultOutputFn())
+ m_columnConfigs(std::move(config))
{
InitializeColumns(std::move(header));
}
- // Constructor with column definitions (name + config together)
- TableOutput(column_def_t&& columns, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
- m_sizingBuffer(sizingBuffer), m_limitColumnWidths(true), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
+ TableOutput(
+ Reporter& reporter,
+ column_def_t&& columns,
+ size_t sizingBuffer = 50,
+ size_t columnPadding = DefaultColumnPadding,
+ Reporter::Level level = Reporter::Level::Output) :
+ m_reporter(reporter),
+ m_outputLevel(level),
+ m_vtEnabled(reporter.IsVTEnabled(level)),
+ m_colorEnabled(reporter.IsColorEnabled(level)),
+ m_sizingBuffer(sizingBuffer),
+ m_columnPadding(columnPadding)
{
header_t headers;
for (size_t i = 0; i < FieldCount; ++i)
@@ -131,56 +275,58 @@ struct TableOutput
InitializeColumns(std::move(headers));
}
- // Enable/disable column width limiting
- void SetColumnWidthLimiting(bool enable)
- {
- m_limitColumnWidths = enable;
- }
-
- // Set configuration for a specific column
+ // Updates config store and Column state; safe to call after construction.
void SetColumnConfig(size_t columnIndex, const ColumnWidthConfig& config)
{
if (columnIndex < FieldCount)
{
m_columnConfigs[columnIndex] = config;
+ SyncColumnFromConfig(columnIndex);
}
}
- // Set whether to always show header even when there are no rows
void SetAlwaysShowHeader(bool alwaysShow)
{
m_alwaysShowHeader = alwaysShow;
}
-
- // Set whether to show the header row
void SetShowHeader(bool showHeader)
{
m_showHeader = showHeader;
}
-
- // Override the output function (e.g. redirect to a stringstream in tests).
- void SetOutputFunction(OutputFn fn)
+ // Sets spaces prepended to every row. Does not affect column width calculations.
+ void SetRowIndent(size_t spaces)
{
- FAIL_FAST_IF_MSG(!fn, "OutputFn must not be empty");
- m_outputFn = std::move(fn);
+ m_rowIndent = spaces;
}
- // Override the console width used for column shrinking (useful in tests).
- // Pass 0 to restore the default behaviour (query the real console).
+ // Overrides console width for column shrinking; pass 0 to restore default (Reporter-derived).
+ // When set, the wrap pass also runs as if a real console were attached.
void SetConsoleWidthOverride(size_t width)
{
m_consoleWidthOverride = width;
}
+ // Promotes all Truncate columns to Shrink globally; no effect on Wrap columns.
+ void SetColumnWidthLimiting(bool enable)
+ {
+ for (size_t i = 0; i < FieldCount; ++i)
+ {
+ if (enable && m_columnConfigs[i].Overflow == ColumnOverflow::Truncate)
+ {
+ m_columnConfigs[i].Overflow = ColumnOverflow::Shrink;
+ SyncColumnFromConfig(i);
+ }
+ }
+ }
- void OutputLine(line_t&& line)
+ void WriteRow(line_t&& line)
{
m_empty = false;
- // When width limiting is disabled, buffer all rows to ensure accurate column sizing
- // and prevent truncation (e.g., for --no-trunc flag)
- if (!m_limitColumnWidths || m_buffer.size() < m_sizingBuffer)
+ // Buffer rows to ensure accurate column sizing before flush.
+ if (m_dataRowCount < m_sizingBuffer)
{
m_buffer.emplace_back(std::move(line));
+ ++m_dataRowCount;
}
else
{
@@ -189,6 +335,22 @@ struct TableOutput
}
}
+ // Emits a standalone text line that does not participate in column sizing.
+ // Use for section headers or blank separators between data rows.
+ void WriteLine(FormattedCell cell = {})
+ {
+ m_empty = false;
+
+ if (!m_bufferEvaluated)
+ {
+ m_buffer.emplace_back(std::move(cell));
+ }
+ else
+ {
+ OutputCellLineToStream(cell);
+ }
+ }
+
void Complete()
{
if (!m_empty)
@@ -207,7 +369,9 @@ struct TableOutput
}
private:
- // A column in the table.
+ // A break entry is a FormattedCell rendered as a standalone line (section header or blank).
+ using buffer_entry_t = std::variant;
+
struct Column
{
std::wstring Name;
@@ -215,26 +379,44 @@ struct TableOutput
size_t MaxLength = 0;
size_t ConfiguredMaxLength = 0; // Max length from configuration
bool SpaceAfter = true;
+ ColumnOverflow Overflow = ColumnOverflow::Truncate;
};
+ Reporter& m_reporter;
+ Reporter::Level m_outputLevel;
+ const bool m_vtEnabled;
+ const bool m_colorEnabled;
std::array m_columns;
column_config_t m_columnConfigs;
size_t m_sizingBuffer;
size_t m_columnPadding;
- std::vector m_buffer;
+ size_t m_rowIndent = 0;
+ std::vector m_buffer;
+ size_t m_dataRowCount = 0;
bool m_bufferEvaluated = false;
bool m_empty = true;
- bool m_limitColumnWidths = false;
bool m_alwaysShowHeader = true;
bool m_showHeader = true;
bool m_dropEmptyColumns = false;
- std::wstringstream m_stream;
- OutputFn m_outputFn;
size_t m_consoleWidthOverride = 0;
- static OutputFn DefaultOutputFn()
+ // Syncs Column state from m_columnConfigs[i]; call whenever a config entry changes.
+ void SyncColumnFromConfig(size_t i)
{
- return [](const std::wstring& line) { detail::PrintTableLine(line, stdout); };
+ auto& col = m_columns[i];
+ const auto& cfg = m_columnConfigs[i];
+
+ col.Overflow = cfg.Overflow;
+ col.ConfiguredMaxLength = (cfg.MaxWidth != ColumnWidthConfig::NoLimit) ? cfg.MaxWidth : 0;
+
+ if (cfg.MinWidth != ColumnWidthConfig::NoLimit)
+ {
+ col.MinLength = std::max(col.Name.size(), cfg.MinWidth);
+ }
+ else
+ {
+ col.MinLength = col.Name.size();
+ }
}
void InitializeColumns(header_t&& header)
@@ -242,59 +424,137 @@ struct TableOutput
for (size_t i = 0; i < FieldCount; ++i)
{
m_columns[i].Name = std::move(header[i]);
- m_columns[i].MinLength = GetStringColumnWidth(m_columns[i].Name.c_str());
m_columns[i].MaxLength = 0;
+ SyncColumnFromConfig(i);
+ }
+ }
+
+ // Returns the effective console width (in columns) of the destination, or std::nullopt
+ // when the destination is redirected. SetConsoleWidthOverride() takes precedence and is
+ // treated as a real console (the wrap pass uses has_value() to gate its behavior).
+ std::optional GetEffectiveConsoleWidth() const
+ {
+ if (m_consoleWidthOverride > 0)
+ {
+ return m_consoleWidthOverride;
+ }
+
+ if (const auto width = m_reporter.GetConsoleWidth(m_outputLevel); width.has_value())
+ {
+ return static_cast(*width);
+ }
+
+ return std::nullopt;
+ }
- // Apply configured max width if limiting is enabled
- if (m_limitColumnWidths && m_columnConfigs[i].MaxWidth != ColumnWidthConfig::NoLimit)
+ // Splits visible text into word-boundary chunks of at most maxWidth chars.
+ static std::vector WrapText(const std::wstring& text, size_t maxWidth)
+ {
+ if (maxWidth == 0 || text.length() <= maxWidth)
+ {
+ return {text};
+ }
+
+ std::vector lines;
+ size_t pos = 0;
+
+ while (pos < text.length())
+ {
+ size_t chunkEnd = std::min(pos + maxWidth, text.length());
+
+ if (chunkEnd < text.length())
{
- m_columns[i].ConfiguredMaxLength = m_columnConfigs[i].MaxWidth;
+ size_t breakAt = text.rfind(L' ', chunkEnd);
+ if (breakAt != std::wstring::npos && breakAt > pos)
+ {
+ chunkEnd = breakAt;
+ }
}
- // Apply configured min width
- if (m_columnConfigs[i].MinWidth != ColumnWidthConfig::NoLimit)
+ lines.emplace_back(text.substr(pos, chunkEnd - pos));
+
+ pos = chunkEnd;
+ while (pos < text.length() && text[pos] == L' ')
{
- m_columns[i].MinLength = std::max(m_columns[i].MinLength, m_columnConfigs[i].MinWidth);
+ ++pos;
}
}
+
+ return lines;
}
- size_t GetConsoleWidth()
+ // Wraps a cell's visible text into chunks, preserving formatting on each chunk.
+ std::vector BuildWrappedCells(const FormattedCell& cell, const Column& col) const
{
- if (m_consoleWidthOverride > 0)
+ if (col.Overflow != ColumnOverflow::Wrap || col.MaxLength == 0)
{
- return m_consoleWidthOverride;
+ return {cell};
+ }
+
+ // Extract the visible text for wrapping.
+ const size_t visWidth = cell.VisibleWidth();
+ if (visWidth <= col.MaxLength)
+ {
+ return {cell};
+ }
+
+ // For plain cells, wrap the text directly.
+ if (cell.sequences.empty())
+ {
+ auto chunks = WrapText(cell.fmt, col.MaxLength);
+ std::vector result;
+ result.reserve(chunks.size());
+ for (auto& chunk : chunks)
+ {
+ result.emplace_back(std::move(chunk));
+ }
+ return result;
}
- CONSOLE_SCREEN_BUFFER_INFO consoleInfo{};
- HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
+ // Wrapping only supports single-style cells (open + reset). Complex cells with
+ // multiple sequences (e.g., hyperlinks with distinct open/close pairs) cannot be
+ // reliably split across wrapped lines. Callers needing rich formatting in a wrapped
+ // column should use a single constructed Sequence that combines all escape codes.
+ THROW_HR_IF(E_INVALIDARG, cell.sequences.size() > 2);
- if (GetConsoleScreenBufferInfo(hConsole, &consoleInfo))
+ // For formatted cells, extract visible text, wrap it, then re-apply formatting.
+ std::wstring visibleText;
+ visibleText.reserve(visWidth);
+ for (size_t i = 0; i < cell.fmt.size(); ++i)
{
- return static_cast(consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
+ if (i + 1 < cell.fmt.size() && cell.fmt[i] == L'{' && cell.fmt[i + 1] == L'}')
+ {
+ ++i;
+ }
+ else
+ {
+ visibleText += cell.fmt[i];
+ }
}
- // stdout is not a real console (e.g. redirected/piped). Return a large value
- // so column shrinking is not applied — the receiver controls its own display width.
- return DefaultRedirectedConsoleWidth;
+ auto chunks = WrapText(visibleText, col.MaxLength);
+ std::vector result;
+ result.reserve(chunks.size());
+ for (auto& chunk : chunks)
+ {
+ result.emplace_back(FormattedCell(std::wstring_view{chunk}, *cell.sequences.front()));
+ }
+ return result;
}
void OutputHeaderOnly()
{
- // Set MaxLength to MinLength for all columns (header width only)
for (size_t i = 0; i < FieldCount; ++i)
{
m_columns[i].MaxLength = m_columns[i].MinLength;
}
- // Set spacing configuration
m_columns[FieldCount - 1].SpaceAfter = false;
- // Output the header
line_t headerLine;
for (size_t i = 0; i < FieldCount; ++i)
{
- headerLine[i] = m_columns[i].Name.c_str();
+ headerLine[i] = FormattedCell(m_columns[i].Name);
}
OutputLineToStream(headerLine);
@@ -308,26 +568,29 @@ struct TableOutput
return;
}
- // Determine the maximum length for all columns
- for (const auto& line : m_buffer)
+ // Determine the maximum visible width for each column across all buffered data rows.
+ for (const auto& entry : m_buffer)
{
+ const auto* line = std::get_if(&entry);
+ if (!line)
+ {
+ continue;
+ }
+
for (size_t i = 0; i < FieldCount; ++i)
{
- size_t columnWidth = GetStringColumnWidth(line[i].c_str());
+ size_t w = (*line)[i].VisibleWidth();
- // Apply configured max width if limiting is enabled
- if (m_limitColumnWidths && m_columns[i].ConfiguredMaxLength != ColumnWidthConfig::NoLimit)
+ if (m_columns[i].ConfiguredMaxLength != ColumnWidthConfig::NoLimit)
{
- columnWidth = std::min(columnWidth, m_columns[i].ConfiguredMaxLength);
+ w = std::min(w, m_columns[i].ConfiguredMaxLength);
}
- m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, columnWidth);
+ m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, w);
}
}
- // If there are actually columns with data, then also bring in the minimum size.
- // When m_dropEmptyColumns is false, always apply MinLength so empty columns
- // still render at least as wide as their header.
+ // Apply MinLength so empty columns still render at least as wide as their header.
for (size_t i = 0; i < FieldCount; ++i)
{
if (m_columns[i].MaxLength || !m_dropEmptyColumns)
@@ -336,84 +599,102 @@ struct TableOutput
}
}
- // Only output the extra space if:
- // 1. Not the last field
+ // Last column never needs trailing padding.
m_columns[FieldCount - 1].SpaceAfter = false;
- // 2. Not empty (taken care of by not doing anything if empty)
- // 3. There are non-empty fields after
+ // Disable SpaceAfter on columns that are followed only by empty columns.
for (size_t i = FieldCount - 1; i > 0; --i)
{
if (m_columns[i].MaxLength)
{
break;
}
- else
- {
- m_columns[i - 1].SpaceAfter = false;
- }
+ m_columns[i - 1].SpaceAfter = false;
}
- // Determine the total width required to not truncate any columns
+ // Compute total visible width required to not truncate any columns.
size_t totalRequired = 0;
-
for (size_t i = 0; i < FieldCount; ++i)
{
totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? m_columnPadding : 0);
}
- // Only apply console width constraints if m_limitColumnWidths is true
- if (m_limitColumnWidths)
+ const auto consoleWidthOpt = GetEffectiveConsoleWidth();
+ const size_t consoleWidth = consoleWidthOpt.value_or(DefaultRedirectedConsoleWidth);
+ const size_t availableWidth = (consoleWidth > m_rowIndent) ? consoleWidth - m_rowIndent : 0;
+
+ // Shrink pass: reduce Shrink columns until the total fits within the available width.
+ if (totalRequired > availableWidth)
{
- size_t consoleWidth = GetConsoleWidth();
+ size_t extra = totalRequired - availableWidth;
- // If the total space would be too big, shrink them.
- // We don't want to use the last column, lest we auto-wrap
- if (totalRequired >= consoleWidth)
+ while (extra > 0)
{
- size_t extra = (totalRequired - consoleWidth) + 1;
+ size_t targetIndex = FieldCount;
+ size_t targetVal = 0;
- while (extra > 0)
+ for (size_t j = 0; j < FieldCount; ++j)
{
- // Find the largest shrinkable column
- size_t targetIndex = 0;
- size_t targetVal = 0;
-
- for (size_t j = 0; j < FieldCount; ++j)
+ if (m_columns[j].Overflow != ColumnOverflow::Shrink)
+ {
+ continue;
+ }
+ if (m_columns[j].MaxLength <= m_columns[j].MinLength)
{
- // Skip columns at or below minimum
- if (m_columns[j].MaxLength <= m_columns[j].MinLength)
- {
- continue;
- }
-
- // Prefer columns marked as preferredShrink
- bool isPreferredShrink = m_columnConfigs[j].PreferredShrink;
- bool currentIsPreferred = m_columnConfigs[targetIndex].PreferredShrink;
-
- if (isPreferredShrink && !currentIsPreferred)
- {
- targetIndex = j;
- targetVal = m_columns[j].MaxLength;
- }
- else if (isPreferredShrink == currentIsPreferred && m_columns[j].MaxLength > targetVal)
- {
- targetIndex = j;
- targetVal = m_columns[j].MaxLength;
- }
+ continue;
}
- // If no shrinkable column found, break
- if (targetVal == 0)
+ const bool isPreferred = m_columnConfigs[j].PreferredShrink;
+ const bool currentPreferred = (targetIndex < FieldCount) ? m_columnConfigs[targetIndex].PreferredShrink : false;
+
+ if (targetIndex == FieldCount || (isPreferred && !currentPreferred) ||
+ (isPreferred == currentPreferred && m_columns[j].MaxLength > targetVal))
{
- break;
+ targetIndex = j;
+ targetVal = m_columns[j].MaxLength;
}
+ }
+
+ if (targetIndex == FieldCount)
+ {
+ break;
+ }
+
+ m_columns[targetIndex].MaxLength -= 1;
+ extra -= 1;
+ }
+ }
+
+ // Wrap pass: clamp each Wrap column to remaining space after all other columns.
+ // Skipped when the destination is redirected so the receiver controls its own width.
+ if (consoleWidthOpt.has_value())
+ {
+ for (size_t i = 0; i < FieldCount; ++i)
+ {
+ if (m_columns[i].Overflow != ColumnOverflow::Wrap || !m_columns[i].MaxLength)
+ {
+ continue;
+ }
- m_columns[targetIndex].MaxLength -= 1;
- extra -= 1;
+ size_t otherWidth = 0;
+ for (size_t j = 0; j < FieldCount; ++j)
+ {
+ if (j != i)
+ {
+ otherWidth += m_columns[j].MaxLength + (m_columns[j].SpaceAfter ? m_columnPadding : 0);
+ }
+ }
+ if (m_columns[i].SpaceAfter)
+ {
+ otherWidth += m_columnPadding;
}
- totalRequired = std::min(totalRequired, consoleWidth - 1);
+ const size_t wrapBudget = (availableWidth > otherWidth) ? availableWidth - otherWidth : 1;
+
+ if (m_columns[i].MaxLength > wrapBudget)
+ {
+ m_columns[i].MaxLength = std::max(wrapBudget, m_columns[i].MinLength);
+ }
}
}
@@ -422,63 +703,87 @@ struct TableOutput
line_t headerLine;
for (size_t i = 0; i < FieldCount; ++i)
{
- headerLine[i] = m_columns[i].Name.c_str();
+ headerLine[i] = FormattedCell(m_columns[i].Name);
}
-
OutputLineToStream(headerLine);
}
- for (const auto& line : m_buffer)
+ for (const auto& entry : m_buffer)
{
- OutputLineToStream(line);
+ if (const auto* line = std::get_if(&entry))
+ {
+ OutputLineToStream(*line);
+ }
+ else if (const auto* cell = std::get_if(&entry))
+ {
+ OutputCellLineToStream(*cell);
+ }
}
m_bufferEvaluated = true;
}
+ void OutputCellLineToStream(const FormattedCell& cell)
+ {
+ m_reporter.Write(m_outputLevel, L"{}\n", cell.Render(m_vtEnabled, m_colorEnabled));
+ }
+
+ // Renders a logical row, emitting multiple physical rows for word-wrapping columns.
void OutputLineToStream(const line_t& line)
{
+ size_t physicalRows = 1;
+ std::array, FieldCount> wrappedCells;
for (size_t i = 0; i < FieldCount; ++i)
{
- const auto& col = m_columns[i];
+ wrappedCells[i] = BuildWrappedCells(line[i], m_columns[i]);
+ physicalRows = std::max(physicalRows, wrappedCells[i].size());
+ }
+
+ for (size_t row = 0; row < physicalRows; ++row)
+ {
+ std::wstring rowStr;
- if (col.MaxLength)
+ if (m_rowIndent > 0)
{
- size_t valueLength = GetStringColumnWidth(line[i].c_str());
+ rowStr.append(m_rowIndent, L' ');
+ }
- if (valueLength > col.MaxLength)
+ for (size_t i = 0; i < FieldCount; ++i)
+ {
+ const auto& col = m_columns[i];
+ if (!col.MaxLength)
{
- size_t actualWidth;
- m_stream << TrimStringToColumnWidth(line[i].c_str(), col.MaxLength - 1, actualWidth) << L"\u2026"; // Unicode ellipsis character
+ continue;
+ }
- // Some characters take 2 unit space, the trimmed string length might be 1 less than the expected length.
- if (actualWidth != col.MaxLength - 1)
- {
- m_stream << L' ';
- }
+ // On continuation rows, exhausted columns render as blank.
+ static const FormattedCell emptyCell{L""};
+ const FormattedCell& cell = (row < wrappedCells[i].size()) ? wrappedCells[i][row] : emptyCell;
+ const size_t valueLength = cell.VisibleWidth();
+
+ if (col.Overflow != ColumnOverflow::Wrap && valueLength > col.MaxLength)
+ {
+ // Truncate and append ellipsis.
+ rowStr.append(cell.RenderTruncated(col.MaxLength, m_vtEnabled, m_colorEnabled));
if (col.SpaceAfter)
{
- m_stream << std::wstring(m_columnPadding, L' ');
+ rowStr.append(m_columnPadding, L' ');
}
}
else
{
- m_stream << line[i];
+ rowStr.append(cell.Render(m_vtEnabled, m_colorEnabled));
if (col.SpaceAfter)
{
- m_stream << std::wstring(col.MaxLength - valueLength + m_columnPadding, L' ');
+ rowStr.append(col.MaxLength - valueLength + m_columnPadding, L' ');
}
}
}
- }
-
- const std::wstring rendered = m_stream.str();
- m_stream.str(L"");
- m_stream.clear();
- m_outputFn(rendered);
+ m_reporter.Write(m_outputLevel, L"{}\n", rowStr);
+ }
}
};
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index 921dd279dc..ceee220750 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -317,13 +317,15 @@ void ListContainers(CLIExecutionContext& context)
// Create table with or without column limits based on --no-trunc flag
auto table = trunc ? wsl::windows::wslc::TableOutput<6>(
- {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
- {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
- {Localization::WSLCCLI_TableHeaderImage(), {Config::NoLimit, 20, false}},
- {Localization::WSLCCLI_TableHeaderCreated(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderStatus(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderPorts(), {Config::NoLimit, Config::NoLimit, false}}}})
+ context.Reporter,
+ {{{Localization::WSLCCLI_TableHeaderContainerId(), {.MaxWidth = 12}},
+ {Localization::WSLCCLI_TableHeaderName(), {.MaxWidth = 20}},
+ {Localization::WSLCCLI_TableHeaderImage(), {.MaxWidth = 20}},
+ {Localization::WSLCCLI_TableHeaderCreated(), {}},
+ {Localization::WSLCCLI_TableHeaderStatus(), {}},
+ {Localization::WSLCCLI_TableHeaderPorts(), {}}}})
: wsl::windows::wslc::TableOutput<6>(
+ context.Reporter,
{Localization::WSLCCLI_TableHeaderContainerId(),
Localization::WSLCCLI_TableHeaderName(),
Localization::WSLCCLI_TableHeaderImage(),
@@ -334,7 +336,7 @@ void ListContainers(CLIExecutionContext& context)
// Add each container as a row
for (const auto& container : containers)
{
- table.OutputLine({
+ table.WriteRow({
MultiByteToWide(trunc ? TruncateId(container.Id) : container.Id),
MultiByteToWide(container.Name),
MultiByteToWide(container.Image),
@@ -684,15 +686,17 @@ void ShowContainerStats(CLIExecutionContext& context)
bool trunc = !context.Args.Contains(ArgType::NoTrunc);
auto table = trunc ? wsl::windows::wslc::TableOutput<8>(
- {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
- {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
- {Localization::WSLCCLI_TableHeaderCpuPercent(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderMemUsageLimit(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderMemPercent(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderNetIo(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderBlockIo(), {Config::NoLimit, Config::NoLimit, false}},
- {Localization::WSLCCLI_TableHeaderPids(), {Config::NoLimit, Config::NoLimit, false}}}})
+ context.Reporter,
+ {{{Localization::WSLCCLI_TableHeaderContainerId(), {.MaxWidth = 12}},
+ {Localization::WSLCCLI_TableHeaderName(), {.MaxWidth = 20}},
+ {Localization::WSLCCLI_TableHeaderCpuPercent(), {}},
+ {Localization::WSLCCLI_TableHeaderMemUsageLimit(), {}},
+ {Localization::WSLCCLI_TableHeaderMemPercent(), {}},
+ {Localization::WSLCCLI_TableHeaderNetIo(), {}},
+ {Localization::WSLCCLI_TableHeaderBlockIo(), {}},
+ {Localization::WSLCCLI_TableHeaderPids(), {}}}})
: wsl::windows::wslc::TableOutput<8>(
+ context.Reporter,
{Localization::WSLCCLI_TableHeaderContainerId(),
Localization::WSLCCLI_TableHeaderName(),
Localization::WSLCCLI_TableHeaderCpuPercent(),
@@ -705,7 +709,7 @@ void ShowContainerStats(CLIExecutionContext& context)
for (const auto& entry : statsJson)
{
const auto id = entry["ID"].get();
- table.OutputLine({
+ table.WriteRow({
MultiByteToWide(trunc ? TruncateId(id) : id),
MultiByteToWide(entry["Name"].get()),
MultiByteToWide(entry["CPUPerc"].get()),
diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp
index b7e0a8693a..999e0b8f22 100644
--- a/src/windows/wslc/tasks/ImageTasks.cpp
+++ b/src/windows/wslc/tasks/ImageTasks.cpp
@@ -148,17 +148,16 @@ void ListImages(CLIExecutionContext& context)
// Create table — only IMAGE ID uses fixed width; other columns auto-size.
// When --no-trunc is passed, IMAGE ID also shows full length via TruncateId().
- auto table = trunc ? wsl::windows::wslc::TableOutput<5>(
- {{{L"REPOSITORY", {Config::NoLimit, Config::NoLimit, false}},
- {L"TAG", {Config::NoLimit, Config::NoLimit, false}},
- {L"IMAGE ID", {12, 12, false}},
- {L"CREATED", {Config::NoLimit, Config::NoLimit, false}},
- {L"SIZE", {Config::NoLimit, Config::NoLimit, false}}}})
- : wsl::windows::wslc::TableOutput<5>({L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
+ auto table =
+ trunc
+ ? wsl::windows::wslc::TableOutput<5>(
+ context.Reporter,
+ {{{L"REPOSITORY", {}}, {L"TAG", {}}, {L"IMAGE ID", {.MinWidth = 12, .MaxWidth = 12}}, {L"CREATED", {}}, {L"SIZE", {}}}})
+ : wsl::windows::wslc::TableOutput<5>(context.Reporter, {L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
for (const auto& image : images)
{
- table.OutputLine({
+ table.WriteRow({
MultiByteToWide(image.Repository.value_or("")),
MultiByteToWide(image.Tag.value_or("")),
MultiByteToWide(TruncateId(image.Id, trunc)),
diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp
index b14874c495..d9f6f0b966 100644
--- a/src/windows/wslc/tasks/NetworkTasks.cpp
+++ b/src/windows/wslc/tasks/NetworkTasks.cpp
@@ -179,10 +179,10 @@ void ListNetworks(CLIExecutionContext& context)
}
case FormatType::Table:
{
- auto table = wsl::windows::wslc::TableOutput<3>({L"NETWORK ID", L"NAME", L"DRIVER"});
+ auto table = wsl::windows::wslc::TableOutput<3>(context.Reporter, {L"NETWORK ID", L"NAME", L"DRIVER"});
for (const auto& network : networks)
{
- table.OutputLine({
+ table.WriteRow({
MultiByteToWide(TruncateId(network.Id)),
MultiByteToWide(network.Name),
MultiByteToWide(network.Driver),
diff --git a/src/windows/wslc/tasks/SessionTasks.cpp b/src/windows/wslc/tasks/SessionTasks.cpp
index cc8bdd4fa2..7c8e9695ba 100644
--- a/src/windows/wslc/tasks/SessionTasks.cpp
+++ b/src/windows/wslc/tasks/SessionTasks.cpp
@@ -62,11 +62,12 @@ void ListSessions(CLIExecutionContext& context)
}
TableOutput<3> table(
+ context.Reporter,
{Localization::MessageWslcHeaderId(), Localization::MessageWslcHeaderCreatorPid(), Localization::MessageWslcHeaderDisplayName()});
for (const auto& session : sessions)
{
- table.OutputLine({
+ table.WriteRow({
std::to_wstring(session.SessionId),
std::to_wstring(session.CreatorPid),
session.DisplayName,
diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp
index 4d00fa90ad..5816117264 100644
--- a/src/windows/wslc/tasks/VolumeTasks.cpp
+++ b/src/windows/wslc/tasks/VolumeTasks.cpp
@@ -183,10 +183,10 @@ void ListVolumes(CLIExecutionContext& context)
}
case FormatType::Table:
{
- auto table = wsl::windows::wslc::TableOutput<2>({L"DRIVER", L"VOLUME NAME"});
+ auto table = wsl::windows::wslc::TableOutput<2>(context.Reporter, {L"DRIVER", L"VOLUME NAME"});
for (const auto& volume : volumes)
{
- table.OutputLine({
+ table.WriteRow({
MultiByteToWide(volume.Driver),
MultiByteToWide(volume.Name),
});
diff --git a/test/windows/wslc/WSLCCLITableOutputUnitTests.cpp b/test/windows/wslc/WSLCCLITableOutputUnitTests.cpp
index dc575c4bbd..cc3561516a 100644
--- a/test/windows/wslc/WSLCCLITableOutputUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLITableOutputUnitTests.cpp
@@ -17,6 +17,7 @@ Module Name:
#include "WSLCCLITestHelpers.h"
#include "TableOutput.h"
+#include "VTSupport.h"
using namespace wsl::windows::wslc;
using namespace WSLCTestHelpers;
@@ -24,11 +25,11 @@ using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
-namespace WSLCTableOutputUnitTests {
+namespace WSLCCLITableOutputUnitTests {
-class WSLCTableOutputUnitTests
+class WSLCCLITableOutputUnitTests
{
- WSLC_TEST_CLASS(WSLCTableOutputUnitTests)
+ WSLC_TEST_CLASS(WSLCCLITableOutputUnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
@@ -40,99 +41,82 @@ class WSLCTableOutputUnitTests
return true;
}
- // Test: header line is emitted as the first row, even with no data rows.
TEST_METHOD(TableOutput_AlwaysShowHeader_EmitsHeaderWhenEmpty)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetAlwaysShowHeader(true);
-
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(1), cap.lines.size());
- // Header line must contain both column names
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
}
- // Test: no output at all when empty and AlwaysShowHeader is false.
TEST_METHOD(TableOutput_NoHeader_EmitsNothingWhenEmpty)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetAlwaysShowHeader(false);
-
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(0), cap.lines.size());
+ VERIFY_ARE_EQUAL(static_cast(0), cap.lines().size());
}
- // Test: one data row produces header + one data line.
TEST_METHOD(TableOutput_SingleRow_EmitsHeaderPlusOneDataLine)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
- cap.table.OutputLine({L"my-container", L"running"});
+ cap.table.WriteRow({L"my-container", L"running"});
cap.table.Complete();
- // Expect: header row + 1 data row = 2 lines total
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"my-container") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"running") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"my-container") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"running") != std::wstring::npos);
}
- // Test: multiple data rows all appear after the header.
TEST_METHOD(TableOutput_MultipleRows_AllRowsEmittedAfterHeader)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
- cap.table.OutputLine({L"container-a", L"running"});
- cap.table.OutputLine({L"container-b", L"stopped"});
- cap.table.OutputLine({L"container-c", L"paused"});
+ cap.table.WriteRow({L"container-a", L"running"});
+ cap.table.WriteRow({L"container-b", L"stopped"});
+ cap.table.WriteRow({L"container-c", L"paused"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(4), cap.lines.size()); // header + 3 rows
-
- VERIFY_IS_TRUE(cap.lines[1].find(L"container-a") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[2].find(L"container-b") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[3].find(L"container-c") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(4), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"container-a") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[2].find(L"container-b") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[3].find(L"container-c") != std::wstring::npos);
}
- // Test: columns are separated by the correct number of spaces.
TEST_METHOD(TableOutput_ColumnPadding_DefaultPaddingApplied)
{
- // Use a custom padding of 3 (the default) and verify the data row
- // contains at least 3 spaces between the first column value and the
- // start of the second column value.
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3);
- cap.table.OutputLine({L"abc", L"ok"});
+ cap.table.WriteRow({L"abc", L"ok"});
cap.table.Complete();
- // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok"
- // Expected: "abc ok" with appropriate spacing
- const std::wstring& dataLine = cap.lines[1];
+ auto lines = cap.lines();
+ const auto& dataLine = lines[1];
VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos);
VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos);
- // There must be at least 3 spaces between the two values
auto columnPadding = 3;
auto namePos = dataLine.find(L"abc");
auto statusPos = dataLine.find(L"ok");
VERIFY_IS_TRUE(statusPos >= namePos + wcslen(L"abc") + columnPadding);
}
- // Test: custom column padding is respected.
TEST_METHOD(TableOutput_ColumnPadding_CustomPaddingApplied)
{
constexpr size_t customPadding = 5;
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"A", L"B"}, /*sizingBuffer=*/50, customPadding);
- cap.table.OutputLine({L"x", L"y"});
+ cap.table.WriteRow({L"x", L"y"});
cap.table.Complete();
- // "A" header is 1 char wide, "x" value is 1 char wide.
- // With 5-space padding, "y" must start at position >= 1 + 5 = 6.
- const std::wstring& dataLine = cap.lines[1];
+ auto lines = cap.lines();
+ const auto& dataLine = lines[1];
auto posX = dataLine.find(L'x');
auto posY = dataLine.find(L'y');
VERIFY_IS_TRUE(posX != std::wstring::npos);
@@ -140,67 +124,48 @@ class WSLCTableOutputUnitTests
VERIFY_IS_TRUE(posY >= posX + 1 + customPadding);
}
- // Test: column width expands to fit the widest data value.
TEST_METHOD(TableOutput_ColumnWidth_ExpandsToFitData)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"NAME"});
- cap.table.OutputLine({L"1", L"short"});
- cap.table.OutputLine({L"2", L"a-very-long-container-name"});
+ cap.table.WriteRow({L"1", L"short"});
+ cap.table.WriteRow({L"2", L"a-very-long-container-name"});
cap.table.Complete();
- // The second column must accommodate the widest value in every row.
- for (size_t i = 1; i < cap.lines.size(); ++i)
- {
- // The long value must not have been truncated.
- if (cap.lines[i].find(L"a-very-long-container-name") != std::wstring::npos)
- {
- LogComment(L"Long value found intact in row " + std::to_wstring(i));
- }
- }
- VERIFY_IS_TRUE(cap.lines[2].find(L"a-very-long-container-name") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[2].find(L"a-very-long-container-name") != std::wstring::npos);
}
- // Test: column width is at least as wide as the header.
TEST_METHOD(TableOutput_ColumnWidth_AtLeastHeaderWidth)
{
- // Header "CONTAINER_NAME" is 14 chars; data value is only 3 chars.
- // The data line must still be padded to the header width.
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"CONTAINER_NAME", L"ST"});
- cap.table.OutputLine({L"abc", L"ok"});
+ cap.table.WriteRow({L"abc", L"ok"});
cap.table.Complete();
- // Header line: "CONTAINER_NAME" starts at position 0.
- // Data line: "abc" starts at position 0, "ok" must not start before
- // position 14 + padding.
- const std::wstring& dataLine = cap.lines[1];
+ auto lines = cap.lines();
+ const auto& dataLine = lines[1];
auto posOk = dataLine.find(L"ok");
VERIFY_IS_TRUE(posOk != std::wstring::npos);
- // "CONTAINER_NAME" = 14 chars, padding = 3 -> "ok" must be at >= 17
VERIFY_IS_TRUE(posOk >= static_cast(14 + TableOutput<2>::DefaultColumnPadding));
}
- // Test: values exceeding MaxWidth are truncated and an ellipsis appended.
TEST_METHOD(TableOutput_MaxWidth_LongValueIsTruncatedWithEllipsis)
{
TableOutput<2>::column_config_t configs{};
- configs[0].MaxWidth = 8; // limit first column to 8 chars
+ configs[0].MaxWidth = 8;
configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
- cap.table.OutputLine({L"a-very-long-name", L"running"});
+ cap.table.WriteRow({L"a-very-long-name", L"running"});
cap.table.Complete();
- const std::wstring& dataLine = cap.lines[1];
- // Ellipsis character (U+2026) must be present
+ auto lines = cap.lines();
+ const auto& dataLine = lines[1];
VERIFY_IS_TRUE(dataLine.find(L"\x2026") != std::wstring::npos);
- // Full original value must NOT be present
VERIFY_IS_TRUE(dataLine.find(L"a-very-long-name") == std::wstring::npos);
}
- // Test: values within MaxWidth are not truncated.
TEST_METHOD(TableOutput_MaxWidth_ShortValueNotTruncated)
{
TableOutput<2>::column_config_t configs{};
@@ -209,189 +174,844 @@ class WSLCTableOutputUnitTests
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
- cap.table.OutputLine({L"short", L"running"});
+ cap.table.WriteRow({L"short", L"running"});
cap.table.Complete();
- const std::wstring& dataLine = cap.lines[1];
+ auto lines = cap.lines();
+ const auto& dataLine = lines[1];
VERIFY_IS_TRUE(dataLine.find(L"short") != std::wstring::npos);
VERIFY_IS_TRUE(dataLine.find(L"\x2026") == std::wstring::npos);
}
- // Test: columns shrink when total width exceeds console width.
TEST_METHOD(TableOutput_ConsoleWidthLimit_PreferredShrinkColumnIsShrunk)
{
- // Two columns, first marked preferredShrink=false, second preferredShrink=true.
- // With a very narrow console the second column should absorb the cut.
TableOutput<2>::column_config_t configs{};
configs[0].MaxWidth = ColumnWidthConfig::NoLimit;
+ configs[0].Overflow = ColumnOverflow::Shrink;
configs[0].PreferredShrink = false;
configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
+ configs[1].Overflow = ColumnOverflow::Shrink;
configs[1].PreferredShrink = true;
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"DESCRIPTION"}, std::move(configs));
- // Override with a very narrow console: only 20 chars wide.
cap.table.SetConsoleWidthOverride(20);
- cap.table.SetColumnWidthLimiting(true);
- cap.table.OutputLine({L"abc123", L"this-is-a-long-description-value"});
+ cap.table.WriteRow({L"abc123", L"this-is-a-long-description-value"});
cap.table.Complete();
- // The output must fit within 20 chars.
- for (const auto& line : cap.lines)
+ auto lines = cap.lines();
+ for (const auto& line : lines)
{
VERIFY_IS_TRUE(line.size() <= static_cast(20));
}
}
- // Test: IsEmpty returns true before any rows are added, and false after a row is added.
TEST_METHOD(TableOutput_IsEmpty)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
VERIFY_IS_TRUE(cap.table.IsEmpty());
- cap.table.OutputLine({L"foo", L"bar"});
+ cap.table.WriteRow({L"foo", L"bar"});
VERIFY_IS_FALSE(cap.table.IsEmpty());
}
- // Test: column-definition constructor wires up names and configs correctly.
TEST_METHOD(TableOutput_ColumnDefinition_NameAndConfigUsed)
{
TableOutput<2>::column_def_t defs{{
- ColumnDefinition{L"MYID", {ColumnWidthConfig::NoLimit, 6, false}},
- ColumnDefinition{L"MYNAME", {ColumnWidthConfig::NoLimit, ColumnWidthConfig::NoLimit, true}},
+ ColumnDefinition{L"MYID", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = 6, .Overflow = ColumnOverflow::Shrink, .PreferredShrink = false}},
+ ColumnDefinition{L"MYNAME", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = ColumnWidthConfig::NoLimit}},
}};
TableOutputCapture<2> cap(std::move(defs));
- cap.table.OutputLine({L"id-value", L"name-value"});
+ cap.table.WriteRow({L"id-value", L"name-value"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"MYID") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"MYNAME") != std::wstring::npos);
-
- // "id-value" is 8 chars but MaxWidth=6 -> must be truncated
- VERIFY_IS_TRUE(cap.lines[1].find(L"\x2026") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"MYID") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"MYNAME") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"\x2026") != std::wstring::npos);
}
- // Test: SetShowHeader(false) suppresses header when there are data rows.
TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderWithDataRows)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetShowHeader(false);
- cap.table.OutputLine({L"my-container", L"running"});
+ cap.table.WriteRow({L"my-container", L"running"});
cap.table.Complete();
- // Only the data row should be emitted.
- VERIFY_ARE_EQUAL(static_cast(1), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
}
- // Test: SetShowHeader(false) with AlwaysShowHeader(true) still suppresses header when empty.
TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderEvenWhenAlwaysShowHeaderTrue)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetAlwaysShowHeader(true);
cap.table.SetShowHeader(false);
-
cap.table.Complete();
- // SetShowHeader(false) takes precedence. Nothing should be emitted.
- VERIFY_ARE_EQUAL(static_cast(0), cap.lines.size());
+ VERIFY_ARE_EQUAL(static_cast(0), cap.lines().size());
}
- // Test: SetShowHeader(true) is the default. Header appears before data rows.
TEST_METHOD(TableOutput_ShowHeader_True_IsDefaultAndEmitsHeader)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
- // No explicit call to SetShowHeader. Default must be true.
- cap.table.OutputLine({L"my-container", L"running"});
+ cap.table.WriteRow({L"my-container", L"running"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
}
- // Test: SetShowHeader(false) with multiple data rows emits only data rows.
TEST_METHOD(TableOutput_ShowHeader_False_MultipleDataRowsNoHeader)
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetShowHeader(false);
- cap.table.OutputLine({L"container-a", L"running"});
- cap.table.OutputLine({L"container-b", L"stopped"});
+ cap.table.WriteRow({L"container-a", L"running"});
+ cap.table.WriteRow({L"container-b", L"stopped"});
cap.table.Complete();
- // Two data rows, zero header rows.
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
- // Neither line should contain the column header text.
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
}
- // Test: SetShowHeader controls whether the header row is emitted.
- // Covers: default (true), suppression with data rows, suppression when empty
- // (even with AlwaysShowHeader), and multiple data rows with no header.
TEST_METHOD(TableOutput_ShowHeader)
{
- // Default is true. Header appears before data rows without an explicit call.
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
- cap.table.OutputLine({L"my-container", L"running"});
+ cap.table.WriteRow({L"my-container", L"running"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"STATUS") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
}
-
- // SetShowHeader(false) suppresses the header when data rows are present.
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetShowHeader(false);
- cap.table.OutputLine({L"my-container", L"running"});
+ cap.table.WriteRow({L"my-container", L"running"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(1), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"my-container") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
}
-
- // SetShowHeader(false) suppresses the header even when AlwaysShowHeader is true and the table is empty.
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetAlwaysShowHeader(true);
cap.table.SetShowHeader(false);
-
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(0), cap.lines.size());
+ VERIFY_ARE_EQUAL(static_cast(0), cap.lines().size());
}
-
- // SetShowHeader(false) with multiple data rows emits only data rows.
{
TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
cap.table.SetShowHeader(false);
- cap.table.OutputLine({L"container-a", L"running"});
- cap.table.OutputLine({L"container-b", L"stopped"});
+ cap.table.WriteRow({L"container-a", L"running"});
+ cap.table.WriteRow({L"container-b", L"stopped"});
cap.table.Complete();
- VERIFY_ARE_EQUAL(static_cast(2), cap.lines.size());
- VERIFY_IS_TRUE(cap.lines[0].find(L"container-a") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"container-b") != std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[0].find(L"NAME") == std::wstring::npos);
- VERIFY_IS_TRUE(cap.lines[1].find(L"NAME") == std::wstring::npos);
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
+ VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
}
}
+
+ TEST_METHOD(TableOutput_RowIndent_PrependedToEveryRow)
+ {
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
+ cap.table.SetRowIndent(2);
+
+ cap.table.WriteRow({L"abc", L"ok"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L" NAME STATUS"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" abc ok"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_ShortValueProducesOneRow)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 10;
+ configs[1].MaxWidth = 20;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"short desc"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt short desc"}, cap.lines()[0]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_WrapsAtWordBoundary)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 10;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"hello world"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_ContinuationRowHasBlankLeadingColumns)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 10;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"hello world"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_MultipleWrapsProduceMultipleRows)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ configs[1].MaxWidth = 10;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"one two three four"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt one two"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" three four"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_HardBreakWhenNoSpaceFound)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ configs[1].MaxWidth = 6;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"abcdefghij"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt abcdef"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" ghij"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnStillTruncates)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 5;
+ // ColumnOverflow::Truncate (default) — truncates
+ configs[1].MaxWidth = 20;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"toolongname", L"short desc"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"tool\u2026 short desc"}, cap.lines()[0]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_RowIndentAppliedToAllPhysicalRows)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ configs[1].MaxWidth = 8;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+ cap.table.SetRowIndent(2);
+
+ cap.table.WriteRow({L"opt", L"hello world"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L" opt hello"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_DisabledByDefault_LongTextTruncated)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ configs[1].MaxWidth = 8;
+ // ColumnOverflow::Truncate (default) — truncates
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", L"a very long description"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt a very \u2026"}, cap.lines()[0]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_MultipleLogicalRowsEachWrapIndependently)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 10;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt-a", L"hello world"});
+ cap.table.WriteRow({L"opt-b", L"short"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(3), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt-a hello"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
+ VERIFY_ARE_EQUAL(std::wstring{L"opt-b short"}, cap.lines()[2]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColBLonger)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 5;
+ configs[0].Overflow = ColumnOverflow::Wrap;
+ configs[1].MaxWidth = 5;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"ab cd ef", L"one two three"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(3), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"ab cd one"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L"ef two"}, cap.lines()[1]);
+ VERIFY_ARE_EQUAL(std::wstring{L" three"}, cap.lines()[2]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColALonger)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 5;
+ configs[0].Overflow = ColumnOverflow::Wrap;
+ configs[1].MaxWidth = 5;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"one two three", L"ab cd ef"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(3), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
+ VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsWithEqualLengths)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ configs[0].Overflow = ColumnOverflow::Wrap;
+ configs[1].MaxWidth = 4;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"aa bb", L"xx yy"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"aa xx"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L"bb yy"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnBetweenTwoWrappingColumns)
+ {
+ TableOutput<3>::column_config_t configs{};
+ configs[0].MaxWidth = 4;
+ // configs[0].Overflow = ColumnOverflow::Truncate (default) — truncates
+ configs[1].MaxWidth = 5;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+ configs[2].MaxWidth = 5;
+ configs[2].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<3> cap(TableOutput<3>::header_t{L"", L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"tag", L"aa bb", L"xx yy zz"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"tag aa bb xx yy"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" zz"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_WordWrap_FirstColumnLongerThanSecond)
+ {
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 5;
+ configs[0].Overflow = ColumnOverflow::Wrap;
+ configs[1].MaxWidth = 5;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"one two three", L"ab cd ef"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(3), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
+ VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
+ }
+
+ TEST_METHOD(TableOutput_SetColumnConfig_WordWrapAfterConstruction)
+ {
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+ cap.table.SetColumnConfig(
+ 1,
+ ColumnWidthConfig{
+ .MaxWidth = 8,
+ .Overflow = ColumnOverflow::Wrap,
+ });
+
+ cap.table.WriteRow({L"opt", L"hello world"});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
+ VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(FormattedCell_VisibleWidth_PlainText)
+ {
+ FormattedCell cell{L"hello"};
+ VERIFY_ARE_EQUAL(static_cast(5), cell.VisibleWidth());
+ }
+
+ TEST_METHOD(FormattedCell_VisibleWidth_ExcludesPlaceholders)
+ {
+ FormattedCell cell{L"{}hello{}", {&Format::Fg::BrightRed, &Format::Default}};
+ VERIFY_ARE_EQUAL(static_cast(5), cell.VisibleWidth());
+ }
+
+ TEST_METHOD(FormattedCell_Render_ColorEnabled)
+ {
+ FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
+ const auto result = cell.Render(true, true);
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
+ }
+
+ TEST_METHOD(FormattedCell_Render_ColorDisabled)
+ {
+ FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
+ const auto result = cell.Render(false, false);
+ VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
+ }
+
+ TEST_METHOD(FormattedCell_Render_VTEnabled_ColorDisabled_StripsColorSequences)
+ {
+ FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
+ // VT on but color off: color sequences should be stripped
+ const auto result = cell.Render(true, false);
+ VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
+ }
+
+ TEST_METHOD(FormattedCell_RenderTruncated_TruncatesVisibleText)
+ {
+ FormattedCell cell = FormattedCell(L"hello world", Format::Fg::BrightRed);
+ const auto result = cell.RenderTruncated(5, true, true);
+ // Should contain truncated visible text + ellipsis + sequences
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_ColumnWidthBasedOnVisibleChars)
+ {
+ using namespace wsl::windows::common::vt;
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 10;
+ configs[1].MaxWidth = 20;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
+ cap.table.WriteRow({L"end", FormattedCell{L"world"}});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ // First cell is emphasized (has ESC), second is plain
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
+ VERIFY_ARE_EQUAL(std::wstring{L"end world"}, cap.lines()[1]);
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_WrapsWithSequencesOnEachChunk)
+ {
+ using namespace wsl::windows::common::vt;
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 8;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ // Both lines should have emphasis sequences
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
+ }
+
+ TEST_METHOD(FormattedCell_DefaultCtor_EmptyCell)
+ {
+ FormattedCell cell;
+ VERIFY_ARE_EQUAL(static_cast(0), cell.VisibleWidth());
+ VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(true, true));
+ VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(false, false));
+ }
+
+ TEST_METHOD(FormattedCell_SingleSequenceCtor_BuildsFormatWithReset)
+ {
+ FormattedCell cell(L"bold", Format::Bright);
+ VERIFY_ARE_EQUAL(static_cast(4), cell.VisibleWidth());
+ VERIFY_ARE_EQUAL(static_cast(2), cell.sequences.size());
+
+ // Color enabled: sequences emitted
+ const auto rendered = cell.Render(true, true);
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L"bold"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L'\x1b'));
+
+ // Color disabled: plain text only
+ VERIFY_ARE_EQUAL(std::wstring{L"bold"}, cell.Render(false, false));
+ }
+
+ TEST_METHOD(FormattedCell_VisibleWidth_MultiplePlaceholders)
+ {
+ // "{}a{}bc{}" = 3 visible chars (a, b, c), 3 placeholders
+ FormattedCell cell{L"{}a{}bc{}", {&Format::Bright, &Format::Fg::BrightRed, &Format::Default}};
+ VERIFY_ARE_EQUAL(static_cast(3), cell.VisibleWidth());
+ }
+
+ TEST_METHOD(FormattedCell_VisibleWidth_EmptyFormat)
+ {
+ FormattedCell cell{L""};
+ VERIFY_ARE_EQUAL(static_cast(0), cell.VisibleWidth());
+ }
+
+ TEST_METHOD(FormattedCell_VisibleWidth_OnlyPlaceholders)
+ {
+ FormattedCell cell{L"{}{}", {&Format::Bright, &Format::Default}};
+ VERIFY_ARE_EQUAL(static_cast(0), cell.VisibleWidth());
+ }
+
+ TEST_METHOD(FormattedCell_Render_PlainTextNoSequences)
+ {
+ FormattedCell cell{L"plain text"};
+ VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(true, true));
+ VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(false, false));
+ }
+
+ TEST_METHOD(FormattedCell_RenderTruncated_TextFitsNoTruncation)
+ {
+ FormattedCell cell(L"hello", Format::Fg::BrightRed);
+ const auto result = cell.RenderTruncated(10, true, true);
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\u2026')); // no ellipsis
+ }
+
+ TEST_METHOD(FormattedCell_RenderTruncated_ColorDisabled_StillTruncates)
+ {
+ FormattedCell cell(L"hello world", Format::Fg::BrightRed);
+ const auto result = cell.RenderTruncated(5, false, false);
+ // Sequences stripped, but truncation still occurs
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
+ }
+
+ TEST_METHOD(FormattedCell_RenderTruncated_PlainText)
+ {
+ FormattedCell cell{L"abcdefghij"};
+ const auto result = cell.RenderTruncated(5, false, false);
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"fghij"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_ColorDisabled_SequencesStripped)
+ {
+ using namespace wsl::windows::common::vt;
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 10;
+ configs[1].MaxWidth = 20;
+
+ // vtEnabled=false: sequences should be stripped from output
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(1), cap.lines().size());
+ // No ESC bytes in output
+ VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
+ // Visible text still present with correct padding
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_WrapColorDisabled_SequencesStripped)
+ {
+ using namespace wsl::windows::common::vt;
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 8;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
+ cap.table.Complete();
+
+ VERIFY_ARE_EQUAL(static_cast(2), cap.lines().size());
+ // No ESC in either line
+ VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
+ VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
+ // Text still wraps correctly
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_WrapMultipleLines_SequenceReapplied)
+ {
+ using namespace wsl::windows::common::vt;
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 5;
+ configs[1].Overflow = ColumnOverflow::Wrap;
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
+ cap.table.SetShowHeader(false);
+
+ // Text that wraps into 3 lines: "aa bb cc" at width 5 -> "aa bb" / "cc"
+ // Actually at width 5: "aa" "bb" "cc" (word-break at spaces)
+ cap.table.WriteRow({L"cmd", FormattedCell(L"aa bb cc dd", Format::Fg::BrightCyan)});
+ cap.table.Complete();
+
+ // Should produce multiple wrap lines, each with sequences
+ auto lines = cap.lines();
+ VERIFY_IS_GREATER_THAN(lines.size(), static_cast(1));
+ for (const auto& line : lines)
+ {
+ // Every line that has content should have the escape sequence reapplied
+ if (!line.empty())
+ {
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\x1b'));
+ }
+ }
+ }
+
+ TEST_METHOD(TableOutput_FormattedCell_HyperlinkTruncated_BothSequencesEmitted)
+ {
+ using namespace wsl::windows::common::vt;
+
+ // Simulate a hyperlink cell: OSC 8 open + visible text + OSC 8 close.
+ // The open/close are ConstructedSequences since they contain a URL.
+ const ConstructedSequence hyperlinkOpen{L"\x1b]8;;https://example.com\x1b\\"};
+ const ConstructedSequence hyperlinkClose{L"\x1b]8;;\x1b\\"};
+
+ TableOutput<2>::column_config_t configs{};
+ configs[0].MaxWidth = 6;
+ configs[1].MaxWidth = 8; // Force truncation of "click here now" (14 chars)
+
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
+ cap.table.SetShowHeader(false);
+
+ // FormattedCell: {}visible text{} with hyperlink open/close as sequences
+ cap.table.WriteRow({L"link", FormattedCell(L"{}click here now{}", {&hyperlinkOpen, &hyperlinkClose})});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ VERIFY_ARE_EQUAL(static_cast(1), lines.size());
+ const auto& line = lines[0];
+
+ // The hyperlink open sequence must be present (starts the clickable region)
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L"\x1b]8;;https://example.com\x1b\\"));
+ // The hyperlink close sequence must also be present (terminates the clickable region)
+ // Find the close AFTER the open
+ auto openEnd = line.find(L"\x1b]8;;https://example.com\x1b\\");
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, openEnd);
+ auto closePos = line.find(L"\x1b]8;;\x1b\\", openEnd + 1);
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, closePos);
+ // Truncation occurred — full text should NOT be present
+ VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"click here now"));
+ // Ellipsis should be present
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\u2026'));
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_BlankLineEmittedBetweenRows)
+ {
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"row-a", L"val-a"});
+ cap.table.WriteLine();
+ cap.table.WriteRow({L"row-b", L"val-b"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ // 3 lines: data, blank, data
+ VERIFY_ARE_EQUAL(static_cast(3), lines.size());
+ VERIFY_IS_TRUE(lines[0].find(L"row-a") != std::wstring::npos);
+ VERIFY_IS_TRUE(lines[1].empty());
+ VERIFY_IS_TRUE(lines[2].find(L"row-b") != std::wstring::npos);
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_SectionHeaderEmittedBetweenRows)
+ {
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"opt-a", L"desc-a"});
+ cap.table.WriteLine(FormattedCell{L"Global Options:"});
+ cap.table.WriteRow({L"opt-b", L"desc-b"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ VERIFY_ARE_EQUAL(static_cast(3), lines.size());
+ VERIFY_IS_TRUE(lines[0].find(L"opt-a") != std::wstring::npos);
+ VERIFY_ARE_EQUAL(std::wstring{L"Global Options:"}, lines[1]);
+ VERIFY_IS_TRUE(lines[2].find(L"opt-b") != std::wstring::npos);
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_DoesNotAffectColumnWidths)
+ {
+ // The break text is longer than any data cell; column widths should
+ // be driven only by data rows, not breaks.
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteRow({L"ab", L"cd"});
+ cap.table.WriteLine(FormattedCell{L"This is a very long section header that should not widen columns"});
+ cap.table.WriteRow({L"ef", L"gh"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ VERIFY_ARE_EQUAL(static_cast(3), lines.size());
+ // Both data rows should have the same width (driven by "ab"/"ef" column)
+ VERIFY_ARE_EQUAL(lines[0].size(), lines[2].size());
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_SharedColumnWidthsAcrossSections)
+ {
+ // Data rows in different sections share column widths because they
+ // are sized together within a single table instance.
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteLine(FormattedCell{L"Section A:"});
+ cap.table.WriteRow({L"short", L"x"});
+ cap.table.WriteLine(FormattedCell{L"Section B:"});
+ cap.table.WriteRow({L"much-longer-name", L"y"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ // 4 lines: header, data, header, data
+ VERIFY_ARE_EQUAL(static_cast(4), lines.size());
+
+ // "short" row should be padded to match "much-longer-name" column width.
+ // Both data rows have the same total width.
+ VERIFY_ARE_EQUAL(lines[1].size(), lines[3].size());
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_FormattedCellRendersSequences)
+ {
+ using namespace wsl::windows::common::vt;
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, TableOutput<2>::column_config_t{}, true);
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
+ cap.table.WriteRow({L"name", L"desc"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ VERIFY_ARE_EQUAL(static_cast(2), lines.size());
+ // The section header should contain VT sequences (Bright + Default reset)
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
+ VERIFY_IS_TRUE(lines[0].find(L"Options:") != std::wstring::npos);
+ }
+
+ TEST_METHOD(TableOutput_WriteLine_FormattedCellStrippedWhenVTDisabled)
+ {
+ using namespace wsl::windows::common::vt;
+ TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
+ cap.table.SetShowHeader(false);
+
+ cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
+ cap.table.WriteRow({L"name", L"desc"});
+ cap.table.Complete();
+
+ auto lines = cap.lines();
+ VERIFY_ARE_EQUAL(static_cast(2), lines.size());
+ // VT disabled: no escape sequences, just the text
+ VERIFY_ARE_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
+ VERIFY_ARE_EQUAL(std::wstring{L"Options:"}, lines[0]);
+ }
};
-} // namespace WSLCTableOutputUnitTests
\ No newline at end of file
+} // namespace WSLCCLITableOutputUnitTests
diff --git a/test/windows/wslc/WSLCCLITestHelpers.h b/test/windows/wslc/WSLCCLITestHelpers.h
index fe32cd6c60..c35fca1275 100644
--- a/test/windows/wslc/WSLCCLITestHelpers.h
+++ b/test/windows/wslc/WSLCCLITestHelpers.h
@@ -151,16 +151,61 @@ struct CaptureReporter
template
struct TableOutputCapture
{
- std::vector lines;
+ CaptureReporter capture;
wsl::windows::wslc::TableOutput table;
- // Forwards constructor arguments straight to TableOutput.
- template
- explicit TableOutputCapture(Args&&... args) : table(std::forward(args)...)
+ // Header + optional config + optional VT flag.
+ explicit TableOutputCapture(
+ typename wsl::windows::wslc::TableOutput::header_t&& header,
+ size_t sizingBuffer = 50,
+ size_t columnPadding = wsl::windows::wslc::TableOutput::DefaultColumnPadding,
+ bool vtEnabled = false) :
+ capture(vtEnabled), table(capture.reporter, std::move(header), sizingBuffer, columnPadding)
{
- table.SetOutputFunction([this](const std::wstring& line) { lines.push_back(line); });
- // Pin the console width so shrinking tests are deterministic.
table.SetConsoleWidthOverride(120);
}
+
+ // Header + column configs + optional VT flag.
+ explicit TableOutputCapture(
+ typename wsl::windows::wslc::TableOutput::header_t&& header,
+ typename wsl::windows::wslc::TableOutput::column_config_t&& configs,
+ bool vtEnabled = false) :
+ capture(vtEnabled),
+ table(capture.reporter, std::move(header), std::move(configs), 50, wsl::windows::wslc::TableOutput::DefaultColumnPadding)
+ {
+ table.SetConsoleWidthOverride(120);
+ }
+
+ // Column definitions.
+ explicit TableOutputCapture(typename wsl::windows::wslc::TableOutput::column_def_t&& defs, bool vtEnabled = false) :
+ capture(vtEnabled), table(capture.reporter, std::move(defs))
+ {
+ table.SetConsoleWidthOverride(120);
+ }
+
+ // Returns captured output split into lines.
+ std::vector lines()
+ {
+ auto raw = capture.captured();
+ std::vector result;
+ size_t pos = 0;
+ while (pos < raw.size())
+ {
+ auto nl = raw.find(L'\n', pos);
+ if (nl == std::wstring::npos)
+ {
+ result.emplace_back(raw.substr(pos));
+ break;
+ }
+ result.emplace_back(raw.substr(pos, nl - pos));
+ pos = nl + 1;
+ }
+ // Remove trailing empty entry from final newline.
+ if (!result.empty() && result.back().empty())
+ {
+ result.pop_back();
+ }
+ return result;
+ }
};
} // namespace WSLCTestHelpers
\ No newline at end of file
diff --git a/test/windows/wslc/e2e/WSLCE2EAliasTests.cpp b/test/windows/wslc/e2e/WSLCE2EAliasTests.cpp
index f034b91010..331ab9ceb5 100644
--- a/test/windows/wslc/e2e/WSLCE2EAliasTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EAliasTests.cpp
@@ -65,7 +65,15 @@ class WSLCE2EAliasTests
const auto containerResult = RunContainerExe(L"--help");
containerResult.Verify({.Stderr = L"", .ExitCode = 0});
- VERIFY_ARE_EQUAL(wslcResult.Stdout.value(), containerResult.Stdout.value());
+ // Help output should be identical except the executable name in the usage line.
+ auto wslcOutput = wslcResult.Stdout.value();
+ const std::wstring usageNeedle = L"Usage: wslc";
+ const std::wstring usageReplacement = L"Usage: container";
+ auto pos = wslcOutput.find(usageNeedle);
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, pos);
+ wslcOutput.replace(pos, usageNeedle.size(), usageReplacement);
+
+ VERIFY_ARE_EQUAL(wslcOutput, containerResult.Stdout.value());
}
};
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
index 0d1cd6e539..350113a162 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
@@ -44,7 +44,8 @@ class WSLCE2EContainerAttachTests
WSLC_TEST_METHOD(WSLCE2E_Container_Attach_HelpCommand)
{
auto result = RunWslc(L"container attach --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Attach_TTY)
@@ -107,7 +108,8 @@ class WSLCE2EContainerAttachTests
WSLC_TEST_METHOD(WSLCE2E_Container_Attach_MissingContainerId)
{
auto result = RunWslc(L"container attach");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Attach_ContainerNotFound)
@@ -121,44 +123,5 @@ class WSLCE2EContainerAttachTests
private:
const std::wstring WslcContainerName = L"wslc-test-container";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return L"Attaches to a container.\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container attach [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" container-id Container ID\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
index 582e485316..dedffccc5b 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
@@ -75,13 +75,15 @@ class WSLCE2EContainerCreateTests
WSLC_TEST_METHOD(WSLCE2E_Container_Create_HelpCommand)
{
auto result = RunWslc(L"container create --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_MissingImage)
{
auto result = RunWslc(L"container create --name " + WslcContainerName);
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_InvalidImage)
@@ -316,75 +318,110 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(
std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(
std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: ''. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid volume specifications: ''. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). "
+ L"Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: ':ro'. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid volume specifications: ':ro'. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(
std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute path (starting with '/'). Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute "
+ L"path (starting with '/'). Expected format: :[:mode]\r\nError code: "
+ L"E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path (starting with '/'). Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path "
+ L"(starting with '/'). Expected format: :[:mode]\r\nError code: "
+ L"E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected "
+ L"format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -392,7 +429,10 @@ class WSLCE2EContainerCreateTests
// "::/container:ro" - host=":", container="/container". ":" is not a valid Windows path.
auto result = RunWslc(
std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid "
+ L"Windows path.\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -405,13 +445,19 @@ class WSLCE2EContainerCreateTests
{
auto result = RunWslc(
std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). "
+ L"Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -419,14 +465,20 @@ class WSLCE2EContainerCreateTests
// "::" splits as host=":", container="". Container path empty check fires first.
auto result =
RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid volume specifications: 'e2e_test'. Expected format: :[:mode]\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid volume specifications: 'e2e_test'. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -613,14 +665,18 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
{
auto result =
RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
@@ -742,14 +798,18 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container create --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
VerifyContainerIsNotListed(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container create --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
VerifyContainerIsNotListed(WslcContainerName);
}
}
@@ -759,21 +819,26 @@ class WSLCE2EContainerCreateTests
{
auto result = RunWslc(
std::format(L"container create --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
+ L"(Example: SIGKILL, kill, or 9)."));
VerifyContainerIsNotListed(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container create --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid stop-signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
VerifyContainerIsNotListed(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container create --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid stop-signal value: 99 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
VerifyContainerIsNotListed(WslcContainerName);
}
}
@@ -791,7 +856,8 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -799,7 +865,8 @@ class WSLCE2EContainerCreateTests
{
auto result = RunWslc(std::format(
L"container create --name {} --network bridge --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -820,7 +887,8 @@ class WSLCE2EContainerCreateTests
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_EmptyValue_Rejected)
{
auto result = RunWslc(std::format(L"container create --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -877,7 +945,10 @@ class WSLCE2EContainerCreateTests
L"container create --network bridge --network bridge --network-alias db --name {} {} true",
WslcContainerName,
DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
+ L"single --network argument.\r\nError code: E_INVALIDARG"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -885,7 +956,9 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container create --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid network-alias value: network alias cannot be empty or whitespace\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -901,7 +974,9 @@ class WSLCE2EContainerCreateTests
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus_Invalid)
{
auto result = RunWslc(std::format(L"container create --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -920,7 +995,9 @@ class WSLCE2EContainerCreateTests
{
auto result =
RunWslc(std::format(L"container create --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -951,8 +1028,9 @@ class WSLCE2EContainerCreateTests
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit_Invalid)
{
auto result = RunWslc(std::format(L"container create --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify(
- {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -986,8 +1064,9 @@ class WSLCE2EContainerCreateTests
{
auto result = RunWslc(std::format(
L"container create --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify(
- {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -997,7 +1076,9 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(std::format(
L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -1132,73 +1213,5 @@ class WSLCE2EContainerCreateTests
// Test volume files
std::filesystem::path VolumeTestFile1;
std::filesystem::path VolumeTestFile2;
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerCreateLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container create [] [] [...]\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n"
- << L" image Image name\r\n"
- << L" command The command to run\r\n"
- << L" arguments Arguments to pass to container's init process\r\n\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" --cidfile Write the container ID to the provided path\r\n"
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
- << L" --dns-option Set DNS options\r\n"
- << L" --dns-search Set DNS search domains\r\n"
- << L" --domainname Container domain name\r\n"
- << L" --entrypoint Specifies the container init process executable\r\n"
- << L" -e,--env Key=Value pairs for environment variables\r\n"
- << L" --env-file File containing key=value pairs of env variables\r\n"
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
- << L" -h,--hostname Container host name\r\n"
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
- << L" -l,--label Set metadata on an object\r\n"
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
- << L" --name Name of the container\r\n"
- << L" --network Connect a container to a network\r\n"
- << L" --network-alias Add a network-scoped alias for the container\r\n"
- << L" -p,--publish Publish a port from a container to host\r\n"
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
- << L" --rm Remove the container after it stops\r\n"
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
- << L" --stop-signal Signal to stop the container\r\n"
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
- << L" -t,--tty Open a TTY with the container process.\r\n"
- << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
- << L" -v,--volume Bind mount a volume to the container\r\n"
- << L" -w,--workdir Working directory inside the container\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
index 64bba794a8..f35011c4e4 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
@@ -61,13 +61,15 @@ class WSLCE2EContainerExecTests
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_HelpCommand)
{
auto result = RunWslc(L"container exec --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingContainerId)
{
auto result = RunWslc(L"container exec");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_MissingCommand)
@@ -76,7 +78,8 @@ class WSLCE2EContainerExecTests
result.Verify({.Stderr = L"", .ExitCode = 0});
result = RunWslc(std::format(L"container exec {}", WslcContainerName));
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'command'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'command'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_ContainerNotFound)
@@ -265,8 +268,9 @@ class WSLCE2EContainerExecTests
result.Verify({.Stderr = L"", .ExitCode = 0});
result = RunWslc(std::format(L"container exec --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName));
- result.Verify(
- {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MultipleFiles)
@@ -295,7 +299,9 @@ class WSLCE2EContainerExecTests
result.Verify({.Stderr = L"", .ExitCode = 0});
result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName));
- result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_DuplicateKeys_Precedence)
@@ -443,53 +449,5 @@ class WSLCE2EContainerExecTests
// Test environment variable files
std::filesystem::path EnvTestFile1;
std::filesystem::path EnvTestFile2;
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return L"Executes a command in a running container.\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container exec [] [...]\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n"
- << L" container-id Container ID\r\n"
- << L" command The command to run\r\n"
- << L" arguments Arguments to pass to the command being executed inside the container\r\n"
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -d,--detach Run container in detached mode\r\n"
- << L" -e,--env Key=Value pairs for environment variables\r\n"
- << L" --env-file File containing key=value pairs of env variables\r\n"
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
- << L" -t,--tty Open a TTY with the container process.\r\n"
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
- << L" -w,--workdir Working directory inside the container\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp
index 44c94c4684..3bb3e9ca02 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp
@@ -54,13 +54,15 @@ class WSLCE2EContainerExportTests
WSLC_TEST_METHOD(WSLCE2E_Container_Export_HelpCommand)
{
auto result = RunWslc(L"container export --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Export_MissingContainerId)
{
const auto result = RunWslc(std::format(L"container export --output \"{}\"", ExportPath.wstring()));
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Export_ContainerNotFound)
@@ -103,45 +105,5 @@ class WSLCE2EContainerExportTests
const TestImage& DebianImage = DebianTestImage();
std::filesystem::path ExportPath{};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerExportLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container export [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" container-id Container ID\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -o,--output Write to a file, instead of STDOUT\r\n" //
- << L" -?,--help Shows help about the selected command\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp
index abd97240fc..566d48b811 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerInspectTests.cpp
@@ -49,13 +49,15 @@ class WSLCE2EContainerInspectTests
WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_HelpCommand)
{
auto result = RunWslc(L"container inspect --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_MissingContainerId)
{
auto result = RunWslc(L"container inspect");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'container-id'"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Inspect_ContainerNotFound)
@@ -116,44 +118,5 @@ class WSLCE2EContainerInspectTests
const std::wstring TestContainerName1 = L"wslc-e2e-container-inspect-1";
const std::wstring TestContainerName2 = L"wslc-e2e-container-inspect-2";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerInspectLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container inspect [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" container-id Container ID\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -?,--help Shows help about the selected command\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp
index a8e6ac333b..048d1d12ea 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerKillTests.cpp
@@ -47,7 +47,8 @@ class WSLCE2EContainerKillTests
WSLC_TEST_METHOD(WSLCE2E_Container_Kill_HelpCommand)
{
auto result = RunWslc(L"container kill --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Kill_KillsRunningContainer)
@@ -105,12 +106,14 @@ class WSLCE2EContainerKillTests
{
result = RunWslc(std::format(L"container kill {} -s 0", WslcContainerName));
- result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 0 is out of valid range (1-31)."));
}
{
result = RunWslc(std::format(L"container kill {} -s 32", WslcContainerName));
- result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 32 is out of valid range (1-31)."));
}
}
@@ -145,43 +148,5 @@ class WSLCE2EContainerKillTests
const std::wstring WslcContainerName = L"wslc-test-container";
const std::wstring WslcContainerName2 = L"wslc-test-container-2";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerKillLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container kill [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -s,--signal Signal to send\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
index 10e231f2eb..7e3b28a3ef 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
@@ -51,7 +51,8 @@ class WSLCE2EContainerListTests
WSLC_TEST_METHOD(WSLCE2E_Container_List_HelpCommand)
{
auto result = RunWslc(L"container list --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_List_AllOption)
@@ -157,7 +158,9 @@ class WSLCE2EContainerListTests
WSLC_TEST_METHOD(WSLCE2E_Container_List_InvalidFormatOption)
{
const auto result = RunWslc(L"container list --format invalid");
- result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
}
WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat)
@@ -213,7 +216,8 @@ class WSLCE2EContainerListTests
{
// Filter values must be of the form key=value; bare keys are rejected by the CLI.
const auto result = RunWslc(L"container list --filter status");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"status") + L"\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status")));
}
WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue)
@@ -489,47 +493,5 @@ class WSLCE2EContainerListTests
const std::wstring WslcContainerName = L"wslc-test-container";
const std::wstring WslcContainerName2 = L"wslc-test-container-2";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommandAliases() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerListLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container list []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommandAliases() const
- {
- return L"The following command aliases are available: ls ps\r\n\r\n";
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -a,--all Show all regardless of state.\r\n"
- << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
- << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
- << L" -n,--last " << Localization::WSLCCLI_LastArgDescription() << L"\r\n"
- << L" -l,--latest " << Localization::WSLCCLI_LatestArgDescription() << L"\r\n"
- << L" --no-trunc Do not truncate output\r\n"
- << L" -q,--quiet Outputs the container IDs only\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
\ No newline at end of file
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
index 295062712d..5a50f784a4 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
@@ -45,7 +45,8 @@ class WSLCE2EContainerPruneTests
WSLC_TEST_METHOD(WSLCE2E_Container_Prune_HelpCommand)
{
const auto result = RunWslc(L"container prune --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers)
@@ -123,34 +124,5 @@ class WSLCE2EContainerPruneTests
private:
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerPruneLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container prune []\r\n\r\n";
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
index 3caa7c9497..14d4246c07 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
@@ -47,7 +47,8 @@ class WSLCE2EContainerRemoveTests
WSLC_TEST_METHOD(WSLCE2E_Container_Remove_HelpCommand)
{
auto result = RunWslc(L"container remove --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Remove_NotFound)
@@ -160,49 +161,5 @@ class WSLCE2EContainerRemoveTests
const std::wstring WslcContainerName = L"wslc-test-container";
const std::wstring WslcContainerName2 = L"wslc-test-container-2";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommandAliases() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerRemoveLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container remove [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommandAliases() const
- {
- return L"The following command aliases are available: delete rm\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -f,--force Delete containers even if they are running\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
\ No newline at end of file
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
index 23dd502e4c..30d4350c11 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
@@ -76,7 +76,8 @@ class WSLCE2EContainerRunTests
WSLC_TEST_METHOD(WSLCE2E_Container_Run_HelpCommand)
{
auto result = RunWslc(L"container run --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Container_With_Command)
@@ -292,8 +293,9 @@ class WSLCE2EContainerRunTests
auto result = RunWslc(std::format(
L"container run --rm --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify(
- {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_InvalidContent)
@@ -307,7 +309,9 @@ class WSLCE2EContainerRunTests
WslcContainerName,
EscapePath(EnvTestFile1.wstring()),
DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_DuplicateKeys_Precedence)
@@ -615,13 +619,17 @@ class WSLCE2EContainerRunTests
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_RelativePath_Fails)
{
auto result = RunWslc(std::format(L"container run --rm --tmpfs wslc-tmpfs {}", DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_EmptyDestination_Fails)
{
auto result = RunWslc(std::format(L"container run --rm --tmpfs :size=64k {}", DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir)
@@ -680,7 +688,8 @@ class WSLCE2EContainerRunTests
{
auto result =
RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"host mode networking is not supported"));
VerifyContainerIsNotListed(WslcContainerName);
}
@@ -703,7 +712,8 @@ class WSLCE2EContainerRunTests
{
auto result =
RunWslc(std::format(L"container run --rm --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_NonexistentNetwork_Rejected)
@@ -756,14 +766,19 @@ class WSLCE2EContainerRunTests
L"container run --rm --network bridge --network bridge --network-alias db --name {} {} true",
WslcContainerName,
DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Network aliases cannot be specified when multiple networks are requested. Use a "
+ L"single --network argument.\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_EmptyValue_Rejected)
{
auto result = RunWslc(
std::format(L"container run --rm --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid network-alias value: network alias cannot be empty or whitespace\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
@@ -838,14 +853,18 @@ class WSLCE2EContainerRunTests
{
auto result =
RunWslc(std::format(L"container run --rm --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid shm-size argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --rm --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid shm-size argument value: '128X'. Expected a memory size (e.g. 256M, 1G)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -897,7 +916,9 @@ class WSLCE2EContainerRunTests
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
{
auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -905,7 +926,9 @@ class WSLCE2EContainerRunTests
{
auto result =
RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -913,8 +936,9 @@ class WSLCE2EContainerRunTests
{
auto result =
RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify(
- {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)"));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -923,21 +947,26 @@ class WSLCE2EContainerRunTests
{
auto result = RunWslc(
std::format(L"container run --rm --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(
+ result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
+ L"(Example: SIGKILL, kill, or 9)."));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --rm --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid stop-signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result =
RunWslc(std::format(L"container run --rm --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
- result.Verify({.Stderr = L"Invalid stop-signal value: 99 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -971,76 +1000,5 @@ class WSLCE2EContainerRunTests
// Test user-defined network
const std::wstring TestNetworkName = L"wslc-test-network";
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return L"Runs a container. By default, the container is started in the foreground; use --detach to run in the "
- L"background.\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container run [] [] [...]\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n"
- << L" image Image name\r\n"
- << L" command The command to run\r\n"
- << L" arguments Arguments to pass to container's init process\r\n"
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" --cidfile Write the container ID to the provided path\r\n"
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
- << L" -d,--detach Run container in detached mode\r\n"
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
- << L" --dns-option Set DNS options\r\n"
- << L" --dns-search Set DNS search domains\r\n"
- << L" --domainname Container domain name\r\n"
- << L" --entrypoint Specifies the container init process executable\r\n"
- << L" -e,--env Key=Value pairs for environment variables\r\n"
- << L" --env-file File containing key=value pairs of env variables\r\n"
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
- << L" -h,--hostname Container host name\r\n"
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
- << L" -l,--label Set metadata on an object\r\n"
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
- << L" --name Name of the container\r\n"
- << L" --network Connect a container to a network\r\n"
- << L" --network-alias Add a network-scoped alias for the container\r\n"
- << L" -p,--publish Publish a port from a container to host\r\n"
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
- << L" --rm Remove the container after it stops\r\n"
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
- << L" --stop-signal Signal to stop the container\r\n"
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
- << L" -t,--tty Open a TTY with the container process.\r\n"
- << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
- << L" -v,--volume Bind mount a volume to the container\r\n"
- << L" -w,--workdir Working directory inside the container\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
index 0cef70d0d8..9c1de74fc6 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp
@@ -47,7 +47,8 @@ class WSLCE2EContainerStopTests
WSLC_TEST_METHOD(WSLCE2E_Container_Stop_HelpCommand)
{
auto result = RunWslc(L"container stop --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_Stop_InvalidSignal)
@@ -57,12 +58,14 @@ class WSLCE2EContainerStopTests
{
result = RunWslc(std::format(L"container stop {} -s 0 -t 0", WslcContainerName));
- result.Verify({.Stderr = L"Invalid signal value: 0 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 0 is out of valid range (1-31)."));
}
{
result = RunWslc(std::format(L"container stop {} -s 32 -t 0", WslcContainerName));
- result.Verify({.Stderr = L"Invalid signal value: 32 is out of valid range (1-31).\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid signal value: 32 is out of valid range (1-31)."));
}
}
@@ -192,7 +195,9 @@ class WSLCE2EContainerStopTests
// Try to stop with an invalid signal name
result = RunWslc(std::format(L"container stop {} -s SIGINVALID -t 0", containerId));
- 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});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid signal value: SIGINVALID is not a recognized signal name or number (Example: SIGKILL, kill, or 9)."));
// Verify container is still running after failed stop request
VerifyContainerIsListed(containerId, L"running");
@@ -212,7 +217,8 @@ class WSLCE2EContainerStopTests
{
// Invalid integer
result = RunWslc(std::format(L"container stop {} -t abc", containerId));
- result.Verify({.Stderr = L"Invalid time argument value: abc\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: abc"));
// Should still be running after failed stop
VerifyContainerIsListed(containerId, L"running");
@@ -221,7 +227,8 @@ class WSLCE2EContainerStopTests
{
// Another invalid integer shape
result = RunWslc(std::format(L"container stop {} -t 1.5", containerId));
- result.Verify({.Stderr = L"Invalid time argument value: 1.5\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 1.5"));
// Should still be running after failed stop
VerifyContainerIsListed(containerId, L"running");
@@ -230,7 +237,8 @@ class WSLCE2EContainerStopTests
{
// Invalid integer prefixed
result = RunWslc(std::format(L"container stop {} -t 9abc", containerId));
- result.Verify({.Stderr = L"Invalid time argument value: 9abc\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid time argument value: 9abc"));
// Should still be running after failed stop
VerifyContainerIsListed(containerId, L"running");
@@ -260,44 +268,5 @@ class WSLCE2EContainerStopTests
const std::wstring WslcContainerName = L"wslc-test-container";
const std::wstring WslcContainerName2 = L"wslc-test-container-2";
const TestImage& DebianImage = DebianTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerStopLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container stop [] []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" << L" container-id Container ID\r\n" << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -s,--signal Signal to send\r\n"
- << L" -t,--time Time in seconds to wait before executing (default 5)\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
index 09caf623cf..4bf743bc4d 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
@@ -36,77 +36,16 @@ class WSLCE2EContainerTests
WSLC_TEST_METHOD(WSLCE2E_Container_HelpCommand)
{
- RunWslc(L"container --help").Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ auto result = RunWslc(L"container --help");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Container_InvalidCommand_DisplaysErrorMessage)
{
- RunWslc(L"container INVALID_CMD").Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
- }
-
-private:
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ContainerCommandLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc container [] []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::vector> entries = {
- {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
- {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
- {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
- {L"export", Localization::WSLCCLI_ContainerExportDesc()},
- {L"inspect", Localization::WSLCCLI_ContainerInspectDesc()},
- {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
- {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
- {L"list", Localization::WSLCCLI_ContainerListDesc()},
- {L"prune", Localization::WSLCCLI_ContainerPruneDesc()},
- {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
- {L"run", Localization::WSLCCLI_ContainerRunDesc()},
- {L"start", Localization::WSLCCLI_ContainerStartDesc()},
- {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
- {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
- };
-
- size_t maxLen = 0;
- for (const auto& [name, _] : entries)
- {
- maxLen = (std::max)(maxLen, name.size());
- }
-
- std::wstringstream commands;
- commands << Localization::WSLCCLI_AvailableSubcommands() << L"\r\n";
- for (const auto& [name, desc] : entries)
- {
- commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
- }
- commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -?,--help Shows help about the selected command\r\n\r\n";
- return options.str();
+ auto result = RunWslc(L"container INVALID_CMD");
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
}
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
index 500f7b678a..13e5f3772d 100644
--- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
@@ -58,12 +58,49 @@ class WSLCE2EGlobalTests
WSLC_TEST_METHOD(WSLCE2E_HelpCommand)
{
- RunWslcAndVerify(L"--help", {.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ auto result = RunWslc(L"--help");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_InvalidCommand_DisplaysErrorMessage)
{
- RunWslcAndVerify(L"INVALID_CMD", {.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
+ auto result = RunWslc(L"INVALID_CMD");
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Help_RoutesToStdout)
+ {
+ auto result = RunWslc(L"--help");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Usage: wslc"));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Help_ErrorRoutesToStderr)
+ {
+ // Help on error must land on stderr; stdout must remain empty.
+ auto result = RunWslc(L"INVALID_CMD");
+ VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc"));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Help_NoColorWhenRedirected)
+ {
+ // Captured via anonymous pipe; Reporter must suppress VT escape sequences.
+ auto result = RunWslc(L"--help");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_ARE_EQUAL(std::wstring::npos, result.Stdout.value().find(L'\x1b'));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Help_ColorOnTerminal)
+ {
+ // Pseudo console reports VT support; Reporter should emit SGR sequences.
+ auto session = RunWslcInteractive(L"--help", ElevationType::Elevated, PseudoConsole{120, 30});
+ session.WaitForExit();
+ VERIFY_IS_TRUE(session.GetStdoutData().find('\x1b') != std::string::npos);
}
WSLC_TEST_METHOD(WSLCE2E_VersionCommand)
@@ -535,94 +572,9 @@ class WSLCE2EGlobalTests
}
private:
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
std::wstring GetVersionMessage() const
{
return std::format(L"wslc {}\r\n", WSL_PACKAGE_VERSION);
}
-
- std::wstring GetDescription() const
- {
- return L"WSLC is the Windows Subsystem for Linux Container CLI tool. It enables management and interaction with WSL "
- L"containers from the command line.\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc [] []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::vector> entries = {
- {L"container", Localization::WSLCCLI_ContainerCommandDesc()},
- {L"image", Localization::WSLCCLI_ImageCommandDesc()},
- {L"network", Localization::WSLCCLI_NetworkCommandDesc()},
- {L"registry", Localization::WSLCCLI_RegistryCommandDesc()},
- {L"settings", Localization::WSLCCLI_SettingsCommandDesc()},
- {L"system", Localization::WSLCCLI_SystemCommandDesc()},
- {L"volume", Localization::WSLCCLI_VolumeCommandDesc()},
- {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
- {L"build", Localization::WSLCCLI_ImageBuildDesc()},
- {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
- {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
- {L"export", Localization::WSLCCLI_ContainerExportDesc()},
- {L"images", Localization::WSLCCLI_ImageListDesc()},
- {L"import", Localization::WSLCCLI_ImageImportDesc()},
- {L"inspect", Localization::WSLCCLI_InspectDesc()},
- {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
- {L"list", Localization::WSLCCLI_ContainerListDesc()},
- {L"load", Localization::WSLCCLI_ImageLoadDesc()},
- {L"login", Localization::WSLCCLI_LoginDesc()},
- {L"logout", Localization::WSLCCLI_LogoutDesc()},
- {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
- {L"pull", Localization::WSLCCLI_ImagePullDesc()},
- {L"push", Localization::WSLCCLI_ImagePushDesc()},
- {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
- {L"rmi", Localization::WSLCCLI_ImageRemoveDesc()},
- {L"run", Localization::WSLCCLI_ContainerRunDesc()},
- {L"save", Localization::WSLCCLI_ImageSaveDesc()},
- {L"start", Localization::WSLCCLI_ContainerStartDesc()},
- {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
- {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
- {L"tag", Localization::WSLCCLI_ImageTagDesc()},
- {L"version", Localization::WSLCCLI_VersionDesc()},
- };
-
- size_t maxLen = 0;
- for (const auto& [name, _] : entries)
- {
- maxLen = (std::max)(maxLen, name.size());
- }
-
- std::wstringstream commands;
- commands << Localization::WSLCCLI_AvailableCommands() << L"\r\n";
- for (const auto& [name, desc] : entries)
- {
- commands << L" " << name << std::wstring(maxLen - name.size() + 2, L' ') << desc << L"\r\n";
- }
- commands << L"\r\n" << Localization::WSLCCLI_HelpForDetails() << L" [" << WSLC_CLI_HELP_ARG_STRING << L"]\r\n\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -v,--version Show version information for this tool\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
index a4142b90a8..c4d239fd1b 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
@@ -44,7 +44,8 @@ class WSLCE2EImageDeleteTests
WSLC_TEST_METHOD(WSLCE2E_Image_Delete_HelpCommand)
{
auto result = RunWslc(L"image delete --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Delete_ImageNotFound)
@@ -57,7 +58,8 @@ class WSLCE2EImageDeleteTests
WSLC_TEST_METHOD(WSLCE2E_Image_Delete_MissingImageName)
{
auto result = RunWslc(L"image delete");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Delete_UnusedImage_Success)
@@ -150,52 +152,5 @@ class WSLCE2EImageDeleteTests
const TestImage& AlpineImage = AlpineTestImage();
const TestImage& InvalidImage = InvalidTestImage();
const TestImage NoPruneTaggedImage{L"wslc-test-noprune", L"alias", L""};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommandAliases() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImageRemoveLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image remove [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommandAliases() const
- {
- return L"The following command aliases are available: delete rm\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" image Image name\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -f,--force Delete images even if they are being used\r\n" //
- << L" --no-prune Do not delete untagged parents\r\n" //
- << L" -?,--help Shows help about the selected command\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
index 89cd6e3773..54f7ca770d 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
@@ -47,13 +47,15 @@ class WSLCE2EImageImportTests
WSLC_TEST_METHOD(WSLCE2E_Image_Import_HelpCommand)
{
auto result = RunWslc(L"image import --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile)
{
const auto result = RunWslc(L"image import");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'file'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'file'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success)
@@ -102,45 +104,5 @@ class WSLCE2EImageImportTests
const TestImage ImportedImage{L"wslc-test-imported", L"latest", L""};
std::filesystem::path SavedArchivePath{};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImageImportLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image import [] []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" //
- << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
index 6fb5240c72..ce45cdb1f6 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
@@ -40,13 +40,15 @@ class WSLCE2EImageInspectTests
WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_HelpCommand)
{
auto result = RunWslc(L"image inspect --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_MissingImageName)
{
auto result = RunWslc(L"image inspect");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_ImageNotFound)
@@ -126,44 +128,5 @@ class WSLCE2EImageInspectTests
const TestImage& DebianImage = DebianTestImage();
const TestImage& InvalidImage = InvalidTestImage();
const TestImage BuiltExposeImage{L"wslc-e2e-inspect-config-extras", L"latest", L""};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImageInspectLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image inspect [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" image Image name\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -?,--help Shows help about the selected command\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
\ No newline at end of file
diff --git a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
index 04ae813c6c..9c24eddb83 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
@@ -43,7 +43,8 @@ class WSLCE2EImageListTests
WSLC_TEST_METHOD(WSLCE2E_Image_List_HelpCommand)
{
const auto result = RunWslc(L"image list --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_List_DisplayLoadedImage)
@@ -82,7 +83,9 @@ class WSLCE2EImageListTests
WSLC_TEST_METHOD(WSLCE2E_Image_List_InvalidFormatOption)
{
const auto result = RunWslc(L"image list --format invalid");
- result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
}
WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat)
@@ -132,7 +135,8 @@ class WSLCE2EImageListTests
{
// Filter values must be of the form key=value; bare keys are rejected by the CLI.
const auto result = RunWslc(L"image list --filter dangling");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"dangling") + L"\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling")));
}
WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_InvalidKey)
@@ -280,45 +284,5 @@ class WSLCE2EImageListTests
private:
const TestImage& DebianImage = DebianTestImage();
const TestImage& AlpineImage = AlpineTestImage();
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommandAliases() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImageListLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image list []\r\n\r\n";
- }
-
- std::wstring GetAvailableCommandAliases() const
- {
- return L"The following command aliases are available: ls\r\n\r\n";
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
- << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
- << L" --no-trunc Do not truncate output\r\n"
- << L" -q,--quiet Outputs the container IDs only\r\n"
- << L" --verbose Output verbose details\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
\ No newline at end of file
diff --git a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
index 4012c5b353..14cd6d928f 100644
--- a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp
@@ -38,7 +38,8 @@ class WSLCE2EImagePruneTests
WSLC_TEST_METHOD(WSLCE2E_Image_Prune_HelpCommand)
{
const auto result = RunWslc(L"image prune --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Prune_NoDanglingImages)
@@ -114,7 +115,8 @@ class WSLCE2EImagePruneTests
{
// Filter values must be of the form key=value; bare keys are rejected by the CLI.
const auto result = RunWslc(L"image prune --filter label");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey)
@@ -187,36 +189,5 @@ class WSLCE2EImagePruneTests
VERIFY_FAIL(std::format(L"Expected stdout to contain '{}'", substring).c_str());
}
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImagePruneLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image prune []\r\n\r\n";
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" -a,--all " << Localization::WSLCCLI_ImagePruneAllArgDescription() << L"\r\n"
- << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
- << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
index a535d6b5eb..2df00fc8cf 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
@@ -46,13 +46,15 @@ class WSLCE2EImageSaveTests
WSLC_TEST_METHOD(WSLCE2E_Image_Save_HelpCommand)
{
auto result = RunWslc(L"image save --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Save_MissingImageName)
{
const auto result = RunWslc(std::format(L"image save --output \"{}\"", SavedArchivePath.wstring()));
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'image'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Save_ImageNotFound)
@@ -108,8 +110,9 @@ class WSLCE2EImageSaveTests
SKIP_TEST_UNSTABLE();
const auto result = RunWslcAndRedirectToFile(std::format(L"image save {}", DebianImage.NameAndTag()));
- result.Verify(
- {.Stderr = L"Cannot write image to terminal. Use the -o flag or redirect stdout.\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ L"Cannot write image to terminal. Use the -o flag or redirect stdout.\r\nError code: E_INVALIDARG"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Save_ToStdout_Load)
@@ -176,45 +179,5 @@ class WSLCE2EImageSaveTests
const TestImage& InvalidImage = InvalidTestImage();
std::filesystem::path SavedArchivePath{};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return Localization::WSLCCLI_ImageSaveLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image save [] \r\n\r\n";
- }
-
- std::wstring GetAvailableCommands() const
- {
- std::wstringstream commands;
- commands << L"The following arguments are available:\r\n" //
- << L" image Image name\r\n" //
- << L"\r\n";
- return commands.str();
- }
-
- std::wstring GetAvailableOptions() const
- {
- std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" -o,--output Path for the saved image\r\n" //
- << L" -?,--help Shows help about the selected command\r\n" //
- << L"\r\n";
- return options.str();
- }
};
} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp
index 16e5d8b864..8bda9c238a 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageTagTests.cpp
@@ -43,19 +43,22 @@ class WSLCE2EImageTagTests
WSLC_TEST_METHOD(WSLCE2E_Image_Tag_HelpCommand)
{
auto result = RunWslc(L"image tag --help");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_FALSE(result.Stdout.value().empty());
}
WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingSourceAndTarget)
{
auto result = RunWslc(L"image tag");
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'source'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'source'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Tag_MissingTarget)
{
auto result = RunWslc(std::format(L"image tag {}", DebianImage.NameAndTag()));
- result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'target'\r\n", .ExitCode = 1});
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'target'"));
}
WSLC_TEST_METHOD(WSLCE2E_Image_Tag_SourceImageNotFound)
@@ -167,45 +170,5 @@ class WSLCE2EImageTagTests
const TestImage& AlpineImage = AlpineTestImage();
const TestImage& InvalidImage = InvalidTestImage();
const TestImage DebianTaggedImage{L"debian", L"e2e-new-tag"};
-
- std::wstring GetHelpMessage() const
- {
- std::wstringstream output;
- output << GetWslcHeader() //
- << GetDescription() //
- << GetUsage() //
- << GetAvailableCommands() //
- << GetAvailableOptions();
- return output.str();
- }
-
- std::wstring GetDescription() const
- {
- return wsl::shared::Localization::WSLCCLI_ImageTagLongDesc() + L"\r\n\r\n";
- }
-
- std::wstring GetUsage() const
- {
- return L"Usage: wslc image tag [