Files
WSL/test/windows/WslcSdkWinRTTests.cpp
ggarzia-MSFTandCopilot 4ebaffce6f Enable unity builds for wsltests (#41247)
wsltests is a single large target whose compile time dominates local test
iteration. Enabling CMake unity builds batches test sources into shared
translation units, cutting wall-clock build time significantly.

Batch size is controlled by the new WSL_TEST_UNITY_BATCH_SIZE cache variable
(default 2). Setting it to 0 disables unity builds and compiles every file
separately, which is useful for isolating a build break to a single source
file. The value is validated at configure time and documented in
UserConfig.cmake.sample.

Concatenating sources into one translation unit surfaced latent collisions
that file scope had previously hidden:

- g_pipelineBuildId was defined as a separate file-static in both Common.cpp
  and InstallerTests.cpp. Common.cpp now owns the definition and
  InstallerTests.cpp declares it extern.
- ProcessOutput was declared at namespace scope in both WslcSdkTests.cpp and
  WslcSdkWinRTTests.cpp. The latter is renamed to CapturedProcessOutput.
- Broad `using namespace wsl::windows::common;` directives in
  WindowsUpdateTests.cpp, WSLCE2EHelpers.cpp, WSLCE2ETlsRegistryTests.cpp and
  WSLCExecutor.cpp leaked into neighboring sources and created ambiguous
  lookups. They are replaced with targeted using-declarations and namespace
  aliases.

Unity builds also change test execution order, which broke assertions that
implicitly depended on running before anything else had created a session.
WSLCTests::ListSessions and WSLCTests::SessionManagement now share a
ListTestSessionNames() helper that filters out the persistent sessions the
wslc CLI creates for itself, so the assertions no longer depend on what else
has run on the machine.

SessionManagement is the only test that creates persistent sessions, which
outlive the COM reference that created them. It now records the ones it
creates and terminates them from a scope_exit handler, so a failure partway
through cannot leak a session into subsequent runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-06 17:53:15 -07:00

2000 lines
86 KiB
C++

/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WslcSdkWinRtTests.cpp
Abstract:
This file contains test cases for the WSLC SDK WinRT projection.
--*/
#include "precomp.h"
#include "Common.h"
#include "wslcsdk.h"
#include "WslcsdkPrivate.h"
#include "WSLCContainerLauncher.h"
#include "wslutil.h"
#include "wslc/e2e/WSLCE2EHelpers.h"
#include "winrt/Session.h"
#include "winrt/Helpers.h"
#include "winrt/ProcessCrashInformation.h"
#include <winrt/Microsoft.WSL.Containers.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Networking.h>
#include <winrt/Windows.Storage.Streams.h>
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::Storage::Streams;
using namespace std::chrono_literals;
namespace WSLCSDK = winrt::Microsoft::WSL::Containers;
extern std::wstring g_testDataPath;
extern bool g_fastTestRun;
#define VERIFY_THROWS_HR(operation, expectedHr) \
VERIFY_THROWS_SPECIFIC( \
operation, winrt::hresult_error, [&](winrt::hresult_error const& e) { return e.code().value == expectedHr; })
#define IGNORE_ERRORS(operation) \
try \
{ \
operation; \
} \
CATCH_LOG()
#define SCOPE_CLEANUP(operation) wil::scope_exit([&]() { IGNORE_ERRORS(operation) })
#define DELETE_CONTAINER_ON_SCOPE_EXIT(container) SCOPE_CLEANUP(container.Delete(WSLCSDK::DeleteContainerOption::Force))
#define DELETE_IMAGE_ON_SCOPE_EXIT(imageName) SCOPE_CLEANUP(m_defaultSession.DeleteImage(imageName))
struct CapturedProcessOutput
{
uint32_t ExitCode;
std::wstring StandardOutput;
std::wstring StandardError;
};
std::wstring ReadStream(IInputStream const& stream)
{
std::wstring output;
DataReader reader{stream};
reader.UnicodeEncoding(winrt::Windows::Storage::Streams::UnicodeEncoding::Utf8);
uint32_t bytesRead;
do
{
bytesRead = reader.LoadAsync(1024).get();
output += reader.ReadString(bytesRead).c_str();
} while (bytesRead > 0);
return output;
}
class WslcSdkWinRtTests
{
WSLC_TEST_CLASS(WslcSdkWinRtTests)
std::filesystem::path m_storagePath;
WSLCSDK::Session m_defaultSession{nullptr};
static inline constexpr auto c_testSessionName = L"wslc-winrt-test";
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
void StartProcessAndWaitForExit(WSLCSDK::Process const& process, std::chrono::milliseconds timeout = 2min)
{
std::promise<void> promise;
auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
process.Start();
VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready);
}
void StartContainerAndWaitForInitProcessExit(WSLCSDK::Container const& container, std::chrono::milliseconds timeout = 2min)
{
auto initProcess = container.InitProcess();
std::promise<void> promise;
auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
container.Start();
VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready);
}
CapturedProcessOutput GetProcessOutput(WSLCSDK::Process const& process)
{
CapturedProcessOutput output;
output.ExitCode = process.ExitCode();
output.StandardOutput = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput));
output.StandardError = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError));
return output;
}
void WaitForProcessOutput(WSLCSDK::Process const& process, std::string_view marker, std::chrono::seconds timeout = 60s)
{
auto stream = process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput);
Buffer buffer{1024};
std::string accumulated;
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (auto now = std::chrono::steady_clock::now(); now < deadline; now = std::chrono::steady_clock::now())
{
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
// N.B. InputStreamOptions::Partial is not supported.
auto read = stream.ReadAsync(buffer, buffer.Capacity(), InputStreamOptions::None);
if (read.wait_for(remaining) != winrt::Windows::Foundation::AsyncStatus::Completed)
{
break;
}
const auto result = read.GetResults();
if (result.Length() == 0)
{
break;
}
accumulated.append(reinterpret_cast<const char*>(result.data()), result.Length());
if (accumulated.find(marker) != std::string::npos)
{
return;
}
}
LogError("Timed out waiting for process output marker: '%hs'. Output: '%hs'", std::string(marker).c_str(), accumulated.c_str());
VERIFY_FAIL();
}
struct RunContainerOptions
{
std::vector<winrt::hstring> commandLine = {};
bool enableGpu = false;
std::optional<winrt::hstring> name = std::nullopt;
std::chrono::milliseconds timeout = 2min;
std::optional<WSLCSDK::ContainerNetworkingMode> networkingMode = std::nullopt;
};
// Creates and starts a one-shot container, waits for the init process to
// exit, and returns the exit code.
CapturedProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {})
{
auto procSettings = WSLCSDK::ProcessSettings();
if (!options.commandLine.empty())
{
procSettings.CommandLine(winrt::single_threaded_vector(std::move(options.commandLine)));
}
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(imageName);
containerSettings.InitProcess(procSettings);
containerSettings.EnableGpu(options.enableGpu);
if (options.name)
{
containerSettings.Name(options.name.value());
}
if (options.networkingMode)
{
containerSettings.NetworkingMode(options.networkingMode.value());
}
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
StartContainerAndWaitForInitProcessExit(container, options.timeout);
auto output = GetProcessOutput(container.InitProcess());
IGNORE_ERRORS(container.Delete(WSLCSDK::DeleteContainerOption::Force));
return output;
}
bool HasImage(winrt::hstring const& imageName)
{
auto images = m_defaultSession.GetImages();
return std::any_of(images.begin(), images.end(), [&](auto const& img) { return img.Name() == imageName; });
}
// Starts a local wslc-registry container using host-mode networking.
// Host networking is not exposed by the WinRT projection, so this helper
// uses WSLCContainerLauncher with the raw session handle.
std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000)
{
// Get the IWSLCSession COM object from the SDK session handle and delegate to the shared helper.
auto comSession =
reinterpret_cast<WslcSessionImpl*>(WSLCSDK::implementation::GetHandle(m_defaultSession))->session.query<IWSLCSession>();
return WSLCE2ETests::StartLocalRegistry(*comSession, username, password, port);
}
// Tags and pushes an image to a local registry via the SDK APIs.
void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth)
{
const auto imageName = winrt::to_hstring(std::format("{}:{}", repo, tag));
const auto registryRepo = winrt::to_hstring(std::format("{}/{}", registryAddress, repo));
const auto registryImage = winrt::to_hstring(std::format("{}/{}:{}", registryAddress, repo, tag));
VERIFY_IS_TRUE(HasImage(imageName));
m_defaultSession.TagImage(WSLCSDK::TagImageOptions(imageName, registryRepo, winrt::to_hstring(tag)));
// Ensures the registry-prefixed tag is removed after the push.
auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(registryImage);
m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(registryImage, winrt::to_hstring(registryAuth))).get();
}
TEST_CLASS_SETUP(TestClassSetup)
{
WSADATA wsaData;
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &wsaData));
winrt::init_apartment();
// Use the same storage path as WSLC runtime tests to reduce pull overhead.
m_storagePath = std::filesystem::current_path() / "test-storage";
// Build session settings using the WinRT API
auto settings = WSLCSDK::SessionSettings(c_testSessionName, m_storagePath.wstring());
settings.CpuCount(4);
settings.MemorySizeInMB(2048);
settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
m_defaultSession = WSLCSDK::Session(settings);
m_defaultSession.Start();
// Pull images required by the tests (no-op if already present).
for (const auto* imageName : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"})
{
const auto imagePath = GetTestImagePath(imageName);
m_defaultSession.LoadImageAsync(imagePath.wstring()).get();
}
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
if (m_defaultSession)
{
m_defaultSession.Terminate();
m_defaultSession = nullptr;
}
// Preserve the VHD in fast-run mode so subsequent runs skip image pulling.
if (!g_fastTestRun && !m_storagePath.empty())
{
std::error_code error;
std::filesystem::remove_all(m_storagePath, error);
if (error)
{
LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
}
}
return true;
}
// -----------------------------------------------------------------------
// Session tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(CreateSession)
{
const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-extra-session-storage";
auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-extra-session", extraStorage.wstring());
settings.CpuCount(2);
settings.MemorySizeInMB(1024);
settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 1024ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
// Positive: Creation must succeed with valid settings.
{
auto session = WSLCSDK::Session(settings);
VERIFY_IS_NOT_NULL(session);
}
// Negative: Must throw if used before Start()
{
auto session = WSLCSDK::Session(settings);
VERIFY_THROWS_HR(std::ignore = session.GetImages(), E_ILLEGAL_METHOD_CALL);
}
// Positive: Starting the session must succeed.
{
auto session = WSLCSDK::Session(settings);
VERIFY_NO_THROW(session.Start());
}
// Negative: Null settings must fail.
{
VERIFY_THROWS_HR(WSLCSDK::Session(WSLCSDK::SessionSettings{nullptr}), E_POINTER);
}
}
WSLC_TEST_METHOD(TerminationHandler)
{
// Positive: Terminating the session must trigger a graceful shutdown and fire the event
std::promise<WSLCSDK::SessionTerminationReason> promise;
const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-termh-storage";
auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-termh", extraStorage.wstring());
settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
auto session = WSLCSDK::Session(settings);
session.Terminated([&](WSLCSDK::SessionTerminationReason reason) { promise.set_value(reason); });
session.Start();
session.Terminate();
auto future = promise.get_future();
VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready);
VERIFY_ARE_EQUAL(future.get(), WSLCSDK::SessionTerminationReason::Shutdown);
}
WSLC_TEST_METHOD(ProcessCrashedEvent)
{
// Start a long-running container so we can exec non-PID-1 processes into it.
// The crashing process must NOT be the container's init process: Linux silently
// drops kill()-sent signals with default disposition when targeting PID 1 in a
// PID namespace, so no core dump would be generated if we crash the init process.
auto initProcSettings = WSLCSDK::ProcessSettings();
initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(initProcSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
container.Start();
// Positive: A crashing exec process must fire the ProcessCrashed event with correctly populated info.
{
std::promise<WSLCSDK::ProcessCrashInformation> promise;
auto revoker = m_defaultSession.ProcessCrashed(winrt::auto_revoke, [&](WSLCSDK::ProcessCrashInformation info) {
// Guard against multiple firings from concurrently running tests.
try
{
promise.set_value(info);
}
catch (...)
{
}
});
auto execSettings = WSLCSDK::ProcessSettings();
execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"kill -SEGV $$"}));
const auto beforeCrash = winrt::clock::now();
StartProcessAndWaitForExit(container.CreateProcess(execSettings), 30s);
auto future = promise.get_future();
VERIFY_ARE_EQUAL(future.wait_for(60s), std::future_status::ready);
const auto afterCrash = winrt::clock::now();
auto info = future.get();
VERIFY_IS_FALSE(info.DumpPath().empty());
VERIFY_IS_TRUE(std::filesystem::exists(info.DumpPath().c_str()));
VERIFY_IS_TRUE(std::wstring_view(info.ProcessName()).find(L"sh") != std::wstring_view::npos);
VERIFY_IS_GREATER_THAN(info.Pid(), 0u);
VERIFY_ARE_EQUAL(info.Signal(), 11u); // SIGSEGV = 11
// Crash timestamps are second-granularity; allow some slack around the measured window.
VERIFY_IS_TRUE(info.Timestamp() >= beforeCrash - 1s);
VERIFY_IS_TRUE(info.Timestamp() <= afterCrash + 1s);
}
// Negative: After revoking the subscription token, the handler must no longer fire.
{
std::atomic<int> callCount{0};
{
auto revoker =
m_defaultSession.ProcessCrashed(winrt::auto_revoke, [&](WSLCSDK::ProcessCrashInformation) { ++callCount; });
// revoker goes out of scope here — handler is unsubscribed before any crash is triggered.
}
auto execSettings = WSLCSDK::ProcessSettings();
execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"kill -SEGV $$"}));
StartProcessAndWaitForExit(container.CreateProcess(execSettings), 60s);
// Allow the event system time to dispatch any pending callbacks.
Sleep(1000);
VERIFY_ARE_EQUAL(callCount.load(), 0);
}
}
WSLC_TEST_METHOD(ProcessCrashInformationProperties)
{
// Verify that ProcessCrashInformation correctly maps all fields from WslcSessionCrashDumpInfo.
constexpr PCWSTR c_dumpPath = L"C:\\test\\dump.dmp";
constexpr PCSTR c_processName = "test-process";
constexpr uint32_t c_pid = 42;
constexpr uint32_t c_signal = 11; // SIGSEGV
constexpr uint64_t c_timestamp = 1700000000; // 2023-11-14 22:13:20 UTC
WslcSessionCrashDumpInfo info{};
info.dumpPath = c_dumpPath;
info.processName = c_processName;
info.pid = c_pid;
info.signal = c_signal;
info.timestamp = c_timestamp;
auto impl = winrt::make_self<WSLCSDK::implementation::ProcessCrashInformation>(&info);
VERIFY_ARE_EQUAL(impl->DumpPath(), winrt::hstring(c_dumpPath));
VERIFY_ARE_EQUAL(impl->ProcessName(), winrt::to_hstring(c_processName));
VERIFY_ARE_EQUAL(impl->Pid(), c_pid);
VERIFY_ARE_EQUAL(impl->Signal(), c_signal);
VERIFY_ARE_EQUAL(winrt::clock::to_time_t(impl->Timestamp()), static_cast<time_t>(c_timestamp));
}
// -----------------------------------------------------------------------
// Image tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(ImageList)
{
// Session has images pre-loaded - list must return at least one entry.
const auto images = m_defaultSession.GetImages();
VERIFY_IS_TRUE(images.Size() >= 1);
// At least one image must be non-empty
bool foundNonEmpty = false;
for (auto const& img : images)
{
if (!img.Name().empty() && img.Size() != 0)
{
foundNonEmpty = true;
break;
}
}
VERIFY_IS_TRUE(foundNonEmpty);
}
WSLC_TEST_METHOD(LoadImage)
{
// Positive: load a saved image tar and verify the image can be run
{
// Remove the image if it already exists
IGNORE_ERRORS(m_defaultSession.DeleteImage(L"hello-world:latest"));
const auto imageTar = GetTestImagePath("hello-world:latest");
// Positive: load from file path.
VERIFY_NO_THROW(m_defaultSession.LoadImageAsync(imageTar.wstring()).get());
// Verify the loaded image is usable
VERIFY_IS_TRUE(HasImage(L"hello-world:latest"));
auto output = RunContainerAndWaitForExit(L"hello-world:latest", {});
VERIFY_ARE_EQUAL(output.ExitCode, 0);
VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos);
}
// Negative: empty path must fail
{
VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"").get(), E_INVALIDARG);
}
// Negative: non-existent path must fail.
{
VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"C:\\bogus\\image.tar").get(), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
}
}
WSLC_TEST_METHOD(ImportImage)
{
const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
constexpr auto c_importedImageName = L"my-hello-world-winrt:test";
IGNORE_ERRORS(m_defaultSession.DeleteImage(c_importedImageName));
// Positive: import an exported image tar via path.
{
auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(c_importedImageName);
VERIFY_NO_THROW(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), c_importedImageName).get());
VERIFY_IS_TRUE(HasImage(c_importedImageName));
auto output = RunContainerAndWaitForExit(c_importedImageName, {.commandLine = {L"/hello"}});
VERIFY_ARE_EQUAL(output.ExitCode, 0);
VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos);
}
// Negative: empty path must fail.
{
VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(L"", c_importedImageName).get(), E_INVALIDARG);
}
// Negative: empty image name must fail.
{
VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), L"").get(), E_INVALIDARG);
}
// Negative: non-tar file must fail.
{
std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(pathToSelf.wstring(), L"import-self:test").get(), E_FAIL);
}
}
WSLC_TEST_METHOD(ImageDelete)
{
VERIFY_IS_TRUE(HasImage(L"hello-world:latest"));
// Positive: delete an existing image.
{
m_defaultSession.DeleteImage(L"hello-world:latest");
VERIFY_IS_FALSE(HasImage(L"hello-world:latest"));
// Reload for subsequent tests.
const auto imageTar = GetTestImagePath("hello-world:latest");
m_defaultSession.LoadImageAsync(imageTar.wstring()).get();
}
// Negative: non-existent image name must throw.
{
VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent:no-such-tag"), WSLC_E_IMAGE_NOT_FOUND);
}
}
// -----------------------------------------------------------------------
// Container lifecycle tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(CreateContainer)
{
// Positive: stdout is captured correctly.
{
auto output = RunContainerAndWaitForExit(L"debian:latest", {.commandLine = {L"/bin/echo", L"OK"}});
VERIFY_ARE_EQUAL(output.ExitCode, 0);
VERIFY_ARE_EQUAL(output.StandardOutput, L"OK\n");
VERIFY_ARE_EQUAL(output.StandardError, L"");
}
// Positive: stdout and stderr are routed independently.
{
auto output = RunContainerAndWaitForExit(
L"debian:latest", {.commandLine = {L"/bin/sh", L"-c", L"echo stdout && echo stderr >&2"}});
VERIFY_ARE_EQUAL(output.ExitCode, 0);
VERIFY_ARE_EQUAL(output.StandardOutput, L"stdout\n");
VERIFY_ARE_EQUAL(output.StandardError, L"stderr\n");
}
// Negative: creating a container with a non-existent image fails at CreateContainer.
{
WSLCSDK::ContainerSettings containerSettings{L"invalid-image:notfound"};
VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), WSLC_E_IMAGE_NOT_FOUND);
}
// Negative: an empty image name is rejected.
{
VERIFY_THROWS_HR(WSLCSDK::ContainerSettings{L""}, E_INVALIDARG);
}
// Verify that a null settings pointer is rejected.
{
VERIFY_THROWS_HR(m_defaultSession.CreateContainer(nullptr), E_POINTER);
}
}
WSLC_TEST_METHOD(ContainerGetId)
{
auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
const auto id = container.Id();
VERIFY_IS_FALSE(id.empty());
// Container ID is a 64-character lowercase hex string.
VERIFY_ARE_EQUAL(id.size(), 64u);
}
WSLC_TEST_METHOD(ContainerGetState)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// State after creation: Created.
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Created);
container.Start();
// State while running: Running.
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running);
container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero());
// State after stop: Exited.
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited);
}
WSLC_TEST_METHOD(ContainerStopAndDelete)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"999"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
VERIFY_NO_THROW(container.Start());
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running);
VERIFY_NO_THROW(container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero()));
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited);
VERIFY_NO_THROW(container.Delete(WSLCSDK::DeleteContainerOption::None));
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Deleted);
}
WSLC_TEST_METHOD(ProcessIOHandles)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT_TOKEN; echo STDERR_TOKEN >&2"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto initProcess = container.InitProcess();
std::promise<void> promise;
auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
container.Start();
auto stdoutStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput);
auto stderrStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError);
// Verify that each handle can only be acquired once.
{
VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
}
VERIFY_ARE_EQUAL(promise.get_future().wait_for(1min), std::future_status::ready);
VERIFY_ARE_EQUAL(ReadStream(stdoutStream), L"STDOUT_TOKEN\n");
VERIFY_ARE_EQUAL(ReadStream(stderrStream), L"STDERR_TOKEN\n");
}
WSLC_TEST_METHOD(ContainerNetworkingMode)
{
// BRIDGED: eth0 interface must be present.
{
auto output = RunContainerAndWaitForExit(
L"debian:latest",
{.commandLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
.networkingMode = WSLCSDK::ContainerNetworkingMode::Bridged});
VERIFY_ARE_EQUAL(output.StandardOutput, L"HAS_ETH0\n");
}
// NONE: eth0 interface must not be present.
{
auto output = RunContainerAndWaitForExit(
L"debian:latest",
{.commandLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
.networkingMode = WSLCSDK::ContainerNetworkingMode::None});
VERIFY_ARE_EQUAL(output.StandardOutput, L"NO_ETH0\n");
}
// Invalid networking mode must fail.
{
WSLCSDK::ContainerSettings containerSettings{L"debian:latest"};
VERIFY_THROWS_HR(containerSettings.NetworkingMode(static_cast<WSLCSDK::ContainerNetworkingMode>(99)), E_INVALIDARG);
}
}
WSLC_TEST_METHOD(ContainerPortMapping)
{
// Negative: port mappings with None networking mode must fail at Start.
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::None);
containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>(
{WSLCSDK::ContainerPortMapping(12342, 8000, WSLCSDK::PortProtocol::TCP)}));
VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), E_INVALIDARG);
}
// Functional: BRIDGED networking with port mapping; HTTP server must be reachable.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
procSettings.EnvironmentVariables(
winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
containerSettings.InitProcess(procSettings);
containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>(
{WSLCSDK::ContainerPortMapping(12341, 8000, WSLCSDK::PortProtocol::TCP)}));
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// Wait for the in-container HTTP server to start listening before issuing a request.
WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
ExpectHttpResponse(L"http://127.0.0.1:12341", 200, true);
}
// Functional: port mapping with explicit IPv4 WindowsAddress.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
procSettings.EnvironmentVariables(
winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto portMapping = WSLCSDK::ContainerPortMapping(12343, 8000, WSLCSDK::PortProtocol::TCP);
portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"127.0.0.1"));
auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
containerSettings.InitProcess(procSettings);
containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>({portMapping}));
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// Wait for the in-container HTTP server to start listening before issuing a request.
WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
ExpectHttpResponse(L"http://127.0.0.1:12343", 200, true);
}
// Functional: port mapping with explicit IPv6 WindowsAddress.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"--bind", L"::", L"8000"}));
procSettings.EnvironmentVariables(
winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto portMapping = WSLCSDK::ContainerPortMapping(12344, 8000, WSLCSDK::PortProtocol::TCP);
portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"::1"));
auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
containerSettings.InitProcess(procSettings);
containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>({portMapping}));
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// Wait for the in-container HTTP server to start listening before issuing a request.
WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
ExpectHttpResponse(L"http://[::1]:12344", 200, true);
}
}
WSLC_TEST_METHOD(ContainerVolumeUnit)
{
const auto currentDirectory = std::filesystem::current_path().wstring();
// Negative: non-absolute Windows path must fail at CreateContainer.
VERIFY_THROWS_HR(
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
{WSLCSDK::ContainerVolume(L"relative", L"/mnt/path", false)}));
m_defaultSession.CreateContainer(containerSettings);
},
E_INVALIDARG);
// Negative: non-absolute container path must fail at CreateContainer.
VERIFY_THROWS_HR(
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
{WSLCSDK::ContainerVolume(currentDirectory, L"./mnt/path", false)}));
m_defaultSession.CreateContainer(containerSettings);
},
E_INVALIDARG);
// Positive: absolute paths must succeed.
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
{WSLCSDK::ContainerVolume(currentDirectory, L"/mnt/path", false)}));
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Delete(WSLCSDK::DeleteContainerOption::None);
}
}
WSLC_TEST_METHOD(ContainerVolumeFunctional)
{
const auto hostRwDir = std::filesystem::current_path() / "wslc-winrt-test-vol-rw";
const auto hostRoDir = std::filesystem::current_path() / "wslc-winrt-test-vol-ro";
std::filesystem::create_directories(hostRwDir);
std::filesystem::create_directories(hostRoDir);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code ec;
std::filesystem::remove_all(hostRwDir, ec);
std::filesystem::remove_all(hostRoDir, ec);
});
std::ofstream{hostRwDir / "hello.txt"} << "hello-rw";
std::ofstream{hostRoDir / "hello.txt"} << "hello-ro";
// Container script exits 0 if all checks pass:
// 1. RW mount is readable (hello-rw).
// 2. RO mount is readable (hello-ro).
// 3. Writing to RW mount succeeds.
// 4. Writing to RO mount fails (! touch).
constexpr auto c_script =
"test \"$(cat /mnt/rw/hello.txt)\" = hello-rw && "
"test \"$(cat /mnt/ro/hello.txt)\" = hello-ro && "
"echo container-write > /mnt/rw/written.txt && "
"! touch /mnt/ro/probe 2>/dev/null";
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", winrt::to_hstring(c_script)}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>({
WSLCSDK::ContainerVolume(hostRwDir.wstring(), L"/mnt/rw", false),
WSLCSDK::ContainerVolume(hostRoDir.wstring(), L"/mnt/ro", true),
}));
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
// Verify the file written by the container is visible on the host.
std::ifstream written(hostRwDir / "written.txt");
VERIFY_IS_TRUE(written.is_open());
std::string writtenContent((std::istreambuf_iterator<char>(written)), std::istreambuf_iterator<char>());
VERIFY_ARE_EQUAL(writtenContent, "container-write\n");
}
WSLC_TEST_METHOD(ContainerInspect)
{
auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
const auto inspectJson = container.Inspect();
VERIFY_IS_FALSE(inspectJson.empty());
const auto id = container.Id();
VERIFY_IS_FALSE(id.empty());
// The inspect JSON must contain the container ID.
VERIFY_IS_TRUE(winrt::to_string(inspectJson).find(winrt::to_string(id)) != std::string::npos);
container.Delete(WSLCSDK::DeleteContainerOption::None);
cleanup.release();
}
WSLC_TEST_METHOD(ContainerExec)
{
// Start a long-running container so we can exec into it.
auto initProcSettings = WSLCSDK::ProcessSettings();
initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(initProcSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// Positive: exec a command that exits 0.
{
auto execSettings = WSLCSDK::ProcessSettings();
execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/true"}));
auto execProcess = container.CreateProcess(execSettings);
StartProcessAndWaitForExit(execProcess);
VERIFY_ARE_EQUAL(execProcess.ExitCode(), 0);
}
// Negative: no command line must fail.
VERIFY_THROWS_HR(container.CreateProcess(WSLCSDK::ProcessSettings()).Start(), E_INVALIDARG);
}
WSLC_TEST_METHOD(ContainerHostName)
{
// Unit: setting a hostname must succeed.
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.HostName(L"unit-test-host");
}
// Functional: container process should see the configured hostname.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(hostname)\" = my-test-host"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.HostName(L"my-test-host");
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
}
WSLC_TEST_METHOD(ContainerDomainName)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(domainname)\" = test.local"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.DomainName(L"test.local");
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
// -----------------------------------------------------------------------
// Process tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(ProcessEnvVariables)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$MY_TEST_VAR\" = hello-from-test"}));
procSettings.EnvironmentVariables(
winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"MY_TEST_VAR", L"hello-from-test"}}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
WSLC_TEST_METHOD(ProcessSignal)
{
// Negative: Signal() before Start() must throw.
{
auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
VERIFY_THROWS_HR(container.InitProcess().Signal(WSLCSDK::Signal::SIGKILL), E_ILLEGAL_METHOD_CALL);
}
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto process = container.InitProcess();
std::promise<void> promise;
auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
process.Signal(WSLCSDK::Signal::SIGKILL);
VERIFY_ARE_EQUAL(promise.get_future().wait_for(2min), std::future_status::ready);
const auto state = process.State();
VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited);
}
WSLC_TEST_METHOD(ProcessGetPid)
{
// Negative: Pid() before Start() must throw.
{
auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
VERIFY_THROWS_HR(std::ignore = container.InitProcess().Pid(), E_ILLEGAL_METHOD_CALL);
}
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
VERIFY_IS_TRUE(process.Pid() > 0);
}
WSLC_TEST_METHOD(ProcessGetExitCode)
{
auto runAndGetExitCode = [&](int code) -> int32_t {
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", winrt::to_hstring(std::format("exit {}", code))}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
auto exitCode = container.InitProcess().ExitCode();
container.Delete(WSLCSDK::DeleteContainerOption::Force);
return exitCode;
};
VERIFY_ARE_EQUAL(runAndGetExitCode(0), 0);
VERIFY_ARE_EQUAL(runAndGetExitCode(42), 42);
// Negative: querying ExitCode while process is still running must throw.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
}
}
WSLC_TEST_METHOD(ProcessGetState)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
// State while running.
VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
// Querying ExitCode while running must throw ERROR_INVALID_STATE.
VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
// Register the Exited event.
std::promise<int32_t> exitPromise;
auto token = process.Exited([&](int32_t code) { exitPromise.set_value(code); });
auto cleanupToken = wil::scope_exit([&]() { process.Exited(token); });
process.Signal(WSLCSDK::Signal::SIGKILL);
// The Exited event must fire after the signal.
auto future = exitPromise.get_future();
VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready);
const auto state = process.State();
VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited);
}
WSLC_TEST_METHOD(ProcessWorkingDirectory)
{
// Functional: container should see the configured working directory.
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(pwd)\" = /tmp"}));
procSettings.WorkingDirectory(L"/tmp");
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
// -----------------------------------------------------------------------
// Service tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(GetVersion)
{
auto version = WSLCSDK::WslcService::GetVersion();
VERIFY_IS_TRUE(version.Major() > 0 || version.Minor() > 0 || version.Revision() > 0);
}
WSLC_TEST_METHOD(GetMissingComponents)
{
const auto missing = WSLCSDK::WslcService::GetMissingComponents();
VERIFY_ARE_EQUAL(missing.Size(), 0u);
}
WSLC_TEST_METHOD(InstallWithDependencies)
{
// Pass null options to auto-detect and install any missing components (same behavior as the old no-arg call).
WSLCSDK::WslcService::InstallWithDependenciesAsync(nullptr).get();
VERIFY_ARE_EQUAL(WSLCSDK::WslcService::GetMissingComponents().Size(), 0u);
}
WSLC_TEST_METHOD(InstallOptions_DefaultValues)
{
// Default-constructed InstallOptions must have null Components and Repair=false.
auto options = WSLCSDK::InstallOptions();
VERIFY_IS_NULL(options.Components());
VERIFY_IS_FALSE(options.Repair());
}
WSLC_TEST_METHOD(InstallOptions_SetRepair)
{
auto options = WSLCSDK::InstallOptions();
options.Repair(true);
VERIFY_IS_TRUE(options.Repair());
options.Repair(false);
VERIFY_IS_FALSE(options.Repair());
}
WSLC_TEST_METHOD(InstallOptions_SetComponents)
{
auto options = WSLCSDK::InstallOptions();
auto components = winrt::single_threaded_vector<WSLCSDK::Component>({WSLCSDK::Component::WslPackage});
options.Components(components.GetView());
VERIFY_ARE_EQUAL(1u, options.Components().Size());
VERIFY_ARE_EQUAL(WSLCSDK::Component::WslPackage, options.Components().GetAt(0));
}
WSLC_TEST_METHOD(InstallWithDependencies_SdkNeedsUpdate_Throws)
{
// Passing SdkNeedsUpdate in the component list must throw WSLC_E_SDK_UPDATE_NEEDED.
auto options = WSLCSDK::InstallOptions();
auto components = winrt::single_threaded_vector<WSLCSDK::Component>({WSLCSDK::Component::SdkNeedsUpdate});
options.Components(components.GetView());
VERIFY_THROWS_HR(WSLCSDK::WslcService::InstallWithDependenciesAsync(options).get(), WSLC_E_SDK_UPDATE_NEEDED);
}
// -----------------------------------------------------------------------
// Process IO event tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(ProcessIoEventsUnit)
{
// Negative: registering OutputReceived/ErrorReceived without OutputMode::Event must throw.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
// GetOutputStream requires OutputMode::Stream — must throw with Discard mode (even after Start).
container.Start();
VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
}
// Positive: with OutputMode::Event, registering and revoking event handlers must succeed.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
auto stdoutToken = process.OutputReceived([](winrt::array_view<uint8_t const>) {});
auto stderrToken = process.ErrorReceived([](winrt::array_view<uint8_t const>) {});
auto exitToken = process.Exited([](int32_t) {});
process.OutputReceived(stdoutToken);
process.ErrorReceived(stderrToken);
process.Exited(exitToken);
// GetOutputStream throws when OutputMode is Event.
container.Start();
VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
}
// Negative: OutputReceived/ErrorReceived with OutputMode::Stream must throw.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
}
}
WSLC_TEST_METHOD(ProcessIoEventsInitProcess)
{
std::string stdoutData, stderrData;
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT && echo STDERR >&2"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto process = container.InitProcess();
process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
process.ErrorReceived([&](winrt::array_view<uint8_t const> data) {
stderrData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
// Start: claims IO handles and starts the IOCallback pump thread.
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(stdoutData, "STDOUT\n");
VERIFY_ARE_EQUAL(stderrData, "STDERR\n");
}
WSLC_TEST_METHOD(ProcessIoEventsExecProcess)
{
// Long-running init process to keep the container alive.
auto initProcSettings = WSLCSDK::ProcessSettings();
initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(initProcSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
std::string stdoutData, stderrData;
auto execProcSettings = WSLCSDK::ProcessSettings();
execProcSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo EXEC_OUT && echo EXEC_ERR >&2"}));
execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto execProcess = container.CreateProcess(execProcSettings);
execProcess.OutputReceived([&](winrt::array_view<uint8_t const> data) {
stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
execProcess.ErrorReceived([&](winrt::array_view<uint8_t const> data) {
stderrData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
StartProcessAndWaitForExit(execProcess);
VERIFY_ARE_EQUAL(stdoutData, "EXEC_OUT\n");
VERIFY_ARE_EQUAL(stderrData, "EXEC_ERR\n");
}
WSLC_TEST_METHOD(ProcessIoEventsHandleExclusion)
{
// Register an OutputReceived handler only. The IOCallback acquires ALL pipe handles
// (draining uncallbacked streams to prevent deadlock), so both stdout and stderr
// handles are consumed and neither can be obtained via GetOutputStream.
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto process = container.InitProcess();
process.OutputReceived([](winrt::array_view<uint8_t const>) {});
container.Start();
// stdout handle was consumed by the OutputReceived handler — must not be obtainable.
VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
// stderr handle was also consumed in order to drain it despite not having a handler.
VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), E_ILLEGAL_METHOD_CALL);
}
WSLC_TEST_METHOD(ProcessIoEventsExitCallback)
{
// Verify the Exited event fires with the correct exit code after IO has been flushed.
auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair<int32_t, std::string> {
std::string stdoutData;
std::promise<int32_t> exitPromise;
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
{L"/bin/sh", L"-c", winrt::hstring(std::format(L"echo HELLO && exit {}", exitCodeArg))}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto process = container.InitProcess();
process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
process.Exited([&](int32_t code) { exitPromise.set_value(code); });
container.Start();
auto future = exitPromise.get_future();
VERIFY_ARE_EQUAL(future.wait_for(60s), std::future_status::ready);
return {future.get(), stdoutData};
};
// Exit 0: Exited event must fire with code 0; IO must have been delivered first.
{
auto [exitCode, output] = RunAndCaptureExit(0);
VERIFY_ARE_EQUAL(exitCode, 0);
VERIFY_ARE_EQUAL(output, "HELLO\n");
}
// Non-zero exit: Exited event must report the correct code.
{
auto [exitCode, output] = RunAndCaptureExit(42);
VERIFY_ARE_EQUAL(exitCode, 42);
VERIFY_ARE_EQUAL(output, "HELLO\n");
}
}
WSLC_TEST_METHOD(ProcessIoEventsCancelOnRelease)
{
// Verify that releasing the process handle while an exec'd process is still running
// and writing IO cancels the event pump:
// - No IO events arrive after the handle is released.
// - Exited is never invoked (cancellation suppresses it).
// Long-running init process to keep the container alive.
auto initProcSettings = WSLCSDK::ProcessSettings();
initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"999"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(initProcSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
std::atomic<int> callbackCount{0};
std::atomic<bool> exitFired{false};
auto execProcSettings = WSLCSDK::ProcessSettings();
execProcSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"while true; do echo LINE; sleep 0.05; done"}));
execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto execProcess = container.CreateProcess(execProcSettings);
execProcess.OutputReceived([&](winrt::array_view<uint8_t const>) { callbackCount.fetch_add(1); });
execProcess.Exited([&](int32_t) { exitFired.store(true); });
execProcess.Start();
// Wait for events to start arriving.
Sleep(500);
VERIFY_IS_TRUE(callbackCount.load() > 0);
// Release the exec process handle while it is still running and writing.
execProcess = nullptr;
const int countAtRelease = callbackCount.load();
// Exited must not have fired: cancellation suppresses it.
VERIFY_IS_FALSE(exitFired.load());
// No further events after release.
Sleep(200);
VERIFY_ARE_EQUAL(callbackCount.load(), countAtRelease);
VERIFY_IS_FALSE(exitFired.load());
}
WSLC_TEST_METHOD(ProcessIoEventsLargeOutput)
{
// Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64
// 1,048,576 zero bytes → base64 output is 1,398,104 bytes.
static constexpr size_t c_expectedBytes = 1'398'104;
std::string stdoutData;
stdoutData.reserve(c_expectedBytes + 4096);
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
{L"/bin/sh", L"-c", L"dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto process = container.InitProcess();
process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
});
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes);
}
// -----------------------------------------------------------------------
// Storage tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(SessionCreateVhd)
{
constexpr auto c_volumeName = L"wslc-winrt-test-data-vol";
constexpr uint64_t c_vhdSizeBytes = 1ull * 1024 * 1024 * 1024; // 1 GiB
const std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-winrt-vhd-test-storage";
IGNORE_ERRORS(std::filesystem::remove_all(vhdSessionStorage));
auto cleanup = SCOPE_CLEANUP(std::filesystem::remove_all(vhdSessionStorage));
// Create a dedicated session so that volume creation does not affect the shared default session.
auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-vhd-test", vhdSessionStorage.wstring());
settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
auto session = WSLCSDK::Session(settings);
session.Start();
// Load debian.
const auto debianTar = GetTestImagePath("debian:latest");
session.LoadImageAsync(debianTar.wstring()).get();
// Positive: create a named VHD volume.
session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic));
// The backing VHD file must exist on disk.
const auto expectedVhdPath = vhdSessionStorage / "volumes" / (std::wstring(c_volumeName) + L".vhdx");
VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
// Positive: write a marker via a container that mounts the named volume.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
{L"/bin/sh", L"-c", L"echo wslc-winrt-vhd-test > /data/marker.txt"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
{WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", false)}));
auto container = session.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
// Positive: read back the marker in a second container (read-only mount).
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
{L"/bin/sh", L"-c", L"test \"$(cat /data/marker.txt)\" = wslc-winrt-vhd-test"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
{WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", true)}));
auto container = session.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
// Positive: delete the volume.
session.DeleteVhdVolume(c_volumeName);
VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath));
// Negative: zero size must fail.
VERIFY_THROWS_HR(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, 0, WSLCSDK::VhdType::Dynamic)), E_INVALIDARG);
// Positive: fixed-allocation VHD; on-disk file size must be >= Size.
{
constexpr auto c_fixedVolumeName = L"wslc-sdk-vhd-fixed";
constexpr auto c_fixedSizeBytes = 64ull * _1MB;
VERIFY_NO_THROW(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_fixedVolumeName, c_fixedSizeBytes, WSLCSDK::VhdType::Fixed)));
auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_fixedVolumeName));
std::filesystem::path expectedVhdPath = vhdSessionStorage / L"volumes" / (std::wstring(c_fixedVolumeName) + L".vhdx");
VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes);
}
// Positive: Owner() bakes uid/gid into the volume root inode at mkfs time.
// Verify by stat-ing the mount inside a container.
{
constexpr auto c_ownedVolumeName = L"wslc-sdk-vhd-owned";
auto vhdOptions = WSLCSDK::VhdOptions(c_ownedVolumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic);
vhdOptions.Owner(WSLCSDK::VhdOwner{65534, 65534}); // nobody:nogroup
session.CreateVhdVolume(vhdOptions);
auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_ownedVolumeName));
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/usr/bin/stat", L"-c", L"%u %g", L"/data"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
{WSLCSDK::ContainerNamedVolume(c_ownedVolumeName, L"/data", false)}));
auto container = session.CreateContainer(containerSettings);
StartContainerAndWaitForInitProcessExit(container);
auto output = GetProcessOutput(container.InitProcess());
VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
VERIFY_ARE_EQUAL(output.StandardOutput, L"65534 65534\n");
container.Delete(WSLCSDK::DeleteContainerOption::Force);
}
}
// -----------------------------------------------------------------------
// Authentication / registry tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(AuthenticateTests)
{
constexpr auto c_username = "wslctest";
constexpr auto c_password = "password";
auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
const auto serverUri = Uri(winrt::to_hstring(std::format("http://{}", registryAddress)));
// Negative: wrong password must fail.
VERIFY_THROWS_HR(m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), L"wrong-password"), E_FAIL);
// Positive: correct credentials must return a non-null token of the expected type.
{
const auto result = m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password));
VERIFY_IS_FALSE(result.IdentityToken().empty());
// The local test registry does not return an identity token, so credentials are embedded.
VERIFY_ARE_EQUAL(result.TokenType(), WSLCSDK::IdentityTokenType::Credentials);
}
const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password);
PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress));
auto cleanup = SCOPE_CLEANUP(m_defaultSession.DeleteImage(image));
// Positive: the IdentityToken from Authenticate can be passed directly as RegistryAuth.
{
const auto authResult = m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password));
auto opts = WSLCSDK::PullImageOptions(image);
opts.RegistryAuth(authResult.IdentityToken());
VERIFY_NO_THROW(m_defaultSession.PullImageAsync(opts).get());
VERIFY_IS_TRUE(HasImage(image));
}
// Negative: pulling without credentials must fail.
{
VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get(), E_FAIL);
}
// Negative: pulling with bad credentials must fail.
{
auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong");
auto opts = WSLCSDK::PullImageOptions(image);
opts.RegistryAuth(winrt::to_hstring(badAuth));
VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), E_FAIL);
}
}
WSLC_TEST_METHOD(PullImage)
{
auto [registryContainer, registryAddress] = StartLocalRegistry();
const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
{
PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress));
auto imageCleanup = SCOPE_CLEANUP(m_defaultSession.DeleteImage(image));
// Delete the image locally so the pull is a real network pull.
IGNORE_ERRORS(m_defaultSession.DeleteImage(image));
// Positive: pull from the local registry.
m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get();
VERIFY_IS_TRUE(HasImage(image));
// Verify the pulled image is runnable.
auto output = RunContainerAndWaitForExit(image, {});
VERIFY_ARE_EQUAL(output.ExitCode, 0);
}
// Negative: image that does not exist in the registry.
{
const auto missing = winrt::to_hstring(std::format("{}/does-not-exist", registryAddress));
auto opts = WSLCSDK::PullImageOptions(missing);
opts.RegistryAuth(winrt::to_hstring(xRegistryAuth));
VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), static_cast<HRESULT>(WSLC_E_IMAGE_NOT_FOUND));
}
// Negative: empty URI must fail.
VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"")).get(), E_INVALIDARG);
}
WSLC_TEST_METHOD(PushImage)
{
auto [registryContainer, registryAddress] = StartLocalRegistry();
const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
// Positive: push an existing image to the local registry.
PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
// Negative: pushing a non-existent image must fail.
VERIFY_THROWS_HR(
m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"does-not-exist", winrt::to_hstring(xRegistryAuth))).get(), E_FAIL);
// Negative: empty image name must fail.
VERIFY_THROWS_HR(m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"", winrt::to_hstring(xRegistryAuth))).get(), E_INVALIDARG);
}
WSLC_TEST_METHOD(TagImage)
{
// Positive: tag an existing image.
m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"winrt-sdk-test-tag"));
VERIFY_IS_TRUE(HasImage(L"debian:winrt-sdk-test-tag"));
auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(L"debian:winrt-sdk-test-tag");
// Negative: empty image name must fail.
VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"", L"debian", L"test")), E_INVALIDARG);
// Negative: empty repository must fail.
VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"", L"test")), E_INVALIDARG);
// Negative: empty tag must fail.
VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"")), E_INVALIDARG);
}
// -----------------------------------------------------------------------
// Negative / edge-case tests
// -----------------------------------------------------------------------
WSLC_TEST_METHOD(ExecOnStoppedContainer)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
// Wait for the short-lived init process to exit
StartContainerAndWaitForInitProcessExit(container);
// The init process has now exited. Attempting to exec on a stopped container must fail.
auto execSettings = WSLCSDK::ProcessSettings();
execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/echo", L"should-fail"}));
VERIFY_THROWS_HR(container.CreateProcess(execSettings).Start(), static_cast<HRESULT>(WSLC_E_CONTAINER_NOT_RUNNING));
}
WSLC_TEST_METHOD(DuplicateContainerName)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.Name(L"duplicate-name-test-winrt");
auto container1 = m_defaultSession.CreateContainer(containerSettings);
container1.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container1);
// Creating a second container with the same name must fail.
VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
}
WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce)
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
auto container = m_defaultSession.CreateContainer(containerSettings);
container.Start();
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
// Deleting a running container without Force must fail.
VERIFY_THROWS_HR(container.Delete(WSLCSDK::DeleteContainerOption::None), static_cast<HRESULT>(WSLC_E_CONTAINER_IS_RUNNING));
}
WSLC_TEST_METHOD(DeleteNonExistentImage)
{
VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent-image:this-tag-does-not-exist"), static_cast<HRESULT>(WSLC_E_IMAGE_NOT_FOUND));
}
WSLC_TEST_METHOD(PullInvalidImageUri)
{
VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"///invalid-registry-url///")).get(), E_INVALIDARG);
}
WSLC_TEST_METHOD(ContainerGpu)
{
// Negative: creating a GPU container on a session without GPU support must fail.
{
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.EnableGpu(true);
VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings).Start(), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
}
// Create a GPU-enabled session.
const std::filesystem::path gpuStorage = m_storagePath / "wslc-winrt-gpu-session-storage";
auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
std::error_code error;
std::filesystem::remove_all(gpuStorage, error);
});
auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-gpu-test", gpuStorage.wstring());
settings.EnableGpu(true);
settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
auto gpuSession = WSLCSDK::Session(settings);
gpuSession.Start();
const auto debianTar = GetTestImagePath("debian:latest");
gpuSession.LoadImageAsync(debianTar.wstring()).get();
// Positive: /dev/dxg must be available with read/write permissions, and the dynamic linker must be configured to resolve
// the WSL GPU libraries inside a GPU container.
{
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
{L"/bin/sh",
L"-c",
L"test -c /dev/dxg && test -r /dev/dxg && test -w /dev/dxg && cat /etc/ld.so.conf.d/ld.wsl.conf"}));
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.EnableGpu(true);
auto container = gpuSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
StartContainerAndWaitForInitProcessExit(container);
auto output = GetProcessOutput(container.InitProcess());
VERIFY_ARE_EQUAL(output.StandardOutput, L"/usr/lib/wsl/lib\n");
}
gpuSession.Terminate();
}
WSLC_TEST_METHOD(OpenContainer)
{
constexpr auto c_containerName = L"wslc-winrt-open-test";
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.Name(c_containerName);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
const auto createdId = container.Id();
// Positive: open by name — IDs must match.
{
auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Discard);
VERIFY_ARE_EQUAL(opened.Id(), createdId);
}
// Positive: open by full 64-character ID.
{
auto opened = m_defaultSession.OpenContainer(createdId, WSLCSDK::ProcessOutputMode::Discard);
VERIFY_ARE_EQUAL(opened.Id(), createdId);
}
// Positive: open by partial ID prefix (12 hex chars — standard short ID).
{
const auto shortId = winrt::hstring(std::wstring(createdId).substr(0, 12));
auto opened = m_defaultSession.OpenContainer(shortId, WSLCSDK::ProcessOutputMode::Discard);
VERIFY_ARE_EQUAL(opened.Id(), createdId);
}
// Negative: non-existent name must throw WSLC_E_CONTAINER_NOT_FOUND.
VERIFY_THROWS_HR(m_defaultSession.OpenContainer(L"no-such-container", WSLCSDK::ProcessOutputMode::Discard), WSLC_E_CONTAINER_NOT_FOUND);
// Negative: empty nameOrId must throw E_INVALIDARG.
VERIFY_THROWS_HR(m_defaultSession.OpenContainer(L"", WSLCSDK::ProcessOutputMode::Discard), E_INVALIDARG);
}
WSLC_TEST_METHOD(OpenContainerInitProcess)
{
// Verify InitProcess() behaviour for an opened container:
// - Before Start(), InitProcess() succeeds but the underlying process handle is not yet
// attached, so methods that require a running process (Pid, State) throw.
// - After Start(), the handle is attached and the process is fully usable.
constexpr auto c_containerName = L"wslc-winrt-open-initproc-test";
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"5"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.Name(c_containerName);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Stream);
// InitProcess() must succeed even before Start() (m_initProcess is always created for opened containers).
auto initProcess = opened.InitProcess();
// The process handle has not been attached yet — Pid() must throw.
VERIFY_THROWS_HR(std::ignore = initProcess.Pid(), E_ILLEGAL_METHOD_CALL);
// Start the opened container; the init process handle will be attached inside Start().
opened.Start();
// Now the init process handle is attached — Pid() must succeed and return a non-zero PID.
VERIFY_IS_GREATER_THAN(initProcess.Pid(), 0u);
opened.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero());
}
WSLC_TEST_METHOD(OpenContainerAndStartStreamMode)
{
// Verify that opening a container with ProcessOutputMode::Stream allows reading output
// from the init process via GetOutputStream after Start().
constexpr auto c_containerName = L"wslc-winrt-open-stream-test";
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT && echo STDERR >&2"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.Name(c_containerName);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Stream);
StartContainerAndWaitForInitProcessExit(opened);
auto output = GetProcessOutput(opened.InitProcess());
VERIFY_ARE_EQUAL(output.ExitCode, 0u);
VERIFY_ARE_EQUAL(output.StandardOutput, L"STDOUT\n");
VERIFY_ARE_EQUAL(output.StandardError, L"STDERR\n");
}
WSLC_TEST_METHOD(OpenContainerAndStartEventMode)
{
// Verify that opening a container with ProcessOutputMode::Event delivers output via
// OutputReceived / ErrorReceived events, and that Exited fires with the correct exit code.
constexpr auto c_containerName = L"wslc-winrt-open-event-test";
auto procSettings = WSLCSDK::ProcessSettings();
procSettings.CommandLine(
winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo EVENT_OUT && echo EVENT_ERR >&2"}));
auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
containerSettings.InitProcess(procSettings);
containerSettings.Name(c_containerName);
auto container = m_defaultSession.CreateContainer(containerSettings);
auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Event);
auto initProcess = opened.InitProcess();
std::string stdoutAccum, stderrAccum;
std::promise<int32_t> exitPromise;
auto stdoutRevoker = initProcess.OutputReceived(winrt::auto_revoke, [&](winrt::array_view<const uint8_t> data) {
stdoutAccum.append(reinterpret_cast<const char*>(data.data()), data.size());
});
auto stderrRevoker = initProcess.ErrorReceived(winrt::auto_revoke, [&](winrt::array_view<const uint8_t> data) {
stderrAccum.append(reinterpret_cast<const char*>(data.data()), data.size());
});
auto exitRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t exitCode) { exitPromise.set_value(exitCode); });
opened.Start();
auto exitFuture = exitPromise.get_future();
VERIFY_ARE_EQUAL(exitFuture.wait_for(30s), std::future_status::ready);
VERIFY_ARE_EQUAL(exitFuture.get(), 0);
VERIFY_ARE_EQUAL(stdoutAccum, "EVENT_OUT\n");
VERIFY_ARE_EQUAL(stderrAccum, "EVENT_ERR\n");
}
};