File: ExtractTestPartitions\ExtractTestPartitionsTests.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.Diagnostics;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>
/// Tests for the ExtractTestPartitions tool.
/// </summary>
public class ExtractTestPartitionsTests : IClassFixture<ExtractTestPartitionsFixture>, IDisposable
{
    private readonly TestTempDirectory _tempDir = new();
    private readonly ExtractTestPartitionsFixture _fixture;
    private readonly ITestOutputHelper _output;
 
    public ExtractTestPartitionsTests(ExtractTestPartitionsFixture fixture, ITestOutputHelper output)
    {
        _fixture = fixture;
        _output = output;
    }
 
    public void Dispose() => _tempDir.Dispose();
 
    [Fact]
    public async Task ExtractsPartitionTraits()
    {
        // Arrange
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithPartitions(
            assemblyPath,
            ("TestClass1", "PartitionA"),
            ("TestClass2", "PartitionB"));
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created");
 
        var partitions = File.ReadAllLines(outputFile);
        Assert.Equal(2, partitions.Length);
        Assert.Contains("PartitionA", partitions);
        Assert.Contains("PartitionB", partitions);
    }
 
    [Fact]
    public async Task IgnoresCollectionAttributes()
    {
        // Arrange - Collection attributes are for shared fixtures, not CI splitting
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithCollections(
            assemblyPath,
            ("TestClass1", "CollectionX"),
            ("TestClass2", "CollectionY"));
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.False(File.Exists(outputFile), "Output file should NOT be created for Collection-only attributes");
        Assert.Contains("No partitions found", result.Output);
    }
 
    [Fact]
    public async Task ExtractsOnlyTraitPartitionsFromMixedAttributes()
    {
        // Arrange
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithMixedAttributes(
            assemblyPath,
            partitions: [("PartitionTest1", "PartA")],
            collections: [("CollectionTest1", "CollB")]);
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert - Only Trait("Partition") entries are extracted, not Collection attributes
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created");
 
        var partitions = File.ReadAllLines(outputFile);
        Assert.Single(partitions);
        Assert.Contains("PartA", partitions);
    }
 
    [Fact]
    public async Task ReturnsEmptyForNoAttributes()
    {
        // Arrange
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithNoAttributes(assemblyPath);
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.False(File.Exists(outputFile), "Output file should NOT be created when no partitions found");
        Assert.Contains("No partitions found", result.Output);
    }
 
    [Fact]
    public async Task SortsPartitionsAlphabetically()
    {
        // Arrange
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithPartitions(
            assemblyPath,
            ("TestZ", "Zebra"),
            ("TestA", "Apple"),
            ("TestM", "Mango"));
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        var partitions = File.ReadAllLines(outputFile);
        Assert.Equal(3, partitions.Length);
        Assert.Equal("Apple", partitions[0]);
        Assert.Equal("Mango", partitions[1]);
        Assert.Equal("Zebra", partitions[2]);
    }
 
    [Fact]
    public async Task DeduplicatesPartitions()
    {
        // Arrange - same partition name with different casing
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithPartitions(
            assemblyPath,
            ("TestClass1", "PartitionA"),
            ("TestClass2", "partitiona"), // lowercase variant
            ("TestClass3", "PARTITIONA")); // uppercase variant
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        var partitions = File.ReadAllLines(outputFile);
        // Should be deduplicated case-insensitively - only one entry
        Assert.Single(partitions);
    }
 
    [Fact]
    public async Task HandlesInvalidAssemblyPath()
    {
        // Arrange
        var nonExistentPath = Path.Combine(_tempDir.Path, "DoesNotExist.dll");
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(nonExistentPath, outputFile);
 
        // Assert
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("not found", result.Output, StringComparison.OrdinalIgnoreCase);
    }
 
    [Fact]
    public async Task HandlesInvalidArguments()
    {
        // Act - run with no arguments
        var result = await RunToolRaw();
 
        // Assert
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Usage:", result.Output);
    }
 
    [Fact]
    public async Task CreatesOutputDirectory()
    {
        // Arrange
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithPartitions(
            assemblyPath,
            ("TestClass1", "PartitionA"));
 
        // Output in nested directory that doesn't exist
        var outputFile = Path.Combine(_tempDir.Path, "nested", "dir", "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created in nested directory");
    }
 
    [Fact]
    public async Task ExtractsPartitionsFromNestedTypes()
    {
        // Arrange - Test classes can be nested (Outer+Inner pattern)
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithNestedTypePartitions(
            assemblyPath,
            ("OuterClass", "InnerClass", "NestedPartition"));
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created");
 
        var partitions = File.ReadAllLines(outputFile);
        Assert.Contains("NestedPartition", partitions);
    }
 
    [Fact]
    public async Task IgnoresEmptyPartitionNames()
    {
        // Arrange - Empty/whitespace partition names should be ignored
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithPartitions(
            assemblyPath,
            ("TestClass1", "ValidPartition"),
            ("TestClass2", ""), // Empty name
            ("TestClass3", "   ")); // Whitespace-only name
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created");
 
        var partitions = File.ReadAllLines(outputFile);
        Assert.Single(partitions);
        Assert.Equal("ValidPartition", partitions[0]);
    }
 
    [Fact]
    public async Task IgnoresNonPartitionTraits()
    {
        // Arrange - Only Traits with key "Partition" should be extracted
        var assemblyPath = Path.Combine(_tempDir.Path, "TestAssembly.dll");
        MockAssemblyBuilder.CreateAssemblyWithNonPartitionTraits(
            assemblyPath,
            ("TestClass1", "Partition", "ShouldInclude"),
            ("TestClass2", "Category", "ShouldIgnore"),
            ("TestClass3", "OtherKey", "AlsoIgnore"));
 
        var outputFile = Path.Combine(_tempDir.Path, "partitions.txt");
 
        // Act
        var result = await RunTool(assemblyPath, outputFile);
 
        // Assert
        Assert.Equal(0, result.ExitCode);
        Assert.True(File.Exists(outputFile), "Output file should be created");
 
        var partitions = File.ReadAllLines(outputFile);
        Assert.Single(partitions);
        Assert.Equal("ShouldInclude", partitions[0]);
    }
 
    private async Task<ToolResult> RunTool(string assemblyPath, string outputFile)
    {
        return await RunToolRaw("--assembly-path", assemblyPath, "--output-file", outputFile);
    }
 
    private async Task<ToolResult> RunToolRaw(params string[] args)
    {
        // Use 'dotnet run --no-build' since the fixture already built the tool
        var psi = new ProcessStartInfo
        {
            FileName = "dotnet",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };
 
        psi.ArgumentList.Add("run");
        psi.ArgumentList.Add("--no-build");
        psi.ArgumentList.Add("--project");
        psi.ArgumentList.Add(_fixture.ToolProjectPath);
 
        if (args.Length > 0)
        {
            psi.ArgumentList.Add("--");
            foreach (var arg in args)
            {
                psi.ArgumentList.Add(arg);
            }
        }
 
        _output.WriteLine($"Running: {psi.FileName} {string.Join(" ", psi.ArgumentList)}");
 
        using var process = new Process { StartInfo = psi };
        var outputLines = new List<string>();
 
        process.OutputDataReceived += (_, e) =>
        {
            if (e.Data is not null)
            {
                outputLines.Add(e.Data);
                _output.WriteLine($"[stdout] {e.Data}");
            }
        };
 
        process.ErrorDataReceived += (_, e) =>
        {
            if (e.Data is not null)
            {
                outputLines.Add(e.Data);
                _output.WriteLine($"[stderr] {e.Data}");
            }
        };
 
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
        await process.WaitForExitAsync(cts.Token);
 
        return new ToolResult(process.ExitCode, string.Join(Environment.NewLine, outputLines));
    }
 
    private sealed record ToolResult(int ExitCode, string Output);
}