File: TestTriggerMap\NativeHostingWorkflowTests.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.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.SelectTests;
using Xunit;
using YamlDotNet.RepresentationModel;
 
namespace Infrastructure.Tests.TestTriggerMap;
 
public sealed class NativeHostingWorkflowTests
{
    [Theory]
    [InlineData("src/Aspire.Hosting.Native.Server/Core/Runtime/RuntimeGeneration.cs")]
    [InlineData("src/Aspire.Hosting.Native.Core/Aspire.Hosting.Native.Core.csproj")]
    [InlineData("src/Aspire.Hosting.Native.Core/Runtime/RuntimeGeneration.cs")]
    [InlineData("src/Aspire.Hosting.Native.CodeGeneration/Program.cs")]
    [InlineData("src/Aspire.Hosting.Native.Client/NativeClient.cs")]
    [InlineData("src/Aspire.Hosting.Native.Client/Authoring/DistributedApplicationBuilder.cs")]
    [InlineData("src/Aspire.Hosting.Native.CodeGeneration/Resources/native-builder.mts")]
    [InlineData("src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts")]
    [InlineData("src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/package.json")]
    [InlineData("src/Aspire.Cli/Templating/Templates/ts-starter/tsconfig.apphost.json")]
    [InlineData("src/Aspire.Cli/Projects/NativeAppHostServerProject.cs")]
    [InlineData("src/Aspire.Managed/AppHostServerV2CodeGeneration.cs")]
    [InlineData("src/Aspire.TerminalHost/Aspire.TerminalHost.csproj")]
    [InlineData("src/Aspire.Dashboard/Components/Pages/Resources.razor")]
    [InlineData("src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto")]
    [InlineData("src/Shared/SocketPermissionHelper.cs")]
    [InlineData("src/Shared/DcpEnvironmentVariables.cs")]
    [InlineData("src/Aspire.Hosting.Native.Server/Transport/SocketConnectionListener.cs")]
    [InlineData("playground/NativeIntegrations/CSharp/Provider/NativeIntegrations.Provider.csproj")]
    [InlineData("playground/NativeIntegrations/CSharp/DockerCompose/DockerComposeEnvironment.cs")]
    [InlineData("playground/NativeIntegrations/CSharp/DockerComposeAppHost/Program.cs")]
    [InlineData("playground/NativeIntegrations/TypeScript/external-api.mts")]
    [InlineData("playground/NativeIntegrations/CSharp/Redis/RedisExtensions.cs")]
    [InlineData("playground/NativeIntegrations/CSharp/RedisProvider/NativeIntegrations.Redis.Provider.csproj")]
    [InlineData("playground/NativeIntegrations/CSharp/RedisValidation/NativeIntegrations.Redis.Validation.csproj")]
    [InlineData("playground/NativeIntegrations/TypeScript/Redis/redis.mts")]
    [InlineData("playground/NativeIntegrations/TypeScript/Redis/redis-protocol.mts")]
    [InlineData("playground/NativeIntegrations/TypeScript/Redis/redis-builder.test.mts")]
    [InlineData("playground/NativeIntegrations/TypeScript/DockerCompose/docker-compose.mts")]
    [InlineData("playground/NativeIntegrations/TypeScript/DockerCompose/AppHost/web/Dockerfile")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/Program.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/NativeRevisionSmoke.mts")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/NativeComposeSmoke.mts")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/NativeComposeComparison.mts")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/CliAppHost/apphost.mts")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/TestServices/DiagnosticCapture.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/NativeCSharpClientTests.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/NativeSymbolicWorkloadSocketTests.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/NativeIpcSecurityTests.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/DcpEnvironmentTests.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/SocketConnectionListenerTests.cs")]
    [InlineData("tests/Aspire.Hosting.Native.Core.Tests/AotSmoke/SymbolicRedisConsumer.mts")]
    [InlineData("tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/package.json")]
    [InlineData("tests/PolyglotAppHosts/Aspire.Hosting.Docker/TypeScript/apphost.mts")]
    [InlineData("tests/Directory.Build.props")]
    [InlineData("tests/Shared/RepoTesting/RepoTesting.csproj")]
    [InlineData("eng/dcppack/Common.projitems")]
    [InlineData("eng/scripts/test-native-hosting.sh")]
    [InlineData("eng/common/tools.sh")]
    [InlineData("Directory.Build.props")]
    [InlineData("Directory.Packages.props")]
    [InlineData(".editorconfig")]
    [InlineData(".config/dotnet-tools.json")]
    [InlineData("global.json")]
    [InlineData("NuGet.config")]
    [InlineData("Aspire.slnx")]
    [InlineData("restore.sh")]
    [InlineData("build.sh")]
    [InlineData(".github/workflows/native-hosting-validation.yml")]
    public void NativeHostingRuntimeAndBuildInputsTriggerIndependentLane(string path)
    {
        var events = Events();
 
        foreach (var eventName in new[] { "pull_request", "push" })
        {
            var paths = Paths(events, eventName);
            Assert.Contains(paths, pattern => TriggerMap.GlobMatches(pattern, path));
        }
    }
 
    [Theory]
    [InlineData("docs/ci/test-trigger-map.md")]
    [InlineData("extension/src/extension.ts")]
    [InlineData("tests/Infrastructure.Tests/TestTriggerMap/NativeHostingWorkflowTests.cs")]
    [InlineData("tests/Aspire.Hosting.Redis.Tests/RedisResourceTests.cs")]
    [InlineData("playground/NativeHosting/AtsServer/AtsSession.cs")]
    public void UnrelatedInputsDoNotTriggerNativeHostingLane(string path)
    {
        Assert.All(Paths(Events(), "pull_request"),
            pattern => Assert.False(TriggerMap.GlobMatches(pattern, path)));
    }
 
    [Fact]
    public void PushAndPullRequestFiltersStayAligned()
    {
        var events = Events();
 
        Assert.Equal(Paths(events, "pull_request"), Paths(events, "push"));
        Assert.True(events.Children.ContainsKey(new YamlScalarNode("workflow_dispatch")));
    }
 
    [Fact]
    public void IndependentLaneIsClassifiedWithoutInventingASelectorJob()
    {
        var map = TriggerMap.Load(Path.Combine(RepoRoot.Path, "eng", "github-ci", "test-trigger-map.yml"));
        const string workflow = ".github/workflows/native-hosting-validation.yml";
        const string harness = "eng/scripts/test-native-hosting.sh";
 
        Assert.Contains(workflow, map.Ignore);
        Assert.Contains("playground/NativeIntegrations/TypeScript/**", map.Ignore);
        foreach (var path in new[] { workflow, harness })
        {
            var targets = map.PathRules
                .Where(rule => rule.Paths.Any(pattern => TriggerMap.GlobMatches(pattern, path)))
                .SelectMany(rule => rule.Targets)
                .Distinct()
                .ToArray();
 
            Assert.Equal(["test:Infrastructure.Tests"], targets);
        }
    }
 
    [Fact]
    public void NativeHostingJobRunsHarnessAndUploadsEvidenceOnFailure()
    {
        var root = Workflow();
        var permissions = (YamlMappingNode)root.Children[new YamlScalarNode("permissions")];
        Assert.Equal("read", permissions.Children[new YamlScalarNode("contents")].ToString());
        var jobs = (YamlMappingNode)root.Children[new YamlScalarNode("jobs")];
        var job = Assert.IsType<YamlMappingNode>(jobs.Children[new YamlScalarNode("native_hosting")]);
        Assert.Equal("ubuntu-latest", job.Children[new YamlScalarNode("runs-on")].ToString());
        Assert.False(job.Children.ContainsKey(new YamlScalarNode("continue-on-error")));
        var steps = (YamlSequenceNode)job.Children[new YamlScalarNode("steps")];
        var nodeSetup = Assert.Single(steps.Cast<YamlMappingNode>(),
            step => step.Children.TryGetValue(new YamlScalarNode("uses"), out var uses)
                && uses.ToString().StartsWith("actions/setup-node@", StringComparison.Ordinal));
        var nodeInputs = (YamlMappingNode)nodeSetup.Children[new YamlScalarNode("with")];
        Assert.Equal("false", nodeInputs.Children[new YamlScalarNode("package-manager-cache")].ToString());
        Assert.False(nodeInputs.Children.ContainsKey(new YamlScalarNode("cache")));
        var harness = Assert.Single(steps.Cast<YamlMappingNode>(),
            step => step.Children.ContainsKey(new YamlScalarNode("run")));
        Assert.Equal("bash eng/scripts/test-native-hosting.sh linux-x64",
            harness.Children[new YamlScalarNode("run")].ToString());
        Assert.False(harness.Children.ContainsKey(new YamlScalarNode("if")));
        Assert.False(harness.Children.ContainsKey(new YamlScalarNode("continue-on-error")));
        var evidence = Assert.Single(steps.Cast<YamlMappingNode>(),
            step => step.Children.TryGetValue(new YamlScalarNode("uses"), out var uses)
                && uses.ToString().StartsWith("actions/upload-artifact@", StringComparison.Ordinal));
        Assert.Equal("always()", evidence.Children[new YamlScalarNode("if")].ToString());
    }
 
    [Fact]
    public void WindowsLaneExercisesThePublishedServerThroughTheRealCli()
    {
        var jobs = (YamlMappingNode)Workflow().Children[new YamlScalarNode("jobs")];
        var job = Assert.IsType<YamlMappingNode>(jobs.Children[new YamlScalarNode("windows_rpc")]);
        Assert.Equal("windows-latest", job.Children[new YamlScalarNode("runs-on")].ToString());
        Assert.False(job.Children.ContainsKey(new YamlScalarNode("continue-on-error")));
        var steps = (YamlSequenceNode)job.Children[new YamlScalarNode("steps")];
        var validation = Assert.Single(steps.Cast<YamlMappingNode>(),
            step => step.Children.ContainsKey(new YamlScalarNode("run")));
        var script = validation.Children[new YamlScalarNode("run")].ToString();
        Assert.Contains("ASPIRE_NATIVE_PRODUCT_CLI", script);
        Assert.Contains("ASPIRE_NATIVE_CSHARP_TEST_SERVER", script);
        Assert.Contains("*.NativeProductCliTests", script);
        Assert.Contains("*.NativeCliProtocolTests", script);
        Assert.Contains("*.NativeAuxiliaryBackchannelTests", script);
        Assert.Contains("*.NativeSocketIsolationTests", script);
        Assert.Contains("*.DcpEnvironmentTests", script);
        Assert.Contains("*.SocketConnectionListenerTests", script);
        Assert.False(validation.Children.ContainsKey(new YamlScalarNode("continue-on-error")));
    }
 
    private static string[] Paths(YamlMappingNode events, string eventName)
    {
        var trigger = (YamlMappingNode)events.Children[new YamlScalarNode(eventName)];
 
        return ((YamlSequenceNode)trigger.Children[new YamlScalarNode("paths")])
            .Select(path => path.ToString()).ToArray();
    }
 
    private static YamlMappingNode Events() =>
        (YamlMappingNode)Workflow().Children[new YamlScalarNode("on")];
 
    private static YamlMappingNode Workflow()
    {
        using var reader = File.OpenText(Path.Combine(RepoRoot.Path, ".github", "workflows", "native-hosting-validation.yml"));
        var yaml = new YamlStream();
        yaml.Load(reader);
 
        return (YamlMappingNode)yaml.Documents[0].RootNode;
    }
}