From 9650129a21c71eedf17a6d480979439c9144cff6 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:57:47 -0700 Subject: [PATCH 1/5] Init volume prune --- localization/strings/en-US/Resources.resw | 18 ++ .../wslc/arguments/ArgumentValidation.cpp | 20 +- .../wslc/arguments/ArgumentValidation.h | 1 + src/windows/wslc/commands/VolumeCommand.cpp | 1 + src/windows/wslc/commands/VolumeCommand.h | 15 + .../wslc/commands/VolumePruneCommand.cpp | 52 ++++ src/windows/wslc/services/VolumeModel.h | 6 + src/windows/wslc/services/VolumeService.cpp | 39 +++ src/windows/wslc/services/VolumeService.h | 1 + src/windows/wslc/tasks/VolumeTasks.cpp | 25 ++ src/windows/wslc/tasks/VolumeTasks.h | 1 + .../wslc/e2e/WSLCE2EVolumePruneTests.cpp | 274 ++++++++++++++++++ test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp | 1 + 13 files changed, 448 insertions(+), 6 deletions(-) create mode 100644 src/windows/wslc/commands/VolumePruneCommand.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index d08df400bc..450a77a5ff 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3033,6 +3033,24 @@ On first run, creates the file with all settings commented out at their defaults Lists all volumes in the session. + + Remove unused local volumes. + + + Removes all unused anonymous local volumes. If --all is specified, also removes unused named volumes. A volume is considered unused when it is not referenced by any container. + {Locked="--all "}Command line arguments, file names and string inserts should not be translated + + + Remove all unused volumes, not just anonymous ones. + + + Deleted: {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + + Total reclaimed space: {:.2f} MB + {FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated + Volume name diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index fda2fd2199..2c70274e44 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -137,10 +137,7 @@ void ValidateFilter(const std::vector& values) { for (const auto& value : values) { - if (value.find(L'=') == std::wstring::npos) - { - throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value)); - } + std::ignore = ParseFilter(value); } } @@ -211,8 +208,8 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring } else { - throw ArgumentException(std::format( - L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); + throw ArgumentException( + std::format(L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); } } @@ -300,4 +297,15 @@ std::pair ParseDriverOption(const std::wstring& value) return {WideToMultiByte(value.substr(0, pos)), WideToMultiByte(value.substr(pos + 1))}; } +std::pair ParseFilter(const std::wstring& value) +{ + auto pos = value.find(L'='); + if (pos == std::wstring::npos) + { + throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value)); + } + + return {WideToMultiByte(value.substr(0, pos)), WideToMultiByte(value.substr(pos + 1))}; +} + } // namespace wsl::windows::wslc::validation diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index 5639922462..1eb9adf901 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.h +++ b/src/windows/wslc/arguments/ArgumentValidation.h @@ -76,5 +76,6 @@ void ValidateFilter(const std::vector& values); std::pair ParseLabel(const std::wstring& value); std::pair ParseDriverOption(const std::wstring& value); +std::pair ParseFilter(const std::wstring& value); } // namespace wsl::windows::wslc::validation diff --git a/src/windows/wslc/commands/VolumeCommand.cpp b/src/windows/wslc/commands/VolumeCommand.cpp index 9c171a3493..dd3ded2980 100644 --- a/src/windows/wslc/commands/VolumeCommand.cpp +++ b/src/windows/wslc/commands/VolumeCommand.cpp @@ -26,6 +26,7 @@ std::vector> VolumeCommand::GetCommands() const 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; } diff --git a/src/windows/wslc/commands/VolumeCommand.h b/src/windows/wslc/commands/VolumeCommand.h index 19a2b0634f..e8cabdf5ec 100644 --- a/src/windows/wslc/commands/VolumeCommand.h +++ b/src/windows/wslc/commands/VolumeCommand.h @@ -92,4 +92,19 @@ struct VolumeListCommand final : public Command void ValidateArgumentsInternal(const ArgMap& execArgs) const override; void ExecuteInternal(CLIExecutionContext& context) const override; }; + +// Prune Command +struct VolumePruneCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"prune"; + VolumePruneCommand(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; +}; } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp new file mode 100644 index 0000000000..d62585a346 --- /dev/null +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -0,0 +1,52 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + VolumePruneCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "VolumeCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "VolumeTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Volume Prune Command +std::vector VolumePruneCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()), + Argument::Create(ArgType::Filter, false, NO_LIMIT), + Argument::Create(ArgType::Session), + }; +} + +std::wstring VolumePruneCommand::ShortDescription() const +{ + return Localization::WSLCCLI_VolumePruneDesc(); +} + +std::wstring VolumePruneCommand::LongDescription() const +{ + return Localization::WSLCCLI_VolumePruneLongDesc(); +} + +void VolumePruneCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << PruneVolumes; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/VolumeModel.h b/src/windows/wslc/services/VolumeModel.h index e31fd1ec39..6aa4adbf85 100644 --- a/src/windows/wslc/services/VolumeModel.h +++ b/src/windows/wslc/services/VolumeModel.h @@ -27,4 +27,10 @@ struct CreateVolumeOptions std::vector> Labels{}; }; +struct PruneVolumesResult +{ + std::vector PrunedVolumes; + ULONGLONG SpaceReclaimed{}; +}; + } // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/services/VolumeService.cpp b/src/windows/wslc/services/VolumeService.cpp index c56da8fdcb..1ae3ea2ec8 100644 --- a/src/windows/wslc/services/VolumeService.cpp +++ b/src/windows/wslc/services/VolumeService.cpp @@ -12,6 +12,7 @@ Module Name: --*/ #include "VolumeService.h" +#include "WarningCallback.h" #include #include @@ -81,4 +82,42 @@ wsl::windows::common::wslc_schema::InspectVolume VolumeService::Inspect(models:: THROW_IF_FAILED(session.Get()->InspectVolume(name.c_str(), &output)); return FromJson(output.get()); } + +models::PruneVolumesResult VolumeService::Prune(models::Session& session, bool all, const std::vector>& filters) +{ + const bool hasExplicitAll = std::any_of(filters.begin(), filters.end(), [](const auto& f) { return f.first == "all"; }); + + std::vector filterEntries; + filterEntries.reserve(filters.size() + ((all && !hasExplicitAll) ? 1 : 0)); + if (all && !hasExplicitAll) + { + filterEntries.push_back({.Key = "all", .Value = "true"}); + } + + for (const auto& [key, value] : filters) + { + filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()}); + } + + auto warningCallback = Microsoft::WRL::Make(); + wil::unique_cotaskmem_array_ptr volumes; + ULONGLONG spaceReclaimed = 0; + THROW_IF_FAILED(session.Get()->PruneVolumes( + filterEntries.empty() ? nullptr : filterEntries.data(), + static_cast(filterEntries.size()), + warningCallback.Get(), + &volumes, + volumes.size_address(), + &spaceReclaimed)); + + models::PruneVolumesResult result; + result.SpaceReclaimed = spaceReclaimed; + result.PrunedVolumes.reserve(volumes.size()); + for (auto ptr = volumes.get(), end = volumes.get() + volumes.size(); ptr != end; ++ptr) + { + result.PrunedVolumes.emplace_back(*ptr); + } + + return result; +} } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/VolumeService.h b/src/windows/wslc/services/VolumeService.h index bd0fe54412..4e468128e9 100644 --- a/src/windows/wslc/services/VolumeService.h +++ b/src/windows/wslc/services/VolumeService.h @@ -24,5 +24,6 @@ struct VolumeService static void Delete(models::Session& session, const std::string& name); static std::vector List(models::Session& session); static wsl::windows::common::wslc_schema::InspectVolume Inspect(models::Session& session, const std::string& name); + static models::PruneVolumesResult Prune(models::Session& session, bool all, const std::vector>& filters = {}); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index c608d4e5a1..ec4cc6147a 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -14,6 +14,7 @@ Module Name: #include "Argument.h" #include "ArgumentValidation.h" #include "CLIExecutionContext.h" +#include "ImageModel.h" #include "VolumeModel.h" #include "VolumeService.h" #include "VolumeTasks.h" @@ -194,4 +195,28 @@ void ListVolumes(CLIExecutionContext& context) THROW_HR(E_UNEXPECTED); } } + +void PruneVolumes(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + + const bool all = context.Args.Contains(ArgType::All); + + std::vector> filters; + for (const auto& value : context.Args.GetAll()) + { + filters.push_back(validation::ParseFilter(value)); + } + + auto result = VolumeService::Prune(session, all, filters); + + for (const auto& volumeName : result.PrunedVolumes) + { + PrintMessage(Localization::WSLCCLI_VolumePruneDeleted(MultiByteToWide(volumeName))); + } + + PrintMessage(L""); + PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(static_cast(result.SpaceReclaimed) / WSLC_IMAGE_1MB)); +} } // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/VolumeTasks.h b/src/windows/wslc/tasks/VolumeTasks.h index d6fe39479c..bb2e3b7a25 100644 --- a/src/windows/wslc/tasks/VolumeTasks.h +++ b/src/windows/wslc/tasks/VolumeTasks.h @@ -21,4 +21,5 @@ void DeleteVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void GetVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void InspectVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void ListVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); +void PruneVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); } // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp new file mode 100644 index 0000000000..a67479ea7f --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -0,0 +1,274 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EVolumePruneTests.cpp + +Abstract: + + This file contains end-to-end tests for the WSLC volume prune command. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EVolumePruneTests +{ + WSLC_TEST_CLASS(WSLCE2EVolumePruneTests) + + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + CleanUpAllTestState(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + CleanUpAllTestState(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + CleanUpAllTestState(); + EnsureImageIsDeleted(DebianImage); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_HelpCommand) + { + const auto result = RunWslc(L"volume prune --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoVolumes) + { + // Prune when no volumes exist should succeed and report a reclaimed-space line. + const auto result = RunWslc(L"volume prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoAllFlag_PreservesNamedVolumes) + { + RunWslc(std::format(L"volume create {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + + auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); + + const auto result = RunWslc(L"volume prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_FALSE( + result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + L"Named volume should not be pruned without --all"); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); + + VerifyVolumeIsListed(TestVolumeName); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_AllFlag_RemovesNamedVolume) + { + RunWslc(std::format(L"volume create {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + + auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); + + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); + + VerifyVolumeIsNotListed(TestVolumeName); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_AllFlag_RemovesMultipleVolumes) + { + RunWslc(std::format(L"volume create {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"volume create {}", TestVolumeName2)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + VerifyVolumeIsListed(TestVolumeName2); + + auto cleanup = wil::scope_exit([&]() { + EnsureVolumeDoesNotExist(TestVolumeName); + EnsureVolumeDoesNotExist(TestVolumeName2); + }); + + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName2))); + + VerifyVolumeIsNotListed(TestVolumeName); + VerifyVolumeIsNotListed(TestVolumeName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_InUseVolume_Preserved) + { + RunWslc(std::format(L"volume create {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + + // Start a container that holds the volume open. + RunWslc( + std::format( + L"container run -d --name {} -v {}:/data {} sleep infinity", WslcContainerName, TestVolumeName, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + auto cleanup = wil::scope_exit([&]() { + EnsureContainerDoesNotExist(WslcContainerName); + EnsureVolumeDoesNotExist(TestVolumeName); + }); + + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_FALSE( + result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + L"Volume in use by a running container must not be pruned"); + + VerifyVolumeIsListed(TestVolumeName); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_LabelFilter_PreservesNonMatchingVolume) + { + RunWslc(std::format(L"volume create {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + + auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); + + // A label filter that does not match the volume should preserve it... + const auto filteredPrune = RunWslc(L"volume prune --all --filter label=wslc.test.never=present"); + filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE( + filteredPrune.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + L"Filtered prune should not have deleted the non-matching volume"); + VerifyVolumeIsListed(TestVolumeName); + + // ...and a subsequent unfiltered prune --all should still remove it, proving the + // filter (not absence of eligible volumes) was the reason it survived. + const auto unfilteredPrune = RunWslc(L"volume prune --all"); + unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); + VerifyVolumeIsNotListed(TestVolumeName); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_LabelFilter_MatchingValueIsDeleted) + { + RunWslc(std::format(L"volume create --label wslc.test.prune=keep {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"volume create {}", TestVolumeName2)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + VerifyVolumeIsListed(TestVolumeName2); + + auto cleanup = wil::scope_exit([&]() { + EnsureVolumeDoesNotExist(TestVolumeName); + EnsureVolumeDoesNotExist(TestVolumeName2); + }); + + const auto result = RunWslc(L"volume prune --all --filter label=wslc.test.prune=keep"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); + VERIFY_IS_FALSE( + result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName2)), + L"Volume without the matching label must not be deleted"); + + VerifyVolumeIsNotListed(TestVolumeName); + VerifyVolumeIsListed(TestVolumeName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NegatedLabelFilter_PreservesLabeledVolume) + { + RunWslc(std::format(L"volume create --label wslc.test.keep=yes {}", TestVolumeName)).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"volume create {}", TestVolumeName2)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName); + VerifyVolumeIsListed(TestVolumeName2); + + auto cleanup = wil::scope_exit([&]() { + EnsureVolumeDoesNotExist(TestVolumeName); + EnsureVolumeDoesNotExist(TestVolumeName2); + }); + + const auto result = RunWslc(L"volume prune --all --filter label!=wslc.test.keep"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName2))); + VERIFY_IS_FALSE( + result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + L"Labeled volume must be preserved when prune negates that label"); + + VerifyVolumeIsListed(TestVolumeName); + VerifyVolumeIsNotListed(TestVolumeName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_MalformedValue) + { + // Filter values must be of the form key=value; bare keys are rejected by the CLI. + const auto result = RunWslc(L"volume prune --filter label"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey) + { + const auto result = RunWslc(L"volume prune --filter color=red"); + VERIFY_ARE_EQUAL(1u, static_cast(result.ExitCode.value_or(0))); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); + } + +private: + const TestImage& DebianImage = DebianTestImage(); + const std::wstring TestVolumeName = L"wslc-e2e-volume-prune"; + const std::wstring TestVolumeName2 = L"wslc-e2e-volume-prune-2"; + const std::wstring WslcContainerName = L"wslc-volume-prune-test-container"; + + void CleanUpAllTestState() + { + EnsureContainerDoesNotExist(WslcContainerName); + 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" --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/WSLCE2EVolumeTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp index 123a801abb..d271922f98 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp @@ -71,6 +71,7 @@ class WSLCE2EVolumeTests {L"remove", Localization::WSLCCLI_VolumeRemoveDesc()}, {L"inspect", Localization::WSLCCLI_VolumeInspectDesc()}, {L"list", Localization::WSLCCLI_VolumeListDesc()}, + {L"prune", Localization::WSLCCLI_VolumePruneDesc()}, }; size_t maxLen = 0; From 59167e938922e43b4997eeb29ce7103a237e9ddb Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:03:29 -0700 Subject: [PATCH 2/5] Clang format --- src/windows/wslc/arguments/ArgumentValidation.cpp | 4 ++-- test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index 2c70274e44..bcaa9e24b1 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -208,8 +208,8 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring } else { - throw ArgumentException( - std::format(L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); + throw ArgumentException(std::format( + L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); } } diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index a67479ea7f..4337520c7b 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -120,9 +120,8 @@ class WSLCE2EVolumePruneTests VerifyVolumeIsListed(TestVolumeName); // Start a container that holds the volume open. - RunWslc( - std::format( - L"container run -d --name {} -v {}:/data {} sleep infinity", WslcContainerName, TestVolumeName, DebianImage.NameAndTag())) + RunWslc(std::format( + L"container run -d --name {} -v {}:/data {} sleep infinity", WslcContainerName, TestVolumeName, DebianImage.NameAndTag())) .Verify({.Stderr = L"", .ExitCode = 0}); auto cleanup = wil::scope_exit([&]() { From 8212ce8455a7513fc3d55ef9b5bb0bd8ded9b2ed Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:38:37 -0700 Subject: [PATCH 3/5] Fix test --- test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index 4337520c7b..d1828c3413 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -69,7 +69,7 @@ class WSLCE2EVolumePruneTests result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( - result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName)), L"Named volume should not be pruned without --all"); VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); @@ -133,7 +133,7 @@ class WSLCE2EVolumePruneTests result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( - result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName)), L"Volume in use by a running container must not be pruned"); VerifyVolumeIsListed(TestVolumeName); @@ -150,7 +150,7 @@ class WSLCE2EVolumePruneTests const auto filteredPrune = RunWslc(L"volume prune --all --filter label=wslc.test.never=present"); filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( - filteredPrune.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + filteredPrune.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName)), L"Filtered prune should not have deleted the non-matching volume"); VerifyVolumeIsListed(TestVolumeName); @@ -179,7 +179,7 @@ class WSLCE2EVolumePruneTests VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); VERIFY_IS_FALSE( - result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName2)), + result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName2)), L"Volume without the matching label must not be deleted"); VerifyVolumeIsNotListed(TestVolumeName); @@ -203,7 +203,7 @@ class WSLCE2EVolumePruneTests VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName2))); VERIFY_IS_FALSE( - result.StdoutContainsSubstring(std::format(L"Deleted: {}", TestVolumeName)), + result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName)), L"Labeled volume must be preserved when prune negates that label"); VerifyVolumeIsListed(TestVolumeName); From 2f5724761747c8610cb93894b7589fa1d1c58ba5 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:14:32 -0700 Subject: [PATCH 4/5] Update tests --- .../wslc/e2e/WSLCE2EVolumePruneTests.cpp | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index d1828c3413..1b36a58330 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -68,10 +68,10 @@ class WSLCE2EVolumePruneTests const auto result = RunWslc(L"volume prune"); result.Verify({.Stderr = L"", .ExitCode = 0}); - VERIFY_IS_FALSE( - result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName)), - L"Named volume should not be pruned without --all"); - VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); + auto output = result.GetStdoutLines(); + VERIFY_ARE_EQUAL(2u, output.size()); + VERIFY_ARE_EQUAL(output[0], L""); + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, output[1].find(L"Total reclaimed space:")); VerifyVolumeIsListed(TestVolumeName); } @@ -86,8 +86,11 @@ class WSLCE2EVolumePruneTests const auto result = RunWslc(L"volume prune --all"); result.Verify({.Stderr = L"", .ExitCode = 0}); - VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); - VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Total reclaimed space:")); + auto output = result.GetStdoutLines(); + VERIFY_ARE_EQUAL(3u, output.size()); + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, output[0].find(std::format(L"Deleted: {}", TestVolumeName))); + VERIFY_ARE_EQUAL(output[1], L""); + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, output[2].find(L"Total reclaimed space:")); VerifyVolumeIsNotListed(TestVolumeName); } @@ -146,7 +149,7 @@ class WSLCE2EVolumePruneTests auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); - // A label filter that does not match the volume should preserve it... + // A label filter that does not match the volume should preserve it const auto filteredPrune = RunWslc(L"volume prune --all --filter label=wslc.test.never=present"); filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( @@ -154,8 +157,8 @@ class WSLCE2EVolumePruneTests L"Filtered prune should not have deleted the non-matching volume"); VerifyVolumeIsListed(TestVolumeName); - // ...and a subsequent unfiltered prune --all should still remove it, proving the - // filter (not absence of eligible volumes) was the reason it survived. + // Subsequent unfiltered prune --all should still remove it, proving + // the filter was the reason it survived. const auto unfilteredPrune = RunWslc(L"volume prune --all"); unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(std::format(L"Deleted: {}", TestVolumeName))); @@ -212,7 +215,6 @@ class WSLCE2EVolumePruneTests WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_MalformedValue) { - // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"volume prune --filter label"); result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1}); } @@ -220,9 +222,7 @@ class WSLCE2EVolumePruneTests WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey) { const auto result = RunWslc(L"volume prune --filter color=red"); - VERIFY_ARE_EQUAL(1u, static_cast(result.ExitCode.value_or(0))); - VERIFY_IS_TRUE(result.Stderr.has_value()); - VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); + result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); } private: From 8840c5173949e3a686291cdfaa56e5428c4d52af Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:07:19 -0700 Subject: [PATCH 5/5] Update unit --- localization/strings/en-US/Resources.resw | 4 ++-- src/windows/wslc/tasks/VolumeTasks.cpp | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 463621831a..70fd4d3905 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3115,8 +3115,8 @@ On first run, creates the file with all settings commented out at their defaults {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated - Total reclaimed space: {:.2f} MB - {FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated + Total reclaimed space: {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated Volume name diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index ec4cc6147a..e472d61cc4 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -14,7 +14,6 @@ Module Name: #include "Argument.h" #include "ArgumentValidation.h" #include "CLIExecutionContext.h" -#include "ImageModel.h" #include "VolumeModel.h" #include "VolumeService.h" #include "VolumeTasks.h" @@ -217,6 +216,6 @@ void PruneVolumes(CLIExecutionContext& context) } PrintMessage(L""); - PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(static_cast(result.SpaceReclaimed) / WSLC_IMAGE_1MB)); + PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(wsl::shared::string::FormatBytes(result.SpaceReclaimed))); } } // namespace wsl::windows::wslc::task