File: ExtractTestPartitions\MockAssemblyBuilder.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;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
 
namespace Infrastructure.Tests;
 
/// <summary>
/// Builds mock test assemblies dynamically using Roslyn for testing ExtractTestPartitions.
/// </summary>
public static class MockAssemblyBuilder
{
    /// <summary>
    /// Creates an assembly with test classes having [Trait("Partition", "name")] attributes.
    /// </summary>
    public static string CreateAssemblyWithPartitions(
        string outputPath,
        params (string ClassName, string PartitionName)[] partitions)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]");
        code.AppendLine("public class TraitAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public TraitAttribute(string name, string value) { Name = name; Value = value; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("    public string Value { get; }");
        code.AppendLine("}");
        code.AppendLine();
 
        foreach (var (className, partitionName) in partitions)
        {
            code.AppendLine($"[Trait(\"Partition\", \"{partitionName}\")]");
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    /// <summary>
    /// Creates an assembly with test classes having [Collection("name")] attributes.
    /// </summary>
    public static string CreateAssemblyWithCollections(
        string outputPath,
        params (string ClassName, string CollectionName)[] collections)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class)]");
        code.AppendLine("public class CollectionAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public CollectionAttribute(string name) { Name = name; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("}");
        code.AppendLine();
 
        foreach (var (className, collectionName) in collections)
        {
            code.AppendLine($"[Collection(\"{collectionName}\")]");
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    /// <summary>
    /// Creates an assembly with test classes having both [Trait] and [Collection] attributes.
    /// </summary>
    public static string CreateAssemblyWithMixedAttributes(
        string outputPath,
        (string ClassName, string PartitionName)[] partitions,
        (string ClassName, string CollectionName)[] collections)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]");
        code.AppendLine("public class TraitAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public TraitAttribute(string name, string value) { Name = name; Value = value; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("    public string Value { get; }");
        code.AppendLine("}");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class)]");
        code.AppendLine("public class CollectionAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public CollectionAttribute(string name) { Name = name; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("}");
        code.AppendLine();
 
        foreach (var (className, partitionName) in partitions)
        {
            code.AppendLine($"[Trait(\"Partition\", \"{partitionName}\")]");
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        foreach (var (className, collectionName) in collections)
        {
            code.AppendLine($"[Collection(\"{collectionName}\")]");
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    /// <summary>
    /// Creates an assembly with test classes having no partition/collection attributes.
    /// </summary>
    public static string CreateAssemblyWithNoAttributes(string outputPath, params string[] classNames)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
 
        var names = classNames.Length > 0 ? classNames : new[] { "TestClass1", "TestClass2" };
 
        foreach (var className in names)
        {
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    /// <summary>
    /// Creates an assembly with classes having duplicate partition names (different casing).
    /// </summary>
    public static string CreateAssemblyWithDuplicatePartitions(
        string outputPath,
        params (string ClassName, string PartitionName)[] partitions)
    {
        // Same as CreateAssemblyWithPartitions - the deduplication is in ExtractTestPartitions
        return CreateAssemblyWithPartitions(outputPath, partitions);
    }
 
    /// <summary>
    /// Creates an assembly with a nested class having a partition attribute.
    /// </summary>
    public static string CreateAssemblyWithNestedTypePartitions(
        string outputPath,
        params (string OuterClassName, string InnerClassName, string PartitionName)[] nestedPartitions)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]");
        code.AppendLine("public class TraitAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public TraitAttribute(string name, string value) { Name = name; Value = value; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("    public string Value { get; }");
        code.AppendLine("}");
        code.AppendLine();
 
        foreach (var (outerClassName, innerClassName, partitionName) in nestedPartitions)
        {
            code.AppendLine($"public class {outerClassName}");
            code.AppendLine("{");
            code.AppendLine($"    [Trait(\"Partition\", \"{partitionName}\")]");
            code.AppendLine($"    public class {innerClassName}");
            code.AppendLine("    {");
            code.AppendLine("        public void TestMethod() { }");
            code.AppendLine("    }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    /// <summary>
    /// Creates an assembly with Trait attributes of various keys (not just "Partition").
    /// </summary>
    public static string CreateAssemblyWithNonPartitionTraits(
        string outputPath,
        params (string ClassName, string TraitKey, string TraitValue)[] traits)
    {
        var code = new StringBuilder();
        code.AppendLine("using System;");
        code.AppendLine();
        code.AppendLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]");
        code.AppendLine("public class TraitAttribute : Attribute");
        code.AppendLine("{");
        code.AppendLine("    public TraitAttribute(string name, string value) { Name = name; Value = value; }");
        code.AppendLine("    public string Name { get; }");
        code.AppendLine("    public string Value { get; }");
        code.AppendLine("}");
        code.AppendLine();
 
        foreach (var (className, traitKey, traitValue) in traits)
        {
            code.AppendLine($"[Trait(\"{traitKey}\", \"{traitValue}\")]");
            code.AppendLine($"public class {className}");
            code.AppendLine("{");
            code.AppendLine("    public void TestMethod() { }");
            code.AppendLine("}");
            code.AppendLine();
        }
 
        return CompileAssembly(outputPath, code.ToString());
    }
 
    private static string CompileAssembly(string outputPath, string code)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(code);
 
        var references = new List<MetadataReference>
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location)
        };
 
        // Add netstandard reference for compilation
        var netstandardPath = Path.Combine(
            Path.GetDirectoryName(typeof(object).Assembly.Location)!,
            "netstandard.dll");
        if (File.Exists(netstandardPath))
        {
            references.Add(MetadataReference.CreateFromFile(netstandardPath));
        }
 
        // Add System.Runtime reference
        var runtimePath = Path.Combine(
            Path.GetDirectoryName(typeof(object).Assembly.Location)!,
            "System.Runtime.dll");
        if (File.Exists(runtimePath))
        {
            references.Add(MetadataReference.CreateFromFile(runtimePath));
        }
 
        var compilation = CSharpCompilation.Create(
            Path.GetFileNameWithoutExtension(outputPath),
            [syntaxTree],
            references,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 
        var outputDir = Path.GetDirectoryName(outputPath);
        if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
        {
            Directory.CreateDirectory(outputDir);
        }
 
        EmitResult result = compilation.Emit(outputPath);
 
        if (!result.Success)
        {
            var errors = string.Join(Environment.NewLine,
                result.Diagnostics
                    .Where(d => d.Severity == DiagnosticSeverity.Error)
                    .Select(d => d.ToString()));
            throw new InvalidOperationException($"Failed to compile mock assembly:{Environment.NewLine}{errors}");
        }
 
        return outputPath;
    }
}