Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcWslcLifecycleE2ETests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Mxc.Sdk;
using Xunit;

namespace Microsoft.Mxc.Sdk.Tests;

/// <summary>
/// Drives the state-aware lifecycle against a live WSLC host, which needs
/// Windows, a host running WSL2 with the WSLC runtime, and a native library
/// built with the wslc feature.
/// </summary>
/// <remarks>
/// These establish that the lifecycle works end to end through this binding
/// rather than only through the engine: the id provision mints, and the output
/// and exit code an exec returns. The image is expected to be cached already;
/// pulling is covered by the WSLC executor E2E suite.
/// </remarks>
[Collection("MxcLiveHost")]
public class MxcWslcLifecycleE2ETests
{
/// <summary>
/// Deprovisions a sandbox the test did not deprovision itself. Provision
/// mints a real container, so an assertion that throws part-way would
/// otherwise leave it on the host.
/// </summary>
private sealed class Teardown : IDisposable
{
private SandboxId? _id;

public Teardown(SandboxId id) => _id = id;

/// <summary>
/// Gives up ownership once the test has deprovisioned itself.
/// Deprovision is not idempotent, so without this the disposal below
/// would report a failure that did not happen.
/// </summary>
public void Defuse() => _id = null;

public void Dispose()
{
if (_id is not { } id)
{
return;
}
_id = null;
try
{
MxcLifecycle.StopSandbox(id);
}
catch (MxcException)
{
// A sandbox that never started, or already stopped, still has to
// be deprovisioned — that is the step that frees the container.
}
try
{
MxcLifecycle.DeprovisionSandbox(id);
}
catch (MxcException e)
{
Console.Error.WriteLine(
$"WARNING: deprovision failed, the container may leak: {e.Message}");
}
}
}

private sealed record Started(SandboxId Id, Teardown Teardown);

private static Started Provision()
{
var provisioned = MxcLifecycle.ProvisionSandbox(
StateAwareContainment.Wslc,
new WslcProvisionOptions { Image = WslcHost.Image });
return new Started(provisioned.SandboxId, new Teardown(provisioned.SandboxId));
}

private static Started ProvisionAndStart()
{
var started = Provision();
try
{
MxcLifecycle.StartSandbox(started.Id);
return started;
}
catch
{
started.Teardown.Dispose();
throw;
}
}

/// <summary>Provision reaches the backend through this binding and mints an
/// id the later phases accept.</summary>
[Fact]
public void Provision_ReturnsAUsableSandboxId()
{
WslcHost.Require();

var started = Provision();
using (started.Teardown)
{
// Post-provision phases resolve the backend from this prefix.
Assert.StartsWith("wslc:", started.Id.Value, StringComparison.Ordinal);

MxcLifecycle.StartSandbox(started.Id);
}
}

/// <summary>The lifecycle runs a command and its output reaches the caller.</summary>
[Fact]
public async Task Lifecycle_RunsEndToEnd()
{
WslcHost.Require();

var started = ProvisionAndStart();
using (started.Teardown)
{
var run = await MxcLifecycle.ExecInSandboxAsync(
started.Id,
"echo dotnet-wslc-marker",
TestContext.Current.CancellationToken);

Assert.False(run.TimedOut);
Assert.Equal(0, run.ExitCode);
Assert.Contains("dotnet-wslc-marker", run.Stdout);
}
}

/// <summary>The sandboxed process's exit code must reach the caller
/// unchanged, through the streaming handle's wait.</summary>
[Fact]
public void Exec_PropagatesANonZeroExitCode()
{
WslcHost.Require();

var started = ProvisionAndStart();
using (started.Teardown)
{
using var proc = MxcLifecycle.ExecInSandbox(started.Id, "exit 7");
var result = proc.Wait();

Assert.False(result.TimedOut);
Assert.Equal(7, result.ExitCode);
}
}

/// <summary>
/// Stop and deprovision must be reachable through this binding, and a
/// deprovisioned id must not still be usable.
/// </summary>
[Fact]
public void Deprovision_RetiresTheSandboxId()
{
WslcHost.Require();

var started = ProvisionAndStart();
using (started.Teardown)
{
MxcLifecycle.StopSandbox(started.Id);
MxcLifecycle.DeprovisionSandbox(started.Id);
started.Teardown.Defuse();

var ex = Assert.Throws<MxcException>(
() => MxcLifecycle.StartSandbox(started.Id));
Assert.Equal(ErrorCode.NotProvisioned, ex.Code);
}
}
}
43 changes: 43 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/WslcHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Mxc.Sdk;
using Xunit;

namespace Microsoft.Mxc.Sdk.Tests;

/// <summary>
/// The live-host gate the WSLC suite shares. A live run needs Windows, a host
/// running WSL2 with the WSLC runtime, and a native library built with the
/// wslc feature.
/// </summary>
internal static class WslcHost
{
// Evaluated once: this answer decides failure versus skip, so it has to be
// the same for every test that consults it.
private static readonly Lazy<bool> Available = new(() =>
MxcSandbox.GetAvailableBackends()
.Any(b => b.Backend == ContainmentBackend.Wslc));

// Without this, a run in which everything skipped is indistinguishable from
// one that passed. The same variable the Rust WSLC suite honours.
private static bool SkipsAreFailures =>
Environment.GetEnvironmentVariable("MXC_WSLC_TESTS_REQUIRED") is "1" or "true";

internal static string Image =>
Environment.GetEnvironmentVariable("MXC_WSLC_TEST_IMAGE") ?? "alpine:latest";

/// <summary>Skips the calling test when the backend is unavailable, or fails
/// it when skips have been declared failures.</summary>
internal static void Require()
{
Assert.False(
SkipsAreFailures && !Available.Value,
"MXC_WSLC_TESTS_REQUIRED is set, but GetAvailableBackends() does not "
+ "report the WSLC backend. That needs both a build with "
+ "MxcWithWslc and a host running WSL2 with the WSLC runtime.");
Assert.SkipUnless(
Available.Value,
"GetAvailableBackends() does not report the WSLC backend");
}
}
8 changes: 7 additions & 1 deletion sdk/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,13 @@ exception messages and stack traces. See
The isolation-session end-to-end tests skip unless `GetAvailableBackends()`
reports that backend, which needs both a build with
`-p:MxcWithIsolationSession=true` and a host running the OS-side service. Set
`MXC_ISO_TESTS_REQUIRED=1` (or `true`) to turn those skips into failures.
`MXC_ISO_TESTS_REQUIRED=1` (or `true`) to turn those skips into failures. The
WSLC end-to-end tests work the same way against
`-p:MxcWithWslc=true` and a host running WSL2 with the WSLC runtime, with
`MXC_WSLC_TESTS_REQUIRED=1` as their gate and `MXC_WSLC_TEST_IMAGE` selecting
the image to provision from (the default `alpine:latest` is expected to be
cached already). Both live-host suites share one xUnit collection, so they
never run concurrently.

Build/test everything: `dotnet test --solution sdk/dotnet/Microsoft.Mxc.Sdk.slnx`.

Expand Down
22 changes: 22 additions & 0 deletions src/core/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,28 @@ host probe fails closed, which is why [`platform_support`] drops `"wslc"`, and a
`Containment::Wslc` run then fails with an error naming the directory it
searched.

#### Host-gated tests

`tests/wslc_state_aware.rs` drives the live lifecycle — provision, start, exec,
deprovision — against a host running WSL2 with the WSLC runtime. A cargo
integration test runs from `target/<profile>/deps/`, one directory below where
the profile build stages the runtime files, so the suite copies both of them
beside its own binary first. Without that copy the host probe finds no
`wslcsdk.dll`, drops `"wslc"`, and every test skips on a host that can run it.

Build the daemon before running the suite, because `--features wslc` does not
pull it into the dependency graph:

```text
cargo build -p wxc_wslc_daemon -p mxc-sdk --features mxc-sdk/wslc
cargo test -p mxc-sdk --features wslc --test wslc_state_aware
```

| Variable | Effect |
|----------|--------|
| `MXC_WSLC_TESTS_REQUIRED=1` | Turns every skip into a failure, so a run that tested nothing is distinguishable from one that passed. |
| `MXC_WSLC_TEST_IMAGE` | Selects the image to provision from; the default `alpine:latest` is expected to be cached already. |

## Telemetry consent

MXC only ever collects telemetry on Windows, and only after the end user has
Expand Down
Loading
Loading