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