File: tests\Aspire.TestUtilities\RequiresFeatureAttribute.cs
Web Access
Project: src\tests\Aspire.Playground.Tests\Aspire.Playground.Tests.csproj (Aspire.Playground.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Utils;
using Aspire.Shared;
using Microsoft.DotNet.XUnitExtensions;
 
namespace Aspire.TestUtilities;
 
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequiresFeatureAttribute(TestFeature feature) : Attribute, ITraitAttribute
{
    private static bool? s_isPlaywrightSupported;
    private static readonly ConcurrentDictionary<string, bool> s_executablesOnPath = new();
 
    public TestFeature Feature { get; } = feature;
 
    public IReadOnlyCollection<KeyValuePair<string, string>> GetTraits()
    {
        if (!IsFeatureSupported(Feature))
        {
            return [new KeyValuePair<string, string>(XunitConstants.Category, "failing")];
        }
 
        return [];
    }
 
    // Logic from RequiresSSLCertificateAttribute
    // Always supported on linux (local and CI), but only local otherwise
    private static bool IsSslCertificateSupported()
    {
        return OperatingSystem.IsLinux() || !PlatformDetection.IsRunningOnCI;
    }
 
    // Logic from RequiresPlaywrightAttribute
    // This property is `true` when Playwright is expected to be installed on the machine.
    //
    // A hard-coded *expected* value is used here to ensure that CI can skip entire
    // jobs (one per test class) when Playwright is not available.
    //
    // Currently this is not supported on Linux agents on helix, and azdo build machines
    // https://github.com/microsoft/aspire/issues/4921
    private static bool IsPlaywrightSupported()
    {
        s_isPlaywrightSupported ??= GetIsPlaywrightSupported();
        return s_isPlaywrightSupported.Value;
    }
 
    private static bool GetIsPlaywrightSupported()
    {
        // Setting PLAYWRIGHT_INSTALLED environment variable takes precedence
        if (Environment.GetEnvironmentVariable("PLAYWRIGHT_INSTALLED") is var playwrightInstalled && !string.IsNullOrEmpty(playwrightInstalled))
        {
            if (bool.TryParse(playwrightInstalled, out var isInstalled))
            {
                return isInstalled;
            }
        }
 
        return !PlatformDetection.IsRunningOnCI // Supported on local runs
            || !OperatingSystem.IsLinux() // always supported on !linux on CI
            || PlatformDetection.IsRunningOnGithubActions;
    }
 
    // Logic from RequiresDevCertAttribute
    // Returns true if a valid ASP.NET Core development certificate is found in the current user's certificate store.
    private static bool IsDevCertSupported()
    {
        return DevCertInStore();
    }
 
    private static bool DevCertInStore()
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);
        return store.Certificates
            .Where(c => c.IsAspNetCoreDevelopmentCertificate())
            .Where(c => c.NotAfter > DateTime.UtcNow)
            .OrderByDescending(c => c.GetCertificateVersion())
            .ThenByDescending(c => c.NotAfter)
            .Any();
    }
 
    // Logic from RequiresDockerAttribute, generalized to any container runtime.
    //
    // On CI a hard-coded *expected* value is used so that container-dependent tests *fail* if the runtime
    // is not available/usable in an environment where it is expected to be available. A run-time check
    // would let those tests skip silently, which is not desirable.
    //
    // scenarios:
    // - Windows: assume installed only for *local* runs as containers aren't supported on CI yet
    //                - https://github.com/microsoft/aspire/issues/4291
    // - Linux - Local, or CI: always assume that a container runtime is installed
    //
    // Locally there is no such expectation, so probe PATH instead: a developer machine may have Podman
    // only, Docker only, or neither, and tests should skip rather than fail against a runtime that is
    // not installed. Only PATH is inspected (no process is spawned) because trait attributes are
    // evaluated during discovery, once per test.
    internal static bool IsContainerRuntimeSupported()
    {
        if (PlatformDetection.IsRunningOnCI)
        {
            return OperatingSystem.IsLinux(); // non-linux on CI does not support containers
        }
 
        return IsOnPath(KnownContainerRuntimes.Docker) || IsOnPath(KnownContainerRuntimes.Podman);
    }
 
    // Same CI expectation as IsContainerRuntimeSupported, but locally requires Docker itself. Used by
    // tests that depend on Docker rather than on containers in general (e.g. the Docker socket).
    private static bool IsDockerSupported()
    {
        if (PlatformDetection.IsRunningOnCI)
        {
            return OperatingSystem.IsLinux();
        }
 
        return IsOnPath(KnownContainerRuntimes.Docker);
    }
 
    // Testcontainers needs a Docker-compatible API *endpoint*, not just a CLI on PATH: it speaks HTTP to
    // the engine rather than shelling out. A Podman-only host does not always have one — Testcontainers
    // 4.8.1 cannot use Podman's Windows named pipe, and on Linux Podman is daemonless unless
    // `podman system service` is running — so gating these fixtures on IsContainerRuntimeSupported() alone
    // would run them straight into DockerUnavailableException instead of skipping them.
    private static bool IsTestcontainersSupported()
    {
        if (!IsContainerRuntimeSupported())
        {
            return false;
        }
 
        // Configuration can set process-wide environment variables, so keep it scoped to tests that
        // actually use Testcontainers rather than generic DCP-backed container tests.
        TestcontainersPodmanConfiguration.EnsureConfigured();
 
        if (PlatformDetection.IsRunningOnCI)
        {
            return true; // CI always has Docker where it has a container runtime at all.
        }
 
        // A Docker install always exposes the socket or named pipe Testcontainers probes by default, so
        // the CLI being on PATH is enough. Podman only works if we managed to find an API socket for it.
        return IsOnPath(KnownContainerRuntimes.Docker) || TestcontainersPodmanConfiguration.HasUsableEndpoint;
    }
 
    // Building an image from a Dockerfile needs the buildx plugin under Docker, which is not available on
    // Azure DevOps build machines or Helix. See: https://github.com/dotnet/dnceng/issues/6232
    // Podman has no such dependency — `podman build` is built into the CLI — so a Podman-only machine
    // satisfies this feature.
    private static bool IsContainerImageBuildSupported()
    {
        if (PlatformDetection.IsRunningOnCI)
        {
            // Deliberately does NOT also require IsContainerRuntimeSupported(): this feature only describes
            // image-build capability, and callers that additionally need a live runtime already combine it
            // with TestFeature.ContainerRuntime. Adding that conjunct here would stop tests which build
            // images purely against fakes from running on GitHub Actions Windows.
            return !PlatformDetection.IsRunningFromAzdo;
        }
 
        // buildx ships with modern Docker installations, so presence of either CLI is enough locally.
        return IsOnPath(KnownContainerRuntimes.Docker) || IsOnPath(KnownContainerRuntimes.Podman);
    }
 
    private static bool IsOnPath(string executable)
    {
        // Cached because trait attributes are constructed for every test in the assembly.
        return s_executablesOnPath.GetOrAdd(executable, static e => FileUtil.FindFullPathFromPath(e) is not null);
    }
 
    /// <summary>
    /// Helper method to check if a specific feature is supported. Used for programmatic checks in test code.
    /// </summary>
    public static bool IsFeatureSupported(TestFeature feature)
    {
        // Check if ALL specified features are supported
        if ((feature & TestFeature.SSLCertificate) == TestFeature.SSLCertificate && !IsSslCertificateSupported())
        {
            return false;
        }
        if ((feature & TestFeature.Playwright) == TestFeature.Playwright && !IsPlaywrightSupported())
        {
            return false;
        }
        if ((feature & TestFeature.DevCert) == TestFeature.DevCert && !IsDevCertSupported())
        {
            return false;
        }
        if ((feature & TestFeature.ContainerRuntime) == TestFeature.ContainerRuntime && !IsContainerRuntimeSupported())
        {
            return false;
        }
        if ((feature & TestFeature.ContainerImageBuild) == TestFeature.ContainerImageBuild && !IsContainerImageBuildSupported())
        {
            return false;
        }
        if ((feature & TestFeature.Docker) == TestFeature.Docker && !IsDockerSupported())
        {
            return false;
        }
        if ((feature & TestFeature.Testcontainers) == TestFeature.Testcontainers && !IsTestcontainersSupported())
        {
            return false;
        }
        return true;
    }
}