From c0e77fb3492733bd0c0a486a237fef17ae527683 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:15:02 -0700 Subject: [PATCH 1/3] Init network commands --- localization/strings/en-US/Resources.resw | 37 ++++ src/shared/inc/JsonUtils.h | 20 ++ .../wslc/arguments/ArgumentDefinitions.h | 1 + src/windows/wslc/commands/NetworkCommand.cpp | 51 +++++ src/windows/wslc/commands/NetworkCommand.h | 95 ++++++++ .../wslc/commands/NetworkCreateCommand.cpp | 53 +++++ .../wslc/commands/NetworkInspectCommand.cpp | 50 +++++ .../wslc/commands/NetworkListCommand.cpp | 65 ++++++ .../wslc/commands/NetworkRemoveCommand.cpp | 50 +++++ src/windows/wslc/commands/RootCommand.cpp | 2 + src/windows/wslc/core/ExecutionContextData.h | 2 + src/windows/wslc/services/NetworkModel.h | 30 +++ src/windows/wslc/services/NetworkService.cpp | 82 +++++++ src/windows/wslc/services/NetworkService.h | 28 +++ src/windows/wslc/tasks/NetworkTasks.cpp | 205 ++++++++++++++++++ src/windows/wslc/tasks/NetworkTasks.h | 24 ++ test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp | 1 + test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 55 +++++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 4 + .../wslc/e2e/WSLCE2ENetworkCreateTests.cpp | 143 ++++++++++++ .../wslc/e2e/WSLCE2ENetworkInspectTests.cpp | 160 ++++++++++++++ .../wslc/e2e/WSLCE2ENetworkListTests.cpp | 134 ++++++++++++ .../wslc/e2e/WSLCE2ENetworkRemoveTests.cpp | 152 +++++++++++++ test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp | 101 +++++++++ 24 files changed, 1545 insertions(+) create mode 100644 src/windows/wslc/commands/NetworkCommand.cpp create mode 100644 src/windows/wslc/commands/NetworkCommand.h create mode 100644 src/windows/wslc/commands/NetworkCreateCommand.cpp create mode 100644 src/windows/wslc/commands/NetworkInspectCommand.cpp create mode 100644 src/windows/wslc/commands/NetworkListCommand.cpp create mode 100644 src/windows/wslc/commands/NetworkRemoveCommand.cpp create mode 100644 src/windows/wslc/services/NetworkModel.h create mode 100644 src/windows/wslc/services/NetworkService.cpp create mode 100644 src/windows/wslc/services/NetworkService.h create mode 100644 src/windows/wslc/tasks/NetworkTasks.cpp create mode 100644 src/windows/wslc/tasks/NetworkTasks.h create mode 100644 test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..ebf66d97f 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2690,6 +2690,43 @@ On first run, creates the file with all settings commented out at their defaults Requested load but no input provided. + + Manage networks. + + + Manage the lifecycle of WSL networks, including creating, inspecting, listing, and deleting them. + {Locked="WSL"}Product names should not be translated + + + Create a network. + + + Creates a new network. + + + Remove one or more networks. + + + Removes one or more networks. A network cannot be removed if it has active endpoints. + + + Display detailed information on one or more networks. + + + Display detailed information on one or more networks. + + + List networks. + + + Lists all networks in the session. + + + Network name + + + Outputs the network names only + Manage volumes. diff --git a/src/shared/inc/JsonUtils.h b/src/shared/inc/JsonUtils.h index 63df6d29b..9bd4f2fbb 100644 --- a/src/shared/inc/JsonUtils.h +++ b/src/shared/inc/JsonUtils.h @@ -190,6 +190,26 @@ struct adl_serializer strncpy_s(volume.Driver, sizeof(volume.Driver), driver.c_str(), _TRUNCATE); } }; + +template <> +struct adl_serializer +{ + static void to_json(json& j, const WSLCNetworkInformation& network) + { + j = json{{"Name", std::string(network.Name)}, {"Id", std::string(network.Id)}, {"Driver", std::string(network.Driver)}}; + } + + static void from_json(const json& j, WSLCNetworkInformation& network) + { + std::string name = j.at("Name").get(); + std::string id = j.at("Id").get(); + std::string driver = j.at("Driver").get(); + + strncpy_s(network.Name, sizeof(network.Name), name.c_str(), _TRUNCATE); + strncpy_s(network.Id, sizeof(network.Id), id.c_str(), _TRUNCATE); + strncpy_s(network.Driver, sizeof(network.Driver), driver.c_str(), _TRUNCATE); + } +}; #endif } // namespace nlohmann \ No newline at end of file diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index a7bfe4e9b..1af70e6ad 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -65,6 +65,7 @@ _(Input, "input", L"i", Kind::Value, L _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \ _(Label, "label", NO_ALIAS, Kind::Value, L"Volume metadata setting") \ _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \ +_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \ /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \ _(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \ _(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \ diff --git a/src/windows/wslc/commands/NetworkCommand.cpp b/src/windows/wslc/commands/NetworkCommand.cpp new file mode 100644 index 000000000..b8a8b6ad5 --- /dev/null +++ b/src/windows/wslc/commands/NetworkCommand.cpp @@ -0,0 +1,51 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ +#include "CLIExecutionContext.h" +#include "NetworkCommand.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Root Command +std::vector> NetworkCommand::GetCommands() const +{ + std::vector> commands; + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + return commands; +} + +std::vector NetworkCommand::GetArguments() const +{ + return {}; +} + +std::wstring NetworkCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkCommandDesc(); +} + +std::wstring NetworkCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkCommandLongDesc(); +} + +void NetworkCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + OutputHelp(); +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkCommand.h b/src/windows/wslc/commands/NetworkCommand.h new file mode 100644 index 000000000..8b4a22fb9 --- /dev/null +++ b/src/windows/wslc/commands/NetworkCommand.h @@ -0,0 +1,95 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkCommand.h + +Abstract: + + Declaration of command classes and interfaces. + +--*/ +#pragma once +#include "Command.h" + +namespace wsl::windows::wslc { +// Root Network Command +struct NetworkCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"network"; + NetworkCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + + std::vector> GetCommands() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Create Command +struct NetworkCreateCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"create"; + NetworkCreateCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Remove Command +struct NetworkRemoveCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"remove"; + NetworkRemoveCommand(const std::wstring& parent) : Command(CommandName, {L"delete", L"rm"}, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Inspect Command +struct NetworkInspectCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"inspect"; + NetworkInspectCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// List Command +struct NetworkListCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"list"; + NetworkListCommand(const std::wstring& parent) : Command(CommandName, {L"ls"}, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ValidateArgumentsInternal(const ArgMap& execArgs) const override; + void ExecuteInternal(CLIExecutionContext& context) const override; +}; +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkCreateCommand.cpp b/src/windows/wslc/commands/NetworkCreateCommand.cpp new file mode 100644 index 000000000..9f8abc5e8 --- /dev/null +++ b/src/windows/wslc/commands/NetworkCreateCommand.cpp @@ -0,0 +1,53 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkCreateCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Create Command +std::vector NetworkCreateCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::NetworkName, true), + Argument::Create(ArgType::Driver), + Argument::Create(ArgType::Options, false, NO_LIMIT), + Argument::Create(ArgType::Label, false, NO_LIMIT), + Argument::Create(ArgType::Session), + }; +} + +std::wstring NetworkCreateCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkCreateDesc(); +} + +std::wstring NetworkCreateCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkCreateLongDesc(); +} + +void NetworkCreateCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << CreateSession // + << CreateNetwork; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkInspectCommand.cpp b/src/windows/wslc/commands/NetworkInspectCommand.cpp new file mode 100644 index 000000000..422673bd4 --- /dev/null +++ b/src/windows/wslc/commands/NetworkInspectCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkInspectCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Inspect Command +std::vector NetworkInspectCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::NetworkName, true, NO_LIMIT), + Argument::Create(ArgType::Session), + }; +} + +std::wstring NetworkInspectCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkInspectDesc(); +} + +std::wstring NetworkInspectCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkInspectLongDesc(); +} + +void NetworkInspectCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << CreateSession // + << InspectNetworks; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkListCommand.cpp b/src/windows/wslc/commands/NetworkListCommand.cpp new file mode 100644 index 000000000..5e7e06853 --- /dev/null +++ b/src/windows/wslc/commands/NetworkListCommand.cpp @@ -0,0 +1,65 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkListCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; +using namespace wsl::shared::string; + +namespace wsl::windows::wslc { +// Network List Command +std::vector NetworkListCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Format), + Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_NetworkListQuietArgDesc()), + Argument::Create(ArgType::Session), + }; +} + +std::wstring NetworkListCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkListDesc(); +} + +std::wstring NetworkListCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkListLongDesc(); +} + +void NetworkListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const +{ + if (execArgs.Contains(ArgType::Format)) + { + auto format = execArgs.Get(); + if (!IsEqual(format, L"json") && !IsEqual(format, L"table")) + { + throw CommandException(Localization::WSLCCLI_InvalidFormatError()); + } + } +} + +void NetworkListCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << CreateSession // + << GetNetworks // + << ListNetworks; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkRemoveCommand.cpp b/src/windows/wslc/commands/NetworkRemoveCommand.cpp new file mode 100644 index 000000000..567a3d719 --- /dev/null +++ b/src/windows/wslc/commands/NetworkRemoveCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkRemoveCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Remove Command +std::vector NetworkRemoveCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::NetworkName, true, NO_LIMIT), + Argument::Create(ArgType::Session), + }; +} + +std::wstring NetworkRemoveCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkRemoveDesc(); +} + +std::wstring NetworkRemoveCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkRemoveLongDesc(); +} + +void NetworkRemoveCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << CreateSession // + << DeleteNetworks; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 5b28bb3f4..21d949437 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -16,6 +16,7 @@ Module Name: // Include all commands that parent to the root. #include "ContainerCommand.h" #include "ImageCommand.h" +#include "NetworkCommand.h" #include "RegistryCommand.h" #include "SessionCommand.h" #include "SettingsCommand.h" @@ -32,6 +33,7 @@ std::vector> RootCommand::GetCommands() const std::vector> commands; commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/core/ExecutionContextData.h b/src/windows/wslc/core/ExecutionContextData.h index a443d71a8..b43018e2c 100644 --- a/src/windows/wslc/core/ExecutionContextData.h +++ b/src/windows/wslc/core/ExecutionContextData.h @@ -38,6 +38,7 @@ enum class Data : size_t ContainerOptions, Images, Volumes, + Networks, Max }; @@ -53,6 +54,7 @@ namespace details { DEFINE_DATA_MAPPING(ContainerOptions, wsl::windows::wslc::models::ContainerOptions); DEFINE_DATA_MAPPING(Images, std::vector); DEFINE_DATA_MAPPING(Volumes, std::vector); + DEFINE_DATA_MAPPING(Networks, std::vector); } // namespace details struct DataMap : wsl::windows::wslc::EnumBasedVariantMap diff --git a/src/windows/wslc/services/NetworkModel.h b/src/windows/wslc/services/NetworkModel.h new file mode 100644 index 000000000..4a69a1d62 --- /dev/null +++ b/src/windows/wslc/services/NetworkModel.h @@ -0,0 +1,30 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkModel.h + +Abstract: + + This file contains the NetworkModel definitions + +--*/ + +#pragma once + +#include "JsonUtils.h" +#include + +namespace wsl::windows::wslc::models { + +struct CreateNetworkOptions +{ + std::string Name; + std::optional Driver; + std::vector> DriverOpts{}; + std::vector> Labels{}; +}; + +} // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/services/NetworkService.cpp b/src/windows/wslc/services/NetworkService.cpp new file mode 100644 index 000000000..b18140d55 --- /dev/null +++ b/src/windows/wslc/services/NetworkService.cpp @@ -0,0 +1,82 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkService.cpp + +Abstract: + + This file contains the NetworkService implementation + +--*/ +#include "NetworkService.h" +#include +#include + +using namespace wsl::shared; +using namespace wsl::shared::string; +using namespace wsl::windows::common::wslutil; + +namespace wsl::windows::wslc::services { + +void NetworkService::Create(models::Session& session, const models::CreateNetworkOptions& createOptions) +{ + WSLCNetworkOptions options{}; + options.Name = createOptions.Name.c_str(); + if (createOptions.Driver.has_value()) + { + options.Driver = createOptions.Driver->c_str(); + } + + // Set driver options + std::vector driverOpts; + for (const auto& option : createOptions.DriverOpts) + { + driverOpts.push_back({.Key = option.first.c_str(), .Value = option.second.c_str()}); + } + + // Set labels + std::vector labels; + for (const auto& label : createOptions.Labels) + { + labels.push_back({.Key = label.first.c_str(), .Value = label.second.c_str()}); + } + + options.DriverOpts = driverOpts.data(); + options.DriverOptsCount = static_cast(driverOpts.size()); + options.Labels = labels.data(); + options.LabelsCount = static_cast(labels.size()); + + THROW_IF_FAILED(session.Get()->CreateNetwork(&options)); +} + +void NetworkService::Delete(models::Session& session, const std::string& name) +{ + THROW_IF_FAILED(session.Get()->DeleteNetwork(name.c_str())); +} + +std::vector NetworkService::List(models::Session& session) +{ + wil::unique_cotaskmem_array_ptr rawNetworks; + ULONG count = 0; + THROW_IF_FAILED(session.Get()->ListNetworks(&rawNetworks, &count)); + + std::vector networks; + networks.reserve(count); + for (auto ptr = rawNetworks.get(), end = rawNetworks.get() + count; ptr != end; ++ptr) + { + networks.push_back(*ptr); + } + + return networks; +} + +wsl::windows::common::wslc_schema::InspectNetwork NetworkService::Inspect(models::Session& session, const std::string& name) +{ + wil::unique_cotaskmem_ansistring output; + THROW_IF_FAILED(session.Get()->InspectNetwork(name.c_str(), &output)); + return FromJson(output.get()); +} +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/NetworkService.h b/src/windows/wslc/services/NetworkService.h new file mode 100644 index 000000000..000312b8f --- /dev/null +++ b/src/windows/wslc/services/NetworkService.h @@ -0,0 +1,28 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkService.h + +Abstract: + + This file contains the NetworkService definition + +--*/ +#pragma once + +#include "SessionModel.h" +#include "NetworkModel.h" +#include + +namespace wsl::windows::wslc::services { +struct NetworkService +{ + static void Create(models::Session& session, const models::CreateNetworkOptions& createOptions); + static void Delete(models::Session& session, const std::string& name); + static std::vector List(models::Session& session); + static wsl::windows::common::wslc_schema::InspectNetwork Inspect(models::Session& session, const std::string& name); +}; +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp new file mode 100644 index 000000000..f359cf7c1 --- /dev/null +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -0,0 +1,205 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkTasks.cpp + +Abstract: + + Implementation of network command related execution logic. + +--*/ +#include "Argument.h" +#include "ArgumentValidation.h" +#include "CLIExecutionContext.h" +#include "NetworkModel.h" +#include "NetworkService.h" +#include "NetworkTasks.h" +#include "TableOutput.h" +#include + +using namespace wsl::shared; +using namespace wsl::windows::common; +using namespace wsl::windows::common::string; +using namespace wsl::windows::common::wslutil; +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::models; +using namespace wsl::windows::wslc::services; + +namespace wsl::windows::wslc::task { + +static std::pair OptionsToKeyValue(const std::wstring& option) +{ + auto pos = option.find('='); + if (pos == std::wstring::npos) + { + return {WideToMultiByte(option), std::string()}; + } + + return {WideToMultiByte(option.substr(0, pos)), WideToMultiByte(option.substr(pos + 1))}; +} + +static bool TryInspectNetwork(Session& session, const std::string& networkName, std::optional& inspectData) +{ + try + { + inspectData = NetworkService::Inspect(session, networkName); + return true; + } + catch (const wil::ResultException& ex) + { + if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND) + { + PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr); + return false; + } + + throw; + } +} + +static bool TryDeleteNetwork(Session& session, const std::string& networkName) +{ + try + { + NetworkService::Delete(session, networkName); + return true; + } + catch (const wil::ResultException& ex) + { + if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND) + { + PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr); + return false; + } + + throw; + } +} + +void CreateNetwork(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); + + models::CreateNetworkOptions options{}; + options.Name = WideToMultiByte(context.Args.Get()); + + for (const auto& option : context.Args.GetAll()) + { + options.DriverOpts.push_back(OptionsToKeyValue(option)); + } + + for (const auto& label : context.Args.GetAll()) + { + options.Labels.push_back(OptionsToKeyValue(label)); + } + + if (context.Args.Contains(ArgType::Driver)) + { + options.Driver = WideToMultiByte(context.Args.Get()); + } + + NetworkService::Create(context.Data.Get(), options); + PrintMessage(MultiByteToWide(options.Name)); +} + +void DeleteNetworks(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + auto networkNames = context.Args.GetAll(); + for (const auto& name : networkNames) + { + if (TryDeleteNetwork(session, WideToMultiByte(name))) + { + PrintMessage(name); + } + else + { + context.ExitCode = 1; + } + } +} + +void GetNetworks(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + context.Data.Add(NetworkService::List(session)); +} + +void InspectNetworks(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + auto networkNames = context.Args.GetAll(); + std::vector result; + for (const auto& name : networkNames) + { + std::optional inspectData; + if (TryInspectNetwork(session, WideToMultiByte(name), inspectData)) + { + result.push_back(*inspectData); + } + else + { + context.ExitCode = 1; + } + } + + auto json = ToJson(result, c_jsonPrettyPrintIndent); + PrintMessage(MultiByteToWide(json)); +} + +void ListNetworks(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Networks)); + auto& networks = context.Data.Get(); + + if (context.Args.Contains(ArgType::Quiet)) + { + for (const auto& network : networks) + { + PrintMessage(MultiByteToWide(network.Name)); + } + + return; + } + + FormatType format = FormatType::Table; + if (context.Args.Contains(ArgType::Format)) + { + format = validation::GetFormatTypeFromString(context.Args.Get()); + } + + switch (format) + { + case FormatType::Json: + { + auto json = ToJson(networks, c_jsonPrettyPrintIndent); + PrintMessage(MultiByteToWide(json)); + break; + } + case FormatType::Table: + { + auto table = wsl::windows::wslc::TableOutput<3>({L"NETWORK ID", L"NAME", L"DRIVER"}); + for (const auto& network : networks) + { + table.OutputLine({ + MultiByteToWide(TruncateId(network.Id)), + MultiByteToWide(network.Name), + MultiByteToWide(network.Driver), + }); + } + + table.Complete(); + break; + } + default: + THROW_HR(E_UNEXPECTED); + } +} +} // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/NetworkTasks.h b/src/windows/wslc/tasks/NetworkTasks.h new file mode 100644 index 000000000..0796b1a48 --- /dev/null +++ b/src/windows/wslc/tasks/NetworkTasks.h @@ -0,0 +1,24 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkTasks.h + +Abstract: + + Declaration of network command execution tasks. + +--*/ +#pragma once +#include "CLIExecutionContext.h" + +namespace wsl::windows::wslc::task { + +void CreateNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context); +void DeleteNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); +void GetNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); +void InspectNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); +void ListNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); +} // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp index 3a59802a2..3367162f4 100644 --- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp @@ -525,6 +525,7 @@ class WSLCE2EGlobalTests std::vector> entries = { {L"container", Localization::WSLCCLI_ContainerCommandDesc()}, {L"image", Localization::WSLCCLI_ImageCommandDesc()}, + {L"network", Localization::WSLCCLI_NetworkCommandDesc()}, {L"registry", Localization::WSLCCLI_RegistryCommandDesc()}, {L"session", Localization::WSLCCLI_SessionCommandDesc()}, {L"settings", Localization::WSLCCLI_SettingsCommandDesc()}, diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index 7039822a5..213b4e902 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -244,6 +244,36 @@ void VerifyVolumeIsNotListed(const std::wstring& volumeName) } } +void VerifyNetworkIsListed(const std::wstring& networkName) +{ + auto result = RunWslc(L"network list --format json"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto networks = wsl::shared::FromJson>(result.Stdout.value().c_str()); + for (const auto& net : networks) + { + if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) + { + return; + } + } + + VERIFY_FAIL(std::format(L"Network '{}' not found in network list output", networkName).c_str()); +} + +void VerifyNetworkIsNotListed(const std::wstring& networkName) +{ + auto result = RunWslc(L"network list --format json"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto networks = wsl::shared::FromJson>(result.Stdout.value().c_str()); + for (const auto& net : networks) + { + if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) + { + VERIFY_FAIL(std::format(L"Network '{}' found in network list output", networkName).c_str()); + } + } +} + std::string GetHashId(const std::string& id, bool fullId) { return wsl::windows::common::string::TruncateId(id, !fullId); @@ -426,6 +456,31 @@ void EnsureVolumeDoesNotExist(const std::wstring& volumeName) } } +void EnsureNetworkDoesNotExist(const std::wstring& networkName) +{ + auto result = RunWslc(L"network list --format json"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto networks = wsl::shared::FromJson>(result.Stdout.value().c_str()); + for (const auto& net : networks) + { + if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) + { + auto deleteResult = RunWslc(std::format(L"network rm {}", networkName)); + deleteResult.Verify({.Stderr = L"", .ExitCode = 0}); + break; + } + } +} + +wslc_schema::InspectNetwork InspectNetwork(const std::wstring& networkName) +{ + auto result = RunWslc(std::format(L"network inspect {}", networkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto inspectData = wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + return inspectData[0]; +} + wil::com_ptr OpenDefaultElevatedSession() { // Ensure the default elevated session exists before opening it via COM. diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 926c8112c..28996f3b9 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -123,11 +123,14 @@ void VerifyImageIsNotUsed(const TestImage& image); void VerifyImageIsListed(const TestImage& image); void VerifyVolumeIsListed(const std::wstring& volumeName); void VerifyVolumeIsNotListed(const std::wstring& volumeName); +void VerifyNetworkIsListed(const std::wstring& networkName); +void VerifyNetworkIsNotListed(const std::wstring& networkName); std::string GetHashId(const std::string& id, bool fullId = false); wsl::windows::common::wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName); wsl::windows::common::wslc_schema::InspectImage InspectImage(const std::wstring& imageName); wsl::windows::common::wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName); +wsl::windows::common::wslc_schema::InspectNetwork InspectNetwork(const std::wstring& networkName); std::vector ListAllContainers(); void EnsureContainerDoesNotExist(const std::wstring& containerName); @@ -136,6 +139,7 @@ void EnsureImageIsDeleted(const TestImage& image); void EnsureImageContainersAreDeleted(const TestImage& image); void EnsureSessionIsTerminated(const std::wstring& sessionName = L""); void EnsureVolumeDoesNotExist(const std::wstring& volumeName); +void EnsureNetworkDoesNotExist(const std::wstring& networkName); void WriteTestFile(const std::filesystem::path& filePath, const std::vector& envVariableLines); std::wstring GetPythonHttpServerScript(uint16_t port); diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp new file mode 100644 index 000000000..f367aef3e --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp @@ -0,0 +1,143 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ENetworkCreateTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2ENetworkCreateTests +{ + WSLC_TEST_CLASS(WSLCE2ENetworkCreateTests) + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Create_HelpCommand) + { + auto result = RunWslc(L"network create --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + 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}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Create_Success) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName, result.GetStdoutOneLine()); + + VerifyNetworkIsListed(TestNetworkName); + auto inspect = InspectNetwork(TestNetworkName); + VERIFY_ARE_EQUAL("bridge", inspect.Driver); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Create_WithLabels_Success) + { + auto result = RunWslc(std::format(L"network create --driver bridge --label env=test --label app=wslc {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName, result.GetStdoutOneLine()); + + VerifyNetworkIsListed(TestNetworkName); + auto inspect = InspectNetwork(TestNetworkName); + VERIFY_ARE_EQUAL("bridge", inspect.Driver); + VERIFY_ARE_EQUAL("test", inspect.Labels["env"]); + VERIFY_ARE_EQUAL("wslc", inspect.Labels["app"]); + } + + 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}); + + VerifyNetworkIsNotListed(TestNetworkName); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Create_Duplicate_Fail) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify( + {.Stdout = L"", + .Stderr = std::format(L"Network '{}' already exists.\r\nError code: ERROR_ALREADY_EXISTS\r\n", TestNetworkName), + .ExitCode = 1}); + } + +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 volume driver name (default guest)\r\n" // + << L" -o,--opt Set driver specific options\r\n" // + << L" --label Volume metadata setting\r\n" // + << L" --session Specify the session to use\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 new file mode 100644 index 000000000..0a9212134 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp @@ -0,0 +1,160 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ENetworkInspectTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; +using namespace wsl::shared::string; + +class WSLCE2ENetworkInspectTests +{ + WSLC_TEST_CLASS(WSLCE2ENetworkInspectTests) + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureNetworkDoesNotExist(TestNetworkName1); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureNetworkDoesNotExist(TestNetworkName1); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_HelpCommand) + { + auto result = RunWslc(L"network inspect --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + 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}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_Success) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName1)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName1, result.GetStdoutOneLine()); + + result = RunWslc(std::format(L"network inspect {}", TestNetworkName1)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + auto inspect = inspectData[0]; + + VERIFY_ARE_EQUAL(WideToMultiByte(TestNetworkName1), inspect.Name); + VERIFY_ARE_EQUAL("bridge", inspect.Driver); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_InspectMultiple_Success) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName1)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName1, result.GetStdoutOneLine()); + result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName2, result.GetStdoutOneLine()); + + result = RunWslc(std::format(L"network inspect {} {}", TestNetworkName1, TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(2u, inspectData.size()); + + auto inspect1 = inspectData[0]; + VERIFY_ARE_EQUAL(WideToMultiByte(TestNetworkName1), inspect1.Name); + VERIFY_ARE_EQUAL("bridge", inspect1.Driver); + + auto inspect2 = inspectData[1]; + VERIFY_ARE_EQUAL(WideToMultiByte(TestNetworkName2), inspect2.Name); + VERIFY_ARE_EQUAL("bridge", inspect2.Driver); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_NotFound) + { + auto result = RunWslc(std::format(L"network inspect {}", TestNetworkName1)); + result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Network not found: '{}'\r\n", TestNetworkName1), .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_MixedFoundNotFound) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName1)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_ARE_EQUAL(TestNetworkName1, result.GetStdoutOneLine()); + + result = RunWslc(std::format(L"network inspect {} {}", TestNetworkName1, TestNetworkName2)); + result.Verify({.Stderr = std::format(L"Network not found: '{}'\r\n", TestNetworkName2), .ExitCode = 1}); + + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + auto inspect = inspectData[0]; + VERIFY_ARE_EQUAL(WideToMultiByte(TestNetworkName1), inspect.Name); + } + +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" --session Specify the session to use\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 new file mode 100644 index 000000000..937d508aa --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -0,0 +1,134 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ENetworkListTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; +using namespace wsl::shared::string; + +class WSLCE2ENetworkListTests +{ + WSLC_TEST_CLASS(WSLCE2ENetworkListTests) + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_HelpCommand) + { + auto result = RunWslc(L"network list --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + 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}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_QuietOption_OutputsNamesOnly) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(L"network list --quiet"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto lines = result.GetStdoutLines(); + VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestNetworkName)); + VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestNetworkName2)); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_JsonFormat) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(L"network list --format json"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto networks = FromJson>(result.Stdout.value().c_str()); + VERIFY_IS_TRUE(networks.size() >= 2U); + + std::vector names; + names.reserve(networks.size()); + for (const auto& network : networks) + { + names.push_back(network.Name); + } + + VERIFY_ARE_NOT_EQUAL(names.end(), std::find(names.begin(), names.end(), WideToMultiByte(TestNetworkName))); + VERIFY_ARE_NOT_EQUAL(names.end(), std::find(names.begin(), names.end(), WideToMultiByte(TestNetworkName2))); + } + +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" --session Specify the session to use\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/WSLCE2ENetworkRemoveTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp new file mode 100644 index 000000000..8acd2df5b --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp @@ -0,0 +1,152 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ENetworkRemoveTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2ENetworkRemoveTests +{ + WSLC_TEST_CLASS(WSLCE2ENetworkRemoveTests) + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureNetworkDoesNotExist(TestNetworkName); + EnsureNetworkDoesNotExist(TestNetworkName2); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_HelpCommand) + { + auto result = RunWslc(L"network remove --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + 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}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Valid) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyNetworkIsListed(TestNetworkName); + + result = RunWslc(std::format(L"network remove {}", TestNetworkName)); + result.Verify({.Stdout = std::format(L"{}\r\n", TestNetworkName), .Stderr = L"", .ExitCode = 0}); + + VerifyNetworkIsNotListed(TestNetworkName); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Multiple_Valid) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyNetworkIsListed(TestNetworkName); + VerifyNetworkIsListed(TestNetworkName2); + + result = RunWslc(std::format(L"network remove {} {}", TestNetworkName, TestNetworkName2)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyNetworkIsNotListed(TestNetworkName); + VerifyNetworkIsNotListed(TestNetworkName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_NotFound) + { + auto result = RunWslc(std::format(L"network remove {}", TestNetworkName)); + result.Verify({.Stdout = L"", .Stderr = std::format(L"Network not found: '{}'\r\n", TestNetworkName), .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_MixedFoundNotFound) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VerifyNetworkIsListed(TestNetworkName); + + result = RunWslc(std::format(L"network remove {} {}", TestNetworkName, TestNetworkName2)); + result.Verify( + {.Stdout = std::format(L"{}\r\n", TestNetworkName), + .Stderr = std::format(L"Network not found: '{}'\r\n", TestNetworkName2), + .ExitCode = 1}); + VerifyNetworkIsNotListed(TestNetworkName); + } + +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" --session Specify the session to use\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 new file mode 100644 index 000000000..c82279c8d --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp @@ -0,0 +1,101 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ENetworkTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" +#include "Argument.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2ENetworkTests +{ + WSLC_TEST_CLASS(WSLCE2ENetworkTests) + + WSLC_TEST_METHOD(WSLCE2E_Network_HelpCommand) + { + auto result = RunWslc(L"network --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_NoSubcommand_ShowsHelp) + { + auto result = RunWslc(L"network"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + 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()}, + }; + + 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(); + } +}; +} // namespace WSLCE2ETests From 2d3d8d1be598173ad88702a99cb86f4cd564ef1c Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:42:04 -0700 Subject: [PATCH 2/3] Integrate network inspect --- .../wslc/arguments/ArgumentValidation.cpp | 6 +- src/windows/wslc/services/InspectModel.h | 5 +- src/windows/wslc/tasks/InspectTasks.cpp | 11 +++ test/windows/wslc/e2e/WSLCE2EInspectTests.cpp | 68 ++++++++++++++++--- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index b96dadc4e..e301189fc 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -180,13 +180,17 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri { return InspectType::Container; } + else if (IsEqual(input, L"network")) + { + return InspectType::Network; + } else if (IsEqual(input, L"volume")) { return InspectType::Volume; } else { - constexpr std::wstring_view supportedValues = L"image, container, volume"; + constexpr std::wstring_view supportedValues = L"image, container, network, volume"; throw ArgumentException(Localization::WSLCCLI_InvalidInspectError(argName, input, supportedValues)); } } diff --git a/src/windows/wslc/services/InspectModel.h b/src/windows/wslc/services/InspectModel.h index e82f38c8b..f24af7db4 100644 --- a/src/windows/wslc/services/InspectModel.h +++ b/src/windows/wslc/services/InspectModel.h @@ -18,8 +18,9 @@ typedef enum _InspectType { Container = 1, Image = 2, - Volume = 4, + Network = 4, + Volume = 8, - All = Container | Image | Volume, + All = Container | Image | Network | Volume, } InspectType; } // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/tasks/InspectTasks.cpp b/src/windows/wslc/tasks/InspectTasks.cpp index d9fda6928..80d726217 100644 --- a/src/windows/wslc/tasks/InspectTasks.cpp +++ b/src/windows/wslc/tasks/InspectTasks.cpp @@ -16,6 +16,7 @@ Module Name: #include "InspectTasks.h" #include "InspectModel.h" #include "ImageService.h" +#include "NetworkService.h" #include "VolumeService.h" #include "ContainerService.h" @@ -57,6 +58,11 @@ static bool TryInspectContainer(wsl::windows::wslc::models::Session& session, co return TryInspect([&]() { result = services::ContainerService::Inspect(session, containerId); }, WSLC_E_CONTAINER_NOT_FOUND); } +static bool TryInspectNetwork(wsl::windows::wslc::models::Session& session, const std::string& networkName, std::optional& result) +{ + return TryInspect([&]() { result = services::NetworkService::Inspect(session, networkName); }, WSLC_E_NETWORK_NOT_FOUND); +} + static bool TryInspectVolume(wsl::windows::wslc::models::Session& session, const std::string& volumeId, std::optional& result) { return TryInspect([&]() { result = services::VolumeService::Inspect(session, volumeId); }, WSLC_E_VOLUME_NOT_FOUND); @@ -80,6 +86,7 @@ void Inspect(CLIExecutionContext& context) auto id = WideToMultiByte(objectId); std::optional container; std::optional image; + std::optional network; std::optional volume; if (WI_IsFlagSet(type, InspectType::Container) && TryInspectContainer(session, id, container)) @@ -90,6 +97,10 @@ void Inspect(CLIExecutionContext& context) { array.push_back(std::move(*image)); } + else if (WI_IsFlagSet(type, InspectType::Network) && TryInspectNetwork(session, id, network)) + { + array.push_back(std::move(*network)); + } else if (WI_IsFlagSet(type, InspectType::Volume) && TryInspectVolume(session, id, volume)) { array.push_back(std::move(*volume)); diff --git a/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp b/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp index da9f1a1c5..bcb117f23 100644 --- a/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EInspectTests.cpp @@ -35,6 +35,7 @@ class WSLCE2EInspectTests { EnsureContainerDoesNotExist(WslcContainerName); EnsureContainerDoesNotExist(DebianImage.Name); + EnsureNetworkDoesNotExist(WslcNetworkName); EnsureImageIsDeleted(DebianImage); return true; } @@ -43,6 +44,7 @@ class WSLCE2EInspectTests { EnsureContainerDoesNotExist(WslcContainerName); EnsureContainerDoesNotExist(DebianImage.Name); + EnsureNetworkDoesNotExist(WslcNetworkName); return true; } @@ -165,14 +167,14 @@ class WSLCE2EInspectTests } } - WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_PriorityOverVolume) + WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_PriorityOverNetwork) { - // When an image and volume share the same name and no --type is specified, - // the image should be returned (image is checked before volume in InspectTasks). - EnsureVolumeDoesNotExist(DebianImage.Name); - auto createResult = RunWslc(std::format(L"volume create {}", DebianImage.Name)); + // When an image and network share the same name and no --type is specified, + // the image should be returned (image is checked before network in InspectTasks). + EnsureNetworkDoesNotExist(DebianImage.Name); + auto createResult = RunWslc(std::format(L"network create --driver bridge {}", DebianImage.Name)); createResult.Verify({.Stderr = L"", .ExitCode = 0}); - auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(DebianImage.Name); }); + auto deleteNetwork = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(DebianImage.Name); }); // No type specified { @@ -199,14 +201,61 @@ class WSLCE2EInspectTests VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0])); } + // With --type network + { + auto result = RunWslc(std::format(L"inspect --type network {}", DebianImage.Name)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name)); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Inspect_Network_PriorityOverVolume) + { + // When a network and volume share the same name and no --type is specified, + // the network should be returned (network is checked before volume in InspectTasks). + EnsureNetworkDoesNotExist(WslcNetworkName); + EnsureVolumeDoesNotExist(WslcNetworkName); + + auto createNetworkResult = RunWslc(std::format(L"network create --driver bridge {}", WslcNetworkName)); + createNetworkResult.Verify({.Stderr = L"", .ExitCode = 0}); + auto deleteNetwork = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(WslcNetworkName); }); + + auto createVolumeResult = RunWslc(std::format(L"volume create {}", WslcNetworkName)); + createVolumeResult.Verify({.Stderr = L"", .ExitCode = 0}); + auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(WslcNetworkName); }); + + // No type specified — network takes priority over volume + { + auto result = RunWslc(std::format(L"inspect {}", WslcNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name)); + } + + // With --type network + { + auto result = RunWslc(std::format(L"inspect --type network {}", WslcNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto inspectData = + wsl::shared::FromJson>(result.Stdout.value().c_str()); + VERIFY_ARE_EQUAL(1u, inspectData.size()); + VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name)); + } + // With --type volume { - auto result = RunWslc(std::format(L"inspect --type volume {}", DebianImage.Name)); + auto result = RunWslc(std::format(L"inspect --type volume {}", WslcNetworkName)); result.Verify({.Stderr = L"", .ExitCode = 0}); auto inspectData = wsl::shared::FromJson>(result.Stdout.value().c_str()); VERIFY_ARE_EQUAL(1u, inspectData.size()); - VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name)); + VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name)); } } @@ -239,7 +288,7 @@ 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, volume.\r\n", .ExitCode = 1}); + 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}); } WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidFormatError) @@ -261,6 +310,7 @@ class WSLCE2EInspectTests const TestImage& DebianImage = DebianTestImage(); 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; From e7efc33896b60cf3062f6c4cc0ae1021de77f1b8 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:42:08 -0700 Subject: [PATCH 3/3] Clang format --- .../wslc/e2e/WSLCE2ENetworkCreateTests.cpp | 16 +++++++--------- .../windows/wslc/e2e/WSLCE2ENetworkListTests.cpp | 8 ++++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp index f367aef3e..c7041cb1a 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp @@ -75,9 +75,7 @@ class WSLCE2ENetworkCreateTests { 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}); + {.Stdout = L"", .Stderr = std::format(L"Unsupported network driver: 'invalid_driver'\r\nError code: E_INVALIDARG\r\n"), .ExitCode = 1}); VerifyNetworkIsNotListed(TestNetworkName); } @@ -130,12 +128,12 @@ class WSLCE2ENetworkCreateTests std::wstring GetAvailableOptions() const { std::wstringstream options; - options << L"The following options are available:\r\n" // - << L" -d,--driver Specify volume driver name (default guest)\r\n" // - << L" -o,--opt Set driver specific options\r\n" // - << L" --label Volume metadata setting\r\n" // - << L" --session Specify the session to use\r\n" // - << L" -?,--help Shows help about the selected command\r\n" // + options << L"The following options are available:\r\n" // + << L" -d,--driver Specify volume driver name (default guest)\r\n" // + << L" -o,--opt Set driver specific options\r\n" // + << L" --label Volume metadata setting\r\n" // + << L" --session Specify the session to use\r\n" // + << L" -?,--help Shows help about the selected command\r\n" // << L"\r\n"; return options.str(); } diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp index 937d508aa..91548231e 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -122,11 +122,11 @@ class WSLCE2ENetworkListTests std::wstring GetAvailableOptions() const { std::wstringstream options; - options << L"The following options are available:\r\n" // + 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" --session Specify the session to use\r\n" // - << L" -?,--help Shows help about the selected command\r\n" // + << L" -q,--quiet Outputs the network names only\r\n" // + << L" --session Specify the session to use\r\n" // + << L" -?,--help Shows help about the selected command\r\n" // << L"\r\n"; return options.str(); }