File: Commands\WslEnvironmentCheckTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Utils.EnvironmentChecker;
 
namespace Aspire.Cli.Tests.Commands;
 
public class WslEnvironmentCheckTests
{
    // Real banner from a WSL 1 distribution. WSL 1 has no kernel of its own and always reports this
    // fixed 4.4.0 compatibility version, which is why an ordinal "major >= 4" comparison misreads it
    // as WSL 2. See https://learn.microsoft.com/windows/wsl/compare-versions
    private const string Wsl1Banner =
        "Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020";
 
    // Historical WSL 1 compatibility banner from https://github.com/Microsoft/WSL/issues/1090.
    // This release string is synthetic and should still be treated as WSL 1, not a real Linux kernel.
    private const string HistoricalWsl1Banner =
        "Linux version 3.4.0-Microsoft (Microsoft@Microsoft.com) (gcc version 4.7 (GCC) ) #1 SMP PREEMPT Wed Dec 31 14:42:53 PST 2014";
 
    // Parseable custom kernel release used by .wslconfig. It carries no WSL-specific tokens, so
    // the WSL environment check must rely on the surrounding shell signals instead of the banner.
    private const string CustomKernelBanner =
        "Linux version 6.1.0-custom (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string CustomTerminalPlusKernelBanner =
        "Linux version 6.1.0+ (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string Wsl2Banner =
        "Linux version 5.15.90.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0) #1 SMP Fri Jan 27 02:56:13 UTC 2023";
 
    private const string EarlyWsl2Banner =
        "Linux version 4.19.84-microsoft-standard (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 SMP Wed Nov 13 11:44:37 UTC 2019";
 
    // Some Microsoft-shipped 4.19 WSL 2 kernels used the legacy "...-microsoft-WSL2-standard"
    // release shape before Microsoft standardized on "...-microsoft-standard[-WSL2]".
    private const string LegacyWsl2Banner =
        "Linux version 4.19.121-microsoft-WSL2-standard (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 SMP Fri Jul 10 23:59:06 UTC 2020";
 
    private const string NewerWsl2Banner =
        "Linux version 6.6.87.2-microsoft-standard-WSL2 (root@builder) (gcc (GCC) 11.2.0) #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025";
 
    private const string MixedCaseWsl2Banner =
        "Linux version 5.15.90.1-MICROSOFT-STANDARD-wsl2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0) #1 SMP Fri Jan 27 02:56:13 UTC 2023";
 
    private const string MixedCaseEarlyWsl2Banner =
        "Linux version 4.19.84-Microsoft-Standard (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 SMP Wed Nov 13 11:44:37 UTC 2019";
 
    private const string CustomWsl2LookingBanner =
        "Linux version 6.1.0-custom-WSL2 (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string CustomMicrosoftStandardLookingBanner =
        "Linux version 6.1.0-microsoft-standard-custom (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string CustomPrefixWsl2LookingBanner =
        "Linux version 6.1.0-custom-microsoft-standard-WSL2 (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string CustomPrefixMicrosoftStandardLookingBanner =
        "Linux version 6.1.0-custom-microsoft-standard (builder@host) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024";
 
    private const string NativeLinuxBanner =
        "Linux version 6.8.0-64-generic (buildd@lcy02-amd64-029) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0) #67-Ubuntu SMP PREEMPT_DYNAMIC Sun Jun 15 20:23:31 UTC 2025";
 
    // A native kernel whose build identity happens to carry a marker this check looks for. /proc/version
    // records who built the kernel and with what, and none of that says anything about WSL.
    private const string NativeLinuxBannerBuiltByMicrosoft =
        "Linux version 6.8.0-64-generic (Microsoft@builder) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0) #67-Ubuntu SMP PREEMPT_DYNAMIC Sun Jun 15 20:23:31 UTC 2025";
 
    // A malformed nonblank /proc/version line that cannot yield a kernel release.
    private const string MalformedProcVersionBanner = "Linux version";
 
    // A malformed release token that contains a Microsoft marker but no numeric kernel version.
    private const string MalformedKernelReleaseBanner =
        "Linux version unknown-microsoft-standard-WSL2 (builder@host) (gcc version 11.2.0) #1 SMP";
 
    public static TheoryData<string?, string?, string> Wsl2PassCases => new()
    {
        { null, null, Wsl2Banner },
        { "", null, Wsl2Banner },
        { "   ", null, Wsl2Banner },
        // Once WSL is established by the shell, a parseable non-WSL1 kernel is WSL2.
        { "Ubuntu-22.04", null, NativeLinuxBanner },
        { null, "/run/WSL/interop", NativeLinuxBanner },
        { "Ubuntu-22.04", null, Wsl2Banner },
        { null, "/run/WSL/interop", Wsl2Banner },
    };
 
    public static TheoryData<string?, string?, string> Wsl1EnvironmentSignalCases => new()
    {
        { "Ubuntu-22.04", null, Wsl1Banner },
        { null, "/run/WSL/interop", Wsl1Banner },
        { "Ubuntu-22.04", "/run/WSL/interop", Wsl1Banner },
        { "Ubuntu-22.04", null, HistoricalWsl1Banner },
        { null, "/run/WSL/interop", HistoricalWsl1Banner },
        { "Ubuntu-22.04", "/run/WSL/interop", HistoricalWsl1Banner },
    };
 
    public static TheoryData<string?, string?, string?> WslUnreadableBannerSignalCases => new()
    {
        { "Ubuntu-22.04", null, null },
        { "Ubuntu-22.04", null, "   " },
        { "Ubuntu-22.04", null, MalformedProcVersionBanner },
        { "Ubuntu-22.04", null, MalformedKernelReleaseBanner },
        { null, "/run/WSL/interop", null },
        { null, "/run/WSL/interop", "   " },
        { null, "/run/WSL/interop", MalformedProcVersionBanner },
        { null, "/run/WSL/interop", MalformedKernelReleaseBanner },
    };
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl1_ForRealWsl1Banner()
    {
        // Regression test: WSL 1 reports kernel 4.4.0, so classifying by "major version >= 4"
        // reported every real WSL 1 system as WSL 2 and suppressed the limited-container warning.
        Assert.Equal(WslVersion.Wsl1, WslEnvironmentCheck.DetermineWslVersion(Wsl1Banner));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl1_ForHistoricalWsl1Banner()
    {
        Assert.Equal(WslVersion.Wsl1, WslEnvironmentCheck.DetermineWslVersion(HistoricalWsl1Banner));
    }
 
    [Theory]
    [InlineData("Linux version 3.4.0-Microsoft-custom (builder@host) (gcc version 4.7) #1 SMP")]
    [InlineData("Linux version 3.4.0-custom-Microsoft (builder@host) (gcc version 4.7) #1 SMP")]
    public void DetermineWslVersion_ReportsUnknown_ForUnofficialHistoricalWsl1Suffixes(string procVersion)
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl2_ForRealWsl2Banner()
    {
        Assert.Equal(WslVersion.Wsl2, WslEnvironmentCheck.DetermineWslVersion(Wsl2Banner));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl2_ForEarlyMicrosoftStandardKernel()
    {
        Assert.Equal(WslVersion.Wsl2, WslEnvironmentCheck.DetermineWslVersion(EarlyWsl2Banner));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl2_ForLegacyMicrosoftWsl2StandardKernel()
    {
        Assert.Equal(WslVersion.Wsl2, WslEnvironmentCheck.DetermineWslVersion(LegacyWsl2Banner));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsWsl2_ForNewerWsl2Kernel()
    {
        Assert.Equal(WslVersion.Wsl2, WslEnvironmentCheck.DetermineWslVersion(NewerWsl2Banner));
    }
 
    [Theory]
    // Official suffix matching is case-insensitive because the kernel release casing is not stable.
    [InlineData(MixedCaseEarlyWsl2Banner)]
    [InlineData(MixedCaseWsl2Banner)]
    public void DetermineWslVersion_ReportsWsl2_ForOfficialKernelReleaseSuffixes_WithDifferentCasing(string procVersion)
    {
        Assert.Equal(WslVersion.Wsl2, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsUnknown_WhenBannerIsUnavailable()
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(null));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsUnknown_WhenBannerIsBlank()
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion("   "));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsUnknown_ForCustomKernelWithoutMarkers()
    {
        // A custom kernel configured through .wslconfig carries neither the WSL2 marker nor the
        // WSL 1 compatibility banner, so neither version can be claimed.
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(CustomKernelBanner));
    }
 
    [Fact]
    public void DetermineWslVersion_ReportsUnknown_WhenMicrosoftBannerHasNoRecognizableVersion()
    {
        // Regression test for the opposite failure: an unparseable Microsoft banner used to fall
        // through to WSL 1 and tell the user to upgrade to WSL 2 they may already be running.
        Assert.Equal(
            WslVersion.Unknown,
            WslEnvironmentCheck.DetermineWslVersion("Linux version unknown-microsoft (build@host)"));
    }
 
    [Theory]
    // A 4.4.x kernel that is not the fixed WSL 1 compatibility release, built by someone whose name
    // happens to contain "Microsoft". Matching only the "4.4." prefix plus a "Microsoft" mention
    // anywhere in the banner reported this as WSL 1 and recommended an upgrade that does not apply.
    [InlineData("Linux version 4.4.1-custom (Microsoft@builder) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024")]
    // The WSL 1 build number is required: a 4.4.0 release without it is a stock kernel, not WSL 1.
    [InlineData("Linux version 4.4.0-generic (Microsoft@builder) #1 SMP Tue Jan 2 00:00:00 UTC 2024")]
    public void DetermineWslVersion_ReportsUnknown_ForNonWsl1KernelsMentioningMicrosoft(string procVersion)
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    // Real WSL 1 banners across Windows builds. The build number varies, the rest of the release does not.
    [InlineData("Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020")]
    [InlineData("Linux version 4.4.0-18362-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #476-Microsoft Fri Nov 01 16:53:00 PST 2019")]
    [InlineData("Linux version 4.4.0-43-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #1-Microsoft Wed Dec 31 14:42:53 PST 2014")]
    public void DetermineWslVersion_ReportsWsl1_AcrossWsl1BuildNumbers(string procVersion)
    {
        Assert.Equal(WslVersion.Wsl1, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    // The WSL 2 marker in the build identity rather than the kernel release. Scanning the whole banner
    // reported this custom kernel as a confident WSL 2 pass.
    [InlineData("Linux version 6.1.0-custom (wsl2@builder) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024")]
    // Same for the early WSL 2 marker: "microsoft-standard" in the build identity is not a kernel release.
    [InlineData("Linux version 6.1.0-custom (microsoft-standard@builder) (gcc version 11.2.0) #1 SMP Tue Jan 2 00:00:00 UTC 2024")]
    public void DetermineWslVersion_ReportsUnknown_WhenMarkersAppearOnlyInBuildMetadata(string procVersion)
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    // These releases only borrow official WSL 2 tokens as substrings. Matching them with Contains(...)
    // reported them as real WSL 2 kernels and suppressed the unknown-version warning.
    [InlineData(CustomWsl2LookingBanner)]
    [InlineData(CustomMicrosoftStandardLookingBanner)]
    public void DetermineWslVersion_ReportsUnknown_ForUnofficialKernelReleaseSuffixes(string procVersion)
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    // These kernels end with the official tokens but insert extra release segments before them.
    // Accepting them would treat a custom kernel name as if it were an observed Microsoft release.
    [InlineData(CustomPrefixWsl2LookingBanner)]
    [InlineData(CustomPrefixMicrosoftStandardLookingBanner)]
    public void DetermineWslVersion_ReportsUnknown_ForUnofficialKernelReleasePrefixes(string procVersion)
    {
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Fact]
    public void CreateResult_MakesTheUpgradeConditional_WhenVersionIsUnknown()
    {
        // A missing or malformed banner can classify an established WSL 2 environment as Unknown,
        // so unconditional advice could tell that user to upgrade an environment already on WSL 2.
        Assert.Equal(
            "Run 'wsl --list --verbose' from Windows to check the version. If it reports 1, upgrade with: wsl --set-version <distro> 2",
            WslEnvironmentCheck.CreateResult(WslVersion.Unknown).Fix);
    }
 
    [Fact]
    public void CreateResult_ReportsWarning_WhenVersionIsUnknown()
    {
        // The core three-state guarantee: an undetermined version must never be reported as healthy.
        var result = WslEnvironmentCheck.CreateResult(WslVersion.Unknown);
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL detected but the version could not be determined", result.Message);
        Assert.Contains("/proc/version", result.Details);
        Assert.NotNull(result.Fix);
    }
 
    [Fact]
    public void CreateResult_ReportsWarning_ForWsl1()
    {
        var result = WslEnvironmentCheck.CreateResult(WslVersion.Wsl1);
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL1 detected - limited container support", result.Message);
    }
 
    [Fact]
    public void CreateResult_ReportsPass_ForWsl2()
    {
        var result = WslEnvironmentCheck.CreateResult(WslVersion.Wsl2);
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
    }
 
    [Fact]
    public async Task CheckAsync_ReportsNothing_WhenNotLinux()
    {
        var check = new WslEnvironmentCheck(TestEnvironment.CreateMacOS(), () => Wsl1Banner);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task CheckAsync_ReportsNothing_WhenLinuxIsNotWsl(string? distroName)
    {
        // Blank WSL_DISTRO_NAME values should behave like the signal is absent.
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment(distroName),
            () => NativeLinuxBanner);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    public async Task CheckAsync_ReportsNothing_WhenWslInteropIsBlank(string wslInterop)
    {
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment(null, wslInterop),
            () => NativeLinuxBanner);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData(NativeLinuxBannerBuiltByMicrosoft)]
    [InlineData("Linux version 6.8.0-64-generic (wsl2@builder) (gcc version 13.3.0) #67 SMP PREEMPT_DYNAMIC Sun Jun 15 20:23:31 UTC 2025")]
    public async Task CheckAsync_ReportsNothing_WhenOnlyBuildMetadataMentionsWsl(string procVersion)
    {
        // Detecting WSL from anywhere in the banner turned a native kernel into a "WSL detected but the
        // version could not be determined" warning on a machine that has nothing to do with WSL.
        var check = new WslEnvironmentCheck(TestEnvironment.CreateLinux(), () => procVersion);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData(CustomWsl2LookingBanner)]
    [InlineData(CustomMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsNothing_ForUnofficialKernelReleaseSuffixes_WhenWslDistroNameIsNotSet(string procVersion)
    {
        var check = new WslEnvironmentCheck(TestEnvironment.CreateLinux(), () => procVersion);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData(CustomPrefixWsl2LookingBanner)]
    [InlineData(CustomPrefixMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsNothing_ForUnofficialKernelReleasePrefixes_WhenWslDistroNameIsNotSet(string procVersion)
    {
        var check = new WslEnvironmentCheck(TestEnvironment.CreateLinux(), () => procVersion);
 
        Assert.Empty(await check.CheckAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [MemberData(nameof(Wsl2PassCases))]
    public async Task CheckAsync_ReportsPass_WhenWsl2IsDetected(
        string? distroName,
        string? wslInterop,
        string procVersion)
    {
        var check = new WslEnvironmentCheck(CreateLinuxEnvironment(distroName, wslInterop), () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
    }
 
    [Fact]
    public async Task CheckAsync_ReportsWarning_ForWsl1()
    {
        var check = new WslEnvironmentCheck(TestEnvironment.CreateLinux(), () => Wsl1Banner);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL1 detected - limited container support", result.Message);
    }
 
    [Fact]
    public async Task CheckAsync_ReportsWarning_ForHistoricalWsl1Banner()
    {
        var check = new WslEnvironmentCheck(TestEnvironment.CreateLinux(), () => HistoricalWsl1Banner);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL1 detected - limited container support", result.Message);
    }
 
    [Theory]
    [MemberData(nameof(Wsl1EnvironmentSignalCases))]
    public async Task CheckAsync_ReportsWarning_ForWsl1Banners_WhenWslDistroNameOrWslInteropIsSet(
        string? distroName,
        string? wslInterop,
        string procVersion)
    {
        // WSL_DISTRO_NAME and WSL_INTEROP establish that the shell is already inside WSL. Those
        // environment signals should not hide a WSL1 banner behind the generic unknown warning.
        var check = new WslEnvironmentCheck(CreateLinuxEnvironment(distroName, wslInterop), () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL1 detected - limited container support", result.Message);
    }
 
    [Theory]
    [InlineData(CustomWsl2LookingBanner)]
    [InlineData(CustomMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsPass_ForUnofficialKernelReleaseSuffixes_WhenWslDistroNameIsSet(string procVersion)
    {
        // WSL_DISTRO_NAME means we're already inside WSL. A parseable custom kernel should be treated
        // as WSL2 instead of being downgraded to "unknown".
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment("Ubuntu-22.04"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    [InlineData(CustomPrefixWsl2LookingBanner)]
    [InlineData(CustomPrefixMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsPass_ForUnofficialKernelReleasePrefixes_WhenWslDistroNameIsSet(string procVersion)
    {
        // WSL_DISTRO_NAME means we're already inside WSL. A parseable custom kernel should be treated
        // as WSL2 instead of being downgraded to "unknown".
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment("Ubuntu-22.04"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    [InlineData(CustomKernelBanner)]
    [InlineData(CustomTerminalPlusKernelBanner)]
    public async Task CheckAsync_ReportsPass_ForCustomKernelWithoutMarkers_WhenWslDistroNameIsSet(string procVersion)
    {
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment("Ubuntu-22.04"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
    }
 
    [Theory]
    [InlineData(CustomWsl2LookingBanner)]
    [InlineData(CustomMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsPass_ForUnofficialKernelReleaseSuffixes_WhenWslInteropIsSet(string procVersion)
    {
        // WSL_INTEROP is injected into WSL shells even when WSL_DISTRO_NAME is absent. Once WSL is
        // established, a parseable custom release should still be treated as WSL2.
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment(null, "/run/WSL/interop"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    [InlineData(CustomPrefixWsl2LookingBanner)]
    [InlineData(CustomPrefixMicrosoftStandardLookingBanner)]
    public async Task CheckAsync_ReportsPass_ForUnofficialKernelReleasePrefixes_WhenWslInteropIsSet(string procVersion)
    {
        // WSL_INTEROP is injected into WSL shells even when WSL_DISTRO_NAME is absent. Once WSL is
        // established, a parseable custom release should still be treated as WSL2.
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment(null, "/run/WSL/interop"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    [InlineData(CustomKernelBanner)]
    [InlineData(CustomTerminalPlusKernelBanner)]
    public async Task CheckAsync_ReportsPass_ForCustomKernelWithoutMarkers_WhenWslInteropIsSet(string procVersion)
    {
        var check = new WslEnvironmentCheck(
            CreateLinuxEnvironment(null, "/run/WSL/interop"),
            () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
    }
 
    [Theory]
    [MemberData(nameof(WslUnreadableBannerSignalCases))]
    public async Task CheckAsync_ReportsWarning_WhenBannerIsMissingOrUnreadableAndWslIsDetected(
        string? distroName,
        string? wslInterop,
        string? procVersion)
    {
        // WSL_DISTRO_NAME and WSL_INTEROP both mean the shell is already inside WSL, so an unreadable
        // banner must warn instead of looking like the machine is not WSL at all.
        var check = new WslEnvironmentCheck(CreateLinuxEnvironment(distroName, wslInterop), () => procVersion);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Warning, result.Status);
        Assert.Equal("WSL detected but the version could not be determined", result.Message);
        Assert.Equal(WslVersion.Unknown, WslEnvironmentCheck.DetermineWslVersion(procVersion));
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("Ubuntu-22.04")]
    public async Task CheckAsync_ReportsPass_ForLegacyMicrosoftWsl2StandardKernel(string? distroName)
    {
        var check = new WslEnvironmentCheck(CreateLinuxEnvironment(distroName), () => LegacyWsl2Banner);
 
        var result = Assert.Single(await check.CheckAsync(TestContext.Current.CancellationToken));
 
        Assert.Equal(EnvironmentCheckStatus.Pass, result.Status);
        Assert.Equal("WSL2 environment detected", result.Message);
    }
 
    private static TestEnvironment CreateLinuxEnvironment(string? distroName, string? wslInterop = null)
    {
        if (distroName is null && wslInterop is null)
        {
            return TestEnvironment.CreateLinux();
        }
 
        var environmentVariables = new Dictionary<string, string?>();
 
        if (distroName is not null)
        {
            environmentVariables["WSL_DISTRO_NAME"] = distroName;
        }
 
        if (wslInterop is not null)
        {
            environmentVariables["WSL_INTEROP"] = wslInterop;
        }
 
        return TestEnvironment.CreateLinux(environmentVariables);
    }
}