mirror of
https://github.com/microsoft/WSL.git
synced 2026-05-31 05:47:06 -05:00
1685 lines
71 KiB
C++
1685 lines
71 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 "winrt/Session.h"
|
|
#include "winrt/Helpers.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::DeleteContainerFlags::Force))
|
|
#define DELETE_IMAGE_ON_SCOPE_EXIT(imageName) SCOPE_CLEANUP(m_defaultSession.DeleteImage(imageName))
|
|
|
|
struct ProcessOutput
|
|
{
|
|
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);
|
|
}
|
|
|
|
ProcessOutput GetProcessOutput(WSLCSDK::Process const& process)
|
|
{
|
|
ProcessOutput output;
|
|
output.ExitCode = process.ExitCode();
|
|
output.StandardOutput = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput));
|
|
output.StandardError = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError));
|
|
|
|
return output;
|
|
}
|
|
|
|
struct RunContainerOptions
|
|
{
|
|
std::vector<winrt::hstring> cmdLine = {};
|
|
WSLCSDK::ContainerFlags flags = WSLCSDK::ContainerFlags::None;
|
|
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.
|
|
ProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {})
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
if (!options.cmdLine.empty())
|
|
{
|
|
procSettings.CmdLine(winrt::single_threaded_vector(std::move(options.cmdLine)));
|
|
}
|
|
|
|
procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
|
|
|
|
auto containerSettings = WSLCSDK::ContainerSettings(imageName);
|
|
containerSettings.InitProcess(procSettings);
|
|
containerSettings.Flags(options.flags);
|
|
|
|
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::DeleteContainerFlags::Force));
|
|
|
|
return output;
|
|
}
|
|
|
|
bool HasImage(winrt::hstring const& imageName)
|
|
{
|
|
auto images = m_defaultSession.Images();
|
|
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)
|
|
{
|
|
VERIFY_IS_TRUE(HasImage(L"wslc-registry:latest"));
|
|
|
|
std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
|
|
if (!username.empty())
|
|
{
|
|
env.push_back(std::format("USERNAME={}", username));
|
|
env.push_back(std::format("PASSWORD={}", password));
|
|
}
|
|
|
|
wsl::windows::common::WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
|
|
launcher.SetEntrypoint({"/entrypoint.sh"});
|
|
launcher.AddPort(port, port, AF_INET);
|
|
|
|
// Get the IWSLCSession COM object from the SDK session handle.
|
|
auto& comSession = *reinterpret_cast<WslcSessionImpl*>(WSLCSDK::implementation::GetHandle(m_defaultSession))->session;
|
|
auto container = launcher.Launch(comSession, WSLCContainerStartFlagsNone);
|
|
|
|
auto registryAddress = std::format("127.0.0.1:{}", port);
|
|
|
|
// Wait for the registry to be ready by probing from the host.
|
|
auto hostUrl = std::format(L"http://{}", registryAddress);
|
|
ExpectHttpResponse(hostUrl.c_str(), 200, true);
|
|
|
|
return {std::move(container), registryAddress};
|
|
}
|
|
|
|
// 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.MemoryMB(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.MemoryMB(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.Images(), 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);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Image tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
WSLC_TEST_METHOD(ImageList)
|
|
{
|
|
// Session has images pre-loaded - list must return at least one entry.
|
|
const auto images = m_defaultSession.Images();
|
|
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.SizeBytes() != 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, {.cmdLine = {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", {.cmdLine = {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", {.cmdLine = {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.CmdLine(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.CmdLine(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::DeleteContainerFlags::None));
|
|
VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Deleted);
|
|
}
|
|
|
|
WSLC_TEST_METHOD(ProcessIOHandles)
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(
|
|
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",
|
|
{.cmdLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
|
|
.flags = WSLCSDK::ContainerFlags::None,
|
|
.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",
|
|
{.cmdLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
|
|
.flags = WSLCSDK::ContainerFlags::None,
|
|
.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.CmdLine(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"}}));
|
|
|
|
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);
|
|
|
|
ExpectHttpResponse(L"http://127.0.0.1:12341", 200, true);
|
|
}
|
|
|
|
// Functional: port mapping with explicit IPv4 WindowsAddress.
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(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"}}));
|
|
|
|
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);
|
|
|
|
ExpectHttpResponse(L"http://127.0.0.1:12343", 200, true);
|
|
}
|
|
|
|
// Functional: port mapping with explicit IPv6 WindowsAddress.
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(
|
|
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"}}));
|
|
|
|
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);
|
|
|
|
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::DeleteContainerFlags::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.CmdLine(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::DeleteContainerFlags::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::DeleteContainerFlags::None);
|
|
cleanup.release();
|
|
}
|
|
|
|
WSLC_TEST_METHOD(ContainerExec)
|
|
{
|
|
// Start a long-running container so we can exec into it.
|
|
auto initProcSettings = WSLCSDK::ProcessSettings();
|
|
initProcSettings.CmdLine(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.CmdLine(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.CmdLine(
|
|
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::DeleteContainerFlags::Force);
|
|
}
|
|
}
|
|
|
|
WSLC_TEST_METHOD(ContainerDomainName)
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(
|
|
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::DeleteContainerFlags::Force);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Process tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
WSLC_TEST_METHOD(ProcessEnvVariables)
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(
|
|
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::DeleteContainerFlags::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.CmdLine(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.CmdLine(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.CmdLine(
|
|
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::DeleteContainerFlags::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.CmdLine(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.CmdLine(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.CmdLine(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::DeleteContainerFlags::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, WSLCSDK::ComponentFlags::None);
|
|
}
|
|
|
|
WSLC_TEST_METHOD(InstallWithDependencies)
|
|
{
|
|
WSLCSDK::WslcService::InstallWithDependenciesAsync().get();
|
|
VERIFY_ARE_EQUAL(WSLCSDK::WslcService::GetMissingComponents(), WSLCSDK::ComponentFlags::None);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Process IO event tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
WSLC_TEST_METHOD(ProcessIoEventsUnit)
|
|
{
|
|
// Negative: registering OutputReceived/ErrorReceived without OutputMode::Event must throw.
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(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.CmdLine(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.CmdLine(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.CmdLine(
|
|
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.CmdLine(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.CmdLine(
|
|
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.CmdLine(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.CmdLine(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.CmdLine(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.CmdLine(
|
|
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.CmdLine(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.CmdLine(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::DeleteContainerFlags::Force);
|
|
}
|
|
|
|
// Positive: read back the marker in a second container (read-only mount).
|
|
{
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(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::DeleteContainerFlags::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 >= SizeBytes.
|
|
{
|
|
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: SetOwner() 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.SetOwner(65534, 65534); // nobody:nogroup
|
|
session.CreateVhdVolume(vhdOptions);
|
|
|
|
auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_ownedVolumeName));
|
|
|
|
auto procSettings = WSLCSDK::ProcessSettings();
|
|
procSettings.CmdLine(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::DeleteContainerFlags::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
|
|
VERIFY_NO_THROW(m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password)));
|
|
|
|
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));
|
|
|
|
// Positive: pulling with correct credentials must succeed.
|
|
{
|
|
auto opts = WSLCSDK::PullImageOptions(image);
|
|
opts.RegistryAuth(winrt::to_hstring(xRegistryAuth));
|
|
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));
|
|
|
|
// 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.CmdLine(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.CmdLine(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.CmdLine(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.CmdLine(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::DeleteContainerFlags::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.Flags(WSLCSDK::ContainerFlags::EnableGpu);
|
|
|
|
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.FeatureFlags(WSLCSDK::SessionFeatureFlags::EnableGpu);
|
|
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.CmdLine(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.Flags(WSLCSDK::ContainerFlags::EnableGpu);
|
|
|
|
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();
|
|
}
|
|
};
|