File: PowerShellScripts\BuildTestMatrixTests.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 System.Text.Json;
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>
/// Tests for eng/scripts/build-test-matrix.ps1
/// </summary>
public class BuildTestMatrixTests : IDisposable
{
    private readonly TestTempDirectory _tempDir = new();
    private readonly string _scriptPath;
    private readonly ITestOutputHelper _output;
 
    public BuildTestMatrixTests(ITestOutputHelper output)
    {
        _output = output;
        _scriptPath = Path.Combine(FindRepoRoot(), "eng", "scripts", "build-test-matrix.ps1");
    }
 
    public void Dispose() => _tempDir.Dispose();
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task GeneratesMatrixFromSingleProject()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "MyProject.tests-metadata.json"),
            projectName: "MyProject",
            testProjectPath: "tests/MyProject/MyProject.csproj",
            shortName: "MyProj");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful("build-test-matrix.ps1 failed");
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var entry = Assert.Single(matrix.Tests);
        Assert.Equal("MyProject", entry.ProjectName);
        Assert.Equal("MyProj", entry.Name);
        Assert.Equal("regular", entry.Type);
        Assert.False(entry.RequiresNugets);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task GeneratesMatrixFromMultipleProjects()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "ProjectA.tests-metadata.json"),
            projectName: "ProjectA",
            testProjectPath: "tests/ProjectA/ProjectA.csproj");
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "ProjectB.tests-metadata.json"),
            projectName: "ProjectB",
            testProjectPath: "tests/ProjectB/ProjectB.csproj");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        Assert.Equal(2, matrix.Tests.Length);
        Assert.Contains(matrix.Tests, e => e.ProjectName == "ProjectA");
        Assert.Contains(matrix.Tests, e => e.ProjectName == "ProjectB");
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task GeneratesPartitionEntries()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateSplitTestsMetadataJson(
            Path.Combine(artifactsDir, "SplitProject.tests-metadata.json"),
            projectName: "SplitProject",
            testProjectPath: "tests/SplitProject/SplitProject.csproj",
            shortName: "Split");
 
        TestDataBuilder.CreateTestsPartitionsJson(
            Path.Combine(artifactsDir, "SplitProject.tests-partitions.json"),
            "PartitionA", "PartitionB");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        // Should have 3 entries: PartitionA, PartitionB, and uncollected
        Assert.Equal(3, matrix.Tests.Length);
 
        var partitionA = matrix.Tests.FirstOrDefault(e => e.Name == "Split-PartitionA");
        Assert.NotNull(partitionA);
        Assert.Equal("collection", partitionA.Type);
        Assert.Contains("--filter-trait", partitionA.ExtraTestArgs);
        Assert.Contains("Partition=PartitionA", partitionA.ExtraTestArgs);
 
        var uncollected = matrix.Tests.FirstOrDefault(e => e.Name == "Split");
        Assert.NotNull(uncollected);
        Assert.Contains("--filter-not-trait", uncollected.ExtraTestArgs);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task GeneratesClassEntries()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateSplitTestsMetadataJson(
            Path.Combine(artifactsDir, "ClassSplitProject.tests-metadata.json"),
            projectName: "ClassSplitProject",
            testProjectPath: "tests/ClassSplitProject/ClassSplitProject.csproj",
            shortName: "ClassSplit");
 
        TestDataBuilder.CreateClassBasedPartitionsJson(
            Path.Combine(artifactsDir, "ClassSplitProject.tests-partitions.json"),
            "MyNamespace.TestClassA", "MyNamespace.TestClassB");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        Assert.Equal(2, matrix.Tests.Length);
 
        var classA = matrix.Tests.FirstOrDefault(e => e.Name == "ClassSplit-TestClassA");
        Assert.NotNull(classA);
        Assert.Equal("class", classA.Type);
        Assert.Contains("--filter-class", classA.ExtraTestArgs);
        Assert.Contains("MyNamespace.TestClassA", classA.ExtraTestArgs);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task AppliesDefaultTimeouts()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        // Create metadata without explicit timeouts
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "NoTimeouts.tests-metadata.json"),
            projectName: "NoTimeouts",
            testProjectPath: "tests/NoTimeouts/NoTimeouts.csproj");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var entry = Assert.Single(matrix.Tests);
        Assert.Equal("20m", entry.TestSessionTimeout);
        Assert.Equal("10m", entry.TestHangTimeout);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task PreservesCustomTimeouts()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "CustomTimeouts.tests-metadata.json"),
            projectName: "CustomTimeouts",
            testProjectPath: "tests/CustomTimeouts/CustomTimeouts.csproj",
            testSessionTimeout: "45m",
            testHangTimeout: "15m");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var entry = Assert.Single(matrix.Tests);
        Assert.Equal("45m", entry.TestSessionTimeout);
        Assert.Equal("15m", entry.TestHangTimeout);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task PreservesRequiresNugetsProperty()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "NeedsNugets.tests-metadata.json"),
            projectName: "NeedsNugets",
            testProjectPath: "tests/NeedsNugets/NeedsNugets.csproj",
            requiresNugets: true);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "NoNugets.tests-metadata.json"),
            projectName: "NoNugets",
            testProjectPath: "tests/NoNugets/NoNugets.csproj",
            requiresNugets: false);
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        Assert.Equal(2, matrix.Tests.Length);
        Assert.Contains(matrix.Tests, e => e.ProjectName == "NeedsNugets" && e.RequiresNugets == true);
        Assert.Contains(matrix.Tests, e => e.ProjectName == "NoNugets" && e.RequiresNugets == false);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task GeneratesCorrectFilterArgs()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateSplitTestsMetadataJson(
            Path.Combine(artifactsDir, "FilterTest.tests-metadata.json"),
            projectName: "FilterTest",
            testProjectPath: "tests/FilterTest/FilterTest.csproj");
 
        TestDataBuilder.CreateTestsPartitionsJson(
            Path.Combine(artifactsDir, "FilterTest.tests-partitions.json"),
            "MyPartition");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var partitionEntry = matrix.Tests.First(e => e.Collection == "MyPartition");
        Assert.Equal("--filter-trait \"Partition=MyPartition\"", partitionEntry.ExtraTestArgs);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task CreatesEmptyMatrixWhenNoMetadataFiles()
    {
        // Arrange
        var emptyArtifactsDir = Path.Combine(_tempDir.Path, "empty-artifacts");
        Directory.CreateDirectory(emptyArtifactsDir);
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(emptyArtifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        Assert.Empty(matrix.Tests);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task UsesUncollectedTimeoutsForUncollectedEntry()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateSplitTestsMetadataJson(
            Path.Combine(artifactsDir, "SplitProject.tests-metadata.json"),
            projectName: "SplitProject",
            testProjectPath: "tests/SplitProject/SplitProject.csproj",
            shortName: "Split",
            testSessionTimeout: "30m",
            testHangTimeout: "15m",
            uncollectedTestsSessionTimeout: "45m",
            uncollectedTestsHangTimeout: "20m");
 
        TestDataBuilder.CreateTestsPartitionsJson(
            Path.Combine(artifactsDir, "SplitProject.tests-partitions.json"),
            "PartitionA");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
 
        // The partitioned entry should have regular timeouts
        var partitionEntry = matrix.Tests.FirstOrDefault(e => e.Name == "Split-PartitionA");
        Assert.NotNull(partitionEntry);
        Assert.Equal("30m", partitionEntry.TestSessionTimeout);
        Assert.Equal("15m", partitionEntry.TestHangTimeout);
 
        // The uncollected entry should have uncollected-specific timeouts
        var uncollectedEntry = matrix.Tests.FirstOrDefault(e => e.Name == "Split");
        Assert.NotNull(uncollectedEntry);
        Assert.Equal("45m", uncollectedEntry.TestSessionTimeout);
        Assert.Equal("20m", uncollectedEntry.TestHangTimeout);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task PassesRequiresTestSdkProperty()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "SdkProject.tests-metadata.json"),
            projectName: "SdkProject",
            testProjectPath: "tests/SdkProject/SdkProject.csproj",
            requiresTestSdk: true);
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var entry = Assert.Single(matrix.Tests);
        Assert.True(entry.RequiresTestSdk);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task PreservesSupportedOSes()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateTestsMetadataJson(
            Path.Combine(artifactsDir, "LinuxOnly.tests-metadata.json"),
            projectName: "LinuxOnly",
            testProjectPath: "tests/LinuxOnly/LinuxOnly.csproj",
            supportedOSes: ["linux"]);
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        var entry = Assert.Single(matrix.Tests);
        Assert.Single(entry.SupportedOSes);
        Assert.Contains("linux", entry.SupportedOSes);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task InheritsSupportedOSesToPartitionEntries()
    {
        // Arrange
        var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
        Directory.CreateDirectory(artifactsDir);
 
        TestDataBuilder.CreateSplitTestsMetadataJson(
            Path.Combine(artifactsDir, "OsRestrictedSplit.tests-metadata.json"),
            projectName: "OsRestrictedSplit",
            testProjectPath: "tests/OsRestrictedSplit/OsRestrictedSplit.csproj",
            shortName: "OsRestrict",
            supportedOSes: ["windows", "linux"]);
 
        TestDataBuilder.CreateTestsPartitionsJson(
            Path.Combine(artifactsDir, "OsRestrictedSplit.tests-partitions.json"),
            "PartA");
 
        var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
 
        // Act
        var result = await RunScript(artifactsDir, outputFile);
 
        // Assert
        result.EnsureSuccessful();
 
        var matrix = ParseCanonicalMatrix(outputFile);
        // Both the partition entry and uncollected entry should have the same supportedOSes
        foreach (var entry in matrix.Tests)
        {
            Assert.Equal(2, entry.SupportedOSes.Length);
            Assert.Contains("windows", entry.SupportedOSes);
            Assert.Contains("linux", entry.SupportedOSes);
        }
    }
 
    private async Task<CommandResult> RunScript(string artifactsDir, string outputFile)
    {
        using var cmd = new PowerShellCommand(_scriptPath, _output)
            .WithTimeout(TimeSpan.FromMinutes(2));
 
        return await cmd.ExecuteAsync(
            "-ArtifactsDir", $"\"{artifactsDir}\"",
            "-OutputMatrixFile", $"\"{outputFile}\"");
    }
 
    private static CanonicalMatrix ParseCanonicalMatrix(string path)
    {
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<CanonicalMatrix>(json)
            ?? throw new InvalidOperationException("Failed to parse matrix JSON");
    }
 
    private static string FindRepoRoot()
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "Aspire.slnx")))
            {
                return dir.FullName;
            }
            dir = dir.Parent;
        }
        throw new InvalidOperationException("Could not find repository root");
    }
}