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 [] \r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" source Current or existing image reference in the image-name[:tag] format\r\n" // - << L" target New image reference in the image-name[:tag] format\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/WSLCE2EImageTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp index 466839b58d..0567fc5957 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp @@ -27,82 +27,22 @@ class WSLCE2EImageTests WSLC_TEST_METHOD(WSLCE2E_Image_HelpCommand) { auto result = RunWslc(L"image --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_NoSubcommand_ShowsHelp) { auto result = RunWslc(L"image"); - 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_InvalidCommand_DisplaysErrorMessage) { auto result = RunWslc(L"image INVALID_CMD"); - result.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_ImageCommandLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc image [] []\r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::vector> entries = { - {L"build", Localization::WSLCCLI_ImageBuildDesc()}, - {L"remove", Localization::WSLCCLI_ImageRemoveDesc()}, - {L"inspect", Localization::WSLCCLI_ImageInspectDesc()}, - {L"list", Localization::WSLCCLI_ImageListDesc()}, - {L"load", Localization::WSLCCLI_ImageLoadDesc()}, - {L"import", Localization::WSLCCLI_ImageImportDesc()}, - {L"prune", Localization::WSLCCLI_ImagePruneDesc()}, - {L"pull", Localization::WSLCCLI_ImagePullDesc()}, - {L"push", Localization::WSLCCLI_ImagePushDesc()}, - {L"save", Localization::WSLCCLI_ImageSaveDesc()}, - {L"tag", Localization::WSLCCLI_ImageTagDesc()}, - }; - - 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" << 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(); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'")); } }; } // namespace WSLCE2ETests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp index 4f822901b8..f9fccd23d1 100644 --- a/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp @@ -51,13 +51,15 @@ class WSLCE2EInspectTests WSLC_TEST_METHOD(WSLCE2E_Inspect_HelpCommand) { auto result = RunWslc(L"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_Inspect_MissingObjectId) { auto result = RunWslc(L"inspect"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'object-id'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'object-id'")); } WSLC_TEST_METHOD(WSLCE2E_Inspect_ObjectNotFound) @@ -288,7 +290,10 @@ class WSLCE2EInspectTests WSLC_TEST_METHOD(WSLCE2E_Inspect_InvalidTypeValue) { auto result = RunWslc(std::format(L"inspect --type invalid {}", DebianImage.NameAndTag())); - result.Verify({.Stderr = L"Invalid type value: invalid is not a recognized inspect type. Supported inspect types are: image, container, network, volume.\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE( + result.StderrContainsSubstring(L"Invalid type value: invalid is not a recognized inspect type. Supported inspect " + L"types are: image, container, network, volume.")); } WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidFormatError) @@ -311,44 +316,5 @@ class WSLCE2EInspectTests const TestImage& InvalidImage = InvalidTestImage(); const std::wstring WslcVolumeName = L"wslc-inspect-test-volume"; const std::wstring WslcNetworkName = L"wslc-inspect-test-network"; - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return Localization::WSLCCLI_InspectLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc inspect [] \r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" object-id Name or Id of any object type\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" -t,--type Type of the object to inspect\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/WSLCE2ENetworkCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp index 8644ba9405..1d8495d765 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp @@ -38,13 +38,15 @@ class WSLCE2ENetworkCreateTests WSLC_TEST_METHOD(WSLCE2E_Network_Create_HelpCommand) { auto result = RunWslc(L"network 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_Network_Create_MissingName) { auto result = RunWslc(L"network create"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'")); } WSLC_TEST_METHOD(WSLCE2E_Network_Create_DefaultDriver_Success) @@ -85,7 +87,8 @@ class WSLCE2ENetworkCreateTests WSLC_TEST_METHOD(WSLCE2E_Network_Create_EmptyLabelKey_Fail) { auto result = RunWslc(std::format(L"network create --driver bridge --label =foo {}", TestNetworkName)); - result.Verify({.Stdout = L"", .Stderr = L"Label key cannot be empty\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Label key cannot be empty\r\nError code: E_INVALIDARG")); VerifyNetworkIsNotListed(TestNetworkName); } @@ -93,8 +96,9 @@ class WSLCE2ENetworkCreateTests WSLC_TEST_METHOD(WSLCE2E_Network_Create_InvalidDriver_Fail) { auto result = RunWslc(std::format(L"network create --driver invalid_driver {}", TestNetworkName)); - result.Verify( - {.Stdout = L"", .Stderr = std::format(L"Unsupported network driver: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n"), .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring( + std::format(L"Unsupported network driver: 'invalid_driver'\r\nError code: E_INVALIDARG"))); VerifyNetworkIsNotListed(TestNetworkName); } @@ -111,47 +115,5 @@ class WSLCE2ENetworkCreateTests private: const std::wstring TestNetworkName = L"wslc-e2e-network-create"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkCreateLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network create [] \r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" network-name Network 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" -d,--driver Specify network driver name (default: bridge)\r\n" // - << L" -o,--opt Set driver specific options\r\n" // - << L" -l,--label Network metadata setting\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/WSLCE2ENetworkInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp index 3f64ad4b55..cde6e506fd 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp @@ -41,13 +41,15 @@ class WSLCE2ENetworkInspectTests WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_HelpCommand) { auto result = RunWslc(L"network 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_Network_Inspect_MissingNetworkName) { auto result = RunWslc(L"network inspect"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'")); } WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_Success) @@ -113,44 +115,5 @@ class WSLCE2ENetworkInspectTests private: const std::wstring TestNetworkName1 = L"wslc-e2e-network-inspect-1"; const std::wstring TestNetworkName2 = L"wslc-e2e-network-inspect-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkInspectLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network inspect [] \r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" network-name Network 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 diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp index 37fdf51f7d..761153cba3 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -41,13 +41,16 @@ class WSLCE2ENetworkListTests WSLC_TEST_METHOD(WSLCE2E_Network_List_HelpCommand) { auto result = RunWslc(L"network 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_Network_List_InvalidFormatOption) { auto result = RunWslc(L"network 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_Network_List_QuietOption_OutputsNamesOnly) @@ -92,42 +95,5 @@ class WSLCE2ENetworkListTests private: const std::wstring TestNetworkName = L"wslc-e2e-network-list"; const std::wstring TestNetworkName2 = L"wslc-e2e-network-list-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommandAliases() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_NetworkListLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network 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" --format Output formatting (json or table) (Default: table)\r\n" // - << L" -q,--quiet Outputs the network names only\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/WSLCE2ENetworkPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp index 9fc0919a80..d635ad08da 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp @@ -46,7 +46,8 @@ class WSLCE2ENetworkPruneTests WSLC_TEST_METHOD(WSLCE2E_Network_Prune_HelpCommand) { const auto result = RunWslc(L"network 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_Network_Prune_NoNetworks) @@ -197,13 +198,15 @@ class WSLCE2ENetworkPruneTests WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_MalformedValue) { const auto result = RunWslc(L"network 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_Network_Prune_Filter_InvalidKey) { const auto result = RunWslc(L"network prune --filter color=red"); - result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG")); } private: @@ -218,36 +221,5 @@ class WSLCE2ENetworkPruneTests EnsureNetworkDoesNotExist(TestNetworkName); EnsureNetworkDoesNotExist(TestNetworkName2); } - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return Localization::WSLCCLI_NetworkPruneLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network prune []\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" --session " << Localization::WSLCCLI_SessionIdArgDescription() << 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/WSLCE2ENetworkRemoveTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp index d5861e9b02..73125f4b83 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp @@ -40,13 +40,15 @@ class WSLCE2ENetworkRemoveTests WSLC_TEST_METHOD(WSLCE2E_Network_Remove_HelpCommand) { auto result = RunWslc(L"network 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_Network_Remove_MissingNetworkName) { auto result = RunWslc(L"network remove"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'network-name'")); } WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Valid) @@ -132,51 +134,5 @@ class WSLCE2ENetworkRemoveTests private: const std::wstring TestNetworkName = L"wslc-e2e-network-remove"; const std::wstring TestNetworkName2 = L"wslc-e2e-network-remove-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommandAliases() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return Localization::WSLCCLI_NetworkRemoveLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network 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" network-name Network 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 Do not error if the network does not exist\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/WSLCE2ENetworkTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp index 87489dfa56..a8f6752bc9 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp @@ -27,76 +27,22 @@ class WSLCE2ENetworkTests WSLC_TEST_METHOD(WSLCE2E_Network_HelpCommand) { auto result = RunWslc(L"network --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_Network_NoSubcommand_ShowsHelp) { auto result = RunWslc(L"network"); - 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_Network_InvalidCommand_DisplaysErrorMessage) { auto result = RunWslc(L"network INVALID_CMD"); - result.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_NetworkCommandLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc network [] []\r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::vector> entries = { - {L"create", Localization::WSLCCLI_NetworkCreateDesc()}, - {L"remove", Localization::WSLCCLI_NetworkRemoveDesc()}, - {L"inspect", Localization::WSLCCLI_NetworkInspectDesc()}, - {L"list", Localization::WSLCCLI_NetworkListDesc()}, - {L"prune", Localization::WSLCCLI_NetworkPruneDesc()}, - }; - - 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" - << L"\r\n"; - return options.str(); + 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/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index 1d51b76f0b..1790032586 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -28,25 +28,29 @@ class WSLCE2EPushPullTests WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand) { auto result = RunWslc(L"image push --help"); - result.Verify({.Stdout = GetPushHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Image_Push_RootAlias) { auto result = RunWslc(L"push --help"); - result.Verify({.Stdout = GetPushRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Image_Pull_HelpCommand) { auto result = RunWslc(L"image pull --help"); - result.Verify({.Stdout = GetPullHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Image_Pull_RootAlias) { auto result = RunWslc(L"pull --help"); - result.Verify({.Stdout = GetPullRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Image_PushPull) @@ -99,82 +103,5 @@ class WSLCE2EPushPullTests L"access to the resource is denied\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n"; result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1}); } - -private: - std::wstring GetPushHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetPushDescription() << GetPushUsage() << GetAvailableArguments() << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetPushRootAliasHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetPushDescription() << GetPushRootUsage() << GetAvailableArguments() << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetPullHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetPullDescription() << GetPullUsage() << GetAvailableArguments() << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetPullRootAliasHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetPullDescription() << GetPullRootUsage() << GetAvailableArguments() << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetPushDescription() const - { - return Localization::WSLCCLI_ImagePushLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetPullDescription() const - { - return Localization::WSLCCLI_ImagePullLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetPushUsage() const - { - return L"Usage: wslc image push [] \r\n\r\n"; - } - - std::wstring GetPushRootUsage() const - { - return L"Usage: wslc push [] \r\n\r\n"; - } - - std::wstring GetPullUsage() const - { - return L"Usage: wslc image pull [] \r\n\r\n"; - } - - std::wstring GetPullRootUsage() const - { - return L"Usage: wslc pull [] \r\n\r\n"; - } - - std::wstring GetAvailableArguments() const - { - std::wstringstream args; - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" - << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" - << L"\r\n"; - return args.str(); - } - - std::wstring GetAvailableOptions() const - { - std::wstringstream options; - options << Localization::WSLCCLI_AvailableOptions() << 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/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 061874a48b..be71fc5903 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -108,13 +108,15 @@ class WSLCE2ERegistryTests WSLC_TEST_METHOD(WSLCE2E_Registry_Login_HelpCommand) { auto result = RunWslc(L"registry login --help"); - result.Verify({.Stdout = GetLoginHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Registry_Logout_HelpCommand) { auto result = RunWslc(L"registry logout --help"); - result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(result.Stdout.value().empty()); } WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordAndStdinMutuallyExclusive) @@ -212,80 +214,5 @@ class WSLCE2ERegistryTests } } } - -private: - std::wstring GetLoginHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetLoginDescription() << GetLoginUsage() << GetLoginAvailableArguments() << GetLoginAvailableOptions(); - return output.str(); - } - - std::wstring GetLogoutHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() << GetLogoutDescription() << GetLogoutUsage() << GetLogoutAvailableArguments() - << GetLogoutAvailableOptions(); - return output.str(); - } - - std::wstring GetLoginDescription() const - { - return Localization::WSLCCLI_LoginLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetLogoutDescription() const - { - return Localization::WSLCCLI_LogoutLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetLoginUsage() const - { - return L"Usage: wslc registry login [] []\r\n\r\n"; - } - - std::wstring GetLogoutUsage() const - { - return L"Usage: wslc registry logout [] []\r\n\r\n"; - } - - std::wstring GetLoginAvailableArguments() const - { - std::wstringstream args; - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" - << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n" - << L"\r\n"; - return args.str(); - } - - std::wstring GetLogoutAvailableArguments() const - { - std::wstringstream args; - args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" - << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n" - << L"\r\n"; - return args.str(); - } - - std::wstring GetLoginAvailableOptions() const - { - std::wstringstream options; - options << Localization::WSLCCLI_AvailableOptions() << L"\r\n" - << L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n" - << L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n" - << L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n" - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" - << L"\r\n"; - return options.str(); - } - - std::wstring GetLogoutAvailableOptions() const - { - std::wstringstream options; - options << Localization::WSLCCLI_AvailableOptions() << 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/WSLCE2EVolumeCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp index 7d5212b076..2e16eb1a32 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeCreateTests.cpp @@ -40,7 +40,8 @@ class WSLCE2EVolumeCreateTests WSLC_TEST_METHOD(WSLCE2E_Volume_Create_HelpCommand) { auto result = RunWslc(L"volume 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_Volume_Create_EmptyName) @@ -82,7 +83,8 @@ class WSLCE2EVolumeCreateTests WSLC_TEST_METHOD(WSLCE2E_Volume_Create_Vhd_MissingOpts_Fail) { auto result = RunWslc(std::format(L"volume create --driver vhd {}", TestVolumeName)); - result.Verify({.Stderr = L"Missing required option: 'SizeBytes'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Missing required option: 'SizeBytes'\r\nError code: E_INVALIDARG")); VerifyVolumeIsNotListed(TestVolumeName); } @@ -91,7 +93,8 @@ class WSLCE2EVolumeCreateTests { auto result = RunWslc(std::format(L"volume create --driver invalid_driver --opt SizeBytes={} {}", DefaultVolumeSizeBytes, TestVolumeName)); - result.Verify({.Stdout = L"", .Stderr = L"Unsupported volume type: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unsupported volume type: 'invalid_driver'\r\nError code: E_INVALIDARG")); VerifyVolumeIsNotListed(TestVolumeName); } @@ -111,47 +114,5 @@ class WSLCE2EVolumeCreateTests private: const std::wstring TestVolumeName = L"wslc-e2e-volume-create"; const int DefaultVolumeSizeBytes = 3 * 1024 * 1024; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeCreateLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume create [] []\r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" volume-name Volume 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" -d,--driver Specify volume driver name, e.g. 'guest' or 'vhd' (default: guest)\r\n" // - << L" -o,--opt Set driver specific options\r\n" // - << L" -l,--label Set metadata on an object\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/WSLCE2EVolumeInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp index d960b85062..6a1623c44d 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeInspectTests.cpp @@ -41,13 +41,15 @@ class WSLCE2EVolumeInspectTests WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_HelpCommand) { auto result = RunWslc(L"volume 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_Volume_Inspect_MissingVolumeName) { auto result = RunWslc(L"volume inspect"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'volume-name'")); } WSLC_TEST_METHOD(WSLCE2E_Volume_Inspect_Success) @@ -121,44 +123,5 @@ class WSLCE2EVolumeInspectTests private: const std::wstring TestVolumeName1 = L"wslc-e2e-volume-inspect-1"; const std::wstring TestVolumeName2 = L"wslc-e2e-volume-inspect-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeInspectLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume inspect [] \r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" volume-name Volume 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 diff --git a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp index 26fdf456b5..280485e936 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp @@ -43,13 +43,16 @@ class WSLCE2EVolumeListTests WSLC_TEST_METHOD(WSLCE2E_Volume_List_HelpCommand) { auto result = RunWslc(L"volume 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_Volume_List_InvalidFormatOption) { auto result = RunWslc(L"volume 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_Volume_List_QuietOption_OutputsNamesOnly) @@ -94,42 +97,5 @@ class WSLCE2EVolumeListTests private: const std::wstring TestVolumeName = L"wslc-e2e-volume-list"; const std::wstring TestVolumeName2 = L"wslc-e2e-volume-list-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommandAliases() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return std::format(L"{}\r\n\r\n", Localization::WSLCCLI_VolumeListLongDesc()); - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume 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" --format Output formatting (json or table) (Default: table)\r\n" - << L" -q,--quiet Outputs the volume names only\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/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index 02d3b97c82..3c4aa6aa2c 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -46,7 +46,8 @@ class WSLCE2EVolumePruneTests WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_HelpCommand) { const auto result = RunWslc(L"volume 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_Volume_Prune_NoVolumes) @@ -216,13 +217,15 @@ class WSLCE2EVolumePruneTests WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_MalformedValue) { const auto result = RunWslc(L"volume 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_Volume_Prune_Filter_InvalidKey) { const auto result = RunWslc(L"volume prune --filter color=red"); - result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG")); } private: @@ -237,36 +240,5 @@ class WSLCE2EVolumePruneTests EnsureVolumeDoesNotExist(TestVolumeName); EnsureVolumeDoesNotExist(TestVolumeName2); } - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return Localization::WSLCCLI_VolumePruneLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume 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_VolumePruneAllArgDescription() << 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/WSLCE2EVolumeRemoveTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp index 43b564c7b5..7f488add11 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp @@ -49,13 +49,15 @@ class WSLCE2EVolumeRemoveTests WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_HelpCommand) { auto result = RunWslc(L"volume 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_Volume_Remove_MissingVolumeName) { auto result = RunWslc(L"volume remove"); - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'volume-name'\r\n", .ExitCode = 1}); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'volume-name'")); } WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Valid) @@ -191,51 +193,5 @@ class WSLCE2EVolumeRemoveTests const TestImage& DebianImage = DebianTestImage(); const std::wstring TestVolumeName = L"wslc-e2e-volume-remove"; const std::wstring TestVolumeName2 = L"wslc-e2e-volume-remove-2"; - - std::wstring GetHelpMessage() const - { - std::wstringstream output; - output << GetWslcHeader() // - << GetDescription() // - << GetUsage() // - << GetAvailableCommandAliases() // - << GetAvailableCommands() // - << GetAvailableOptions(); - return output.str(); - } - - std::wstring GetDescription() const - { - return Localization::WSLCCLI_VolumeRemoveLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume 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" volume-name Volume 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 Do not error if the volume does not exist\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/WSLCE2EVolumeTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp index d271922f98..21776178b2 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp @@ -27,76 +27,22 @@ class WSLCE2EVolumeTests WSLC_TEST_METHOD(WSLCE2E_Volume_HelpCommand) { auto result = RunWslc(L"volume --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_Volume_NoSubcommand_ShowsHelp) { auto result = RunWslc(L"volume"); - 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_Volume_InvalidCommand_DisplaysErrorMessage) { auto result = RunWslc(L"volume INVALID_CMD"); - result.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_VolumeCommandLongDesc() + L"\r\n\r\n"; - } - - std::wstring GetUsage() const - { - return L"Usage: wslc volume [] []\r\n\r\n"; - } - - std::wstring GetAvailableCommands() const - { - std::vector> entries = { - {L"create", Localization::WSLCCLI_VolumeCreateDesc()}, - {L"remove", Localization::WSLCCLI_VolumeRemoveDesc()}, - {L"inspect", Localization::WSLCCLI_VolumeInspectDesc()}, - {L"list", Localization::WSLCCLI_VolumeListDesc()}, - {L"prune", Localization::WSLCCLI_VolumePruneDesc()}, - }; - - 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" - << L"\r\n"; - return options.str(); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'")); } }; } // namespace WSLCE2ETests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 9fa296076e..afc144db11 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -147,6 +147,12 @@ bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) return Stdout.value().find(substring) != std::wstring::npos; } +bool WSLCExecutionResult::StderrContainsSubstring(const std::wstring& substring) const +{ + VERIFY_IS_TRUE(Stderr.has_value()); + return Stderr.value().find(substring) != std::wstring::npos; +} + WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType) { auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine; @@ -231,15 +237,6 @@ WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, st return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode}; } -std::wstring GetWslcHeader() -{ - std::wstringstream header; - header << L"Copyright (c) Microsoft Corporation. All rights reserved.\r\n" - << L"For privacy information about this product please visit https://aka.ms/privacy.\r\n" - << L"\r\n"; - return header.str(); -} - WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType, std::optional pseudoConsole) { auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine; diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index 83e07bca50..a19d1a1b56 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -46,6 +46,7 @@ struct WSLCExecutionResult std::wstring GetStdoutOneLine() const; bool StdoutContainsLine(const std::wstring& expectedLine) const; bool StdoutContainsSubstring(const std::wstring& substring) const; + bool StderrContainsSubstring(const std::wstring& substring) const; }; struct PseudoConsole @@ -133,7 +134,6 @@ WSLCExecutionResult RunWslcAndRedirectToFile( ElevationType elevationType = ElevationType::Elevated); void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated); -std::wstring GetWslcHeader(); WSLCInteractiveSession RunWslcInteractive( const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, std::optional pseudoConsole = std::nullopt);