File: ResourceGeneratorMetadataTests.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 System.Xml.Linq;
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
public sealed class ResourceGeneratorMetadataTests(ITestOutputHelper output)
{
    [Theory]
    [InlineData("Aspire.Cli", "Resources", "ResXFileCodeGenerator")]
    [InlineData("Aspire.Dashboard", "Resources", "PublicResXFileCodeGenerator")]
    [InlineData("Aspire.Hosting", "Resources", "ResXFileCodeGenerator")]
    [InlineData("Aspire.Hosting.Azure", "Resources", "ResXFileCodeGenerator")]
    [InlineData("Aspire.Hosting.Browsers", "Resources", "ResXFileCodeGenerator")]
    [InlineData("Aspire.Hosting.DevTunnels", "Resources", "ResXFileCodeGenerator")]
    [InlineData("Aspire.Hosting.Testing", "Properties", "ResXFileCodeGenerator")]
    [RequiresTools(["pwsh"])]
    public async Task ResourceMetadataCoversExistingAndNewFiles(string projectName, string resourceDirectory, string generator)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        var projectDirectory = Path.Combine(RepoRoot.Path, "src", projectName);
        var source = XDocument.Load(Path.Combine(RepoRoot.Path, "Directory.Build.targets"));
        var configuration = XDocument.Load(Path.Combine(projectDirectory, $"{projectName}.csproj"));
        var neutralResources = Directory.GetFiles(Path.Combine(projectDirectory, resourceDirectory), "*.resx")
            .Select(Path.GetFileName)
            .Append("NewResource.resx")
            .Append("Errors.Validation.resx")
            .Select(name => Path.Combine(resourceDirectory, name!))
            .Order(StringComparer.Ordinal)
            .ToArray();
        var otherResources = new[]
        {
            Path.Combine(resourceDirectory, "NewResource.fr.resx"),
            Path.Combine(resourceDirectory, "NewResource.zh-Hans.resx"),
            Path.Combine(resourceDirectory, "WithoutDesigner.resx"),
            Path.Combine("Other", "Unrelated.resx"),
            Path.Combine(resourceDirectory, "xlf", "Unrelated.resx"),
            Path.Combine("..", "Shared", "Linked.resx")
        };
        var designers = neutralResources.Select(path => Path.ChangeExtension(path, ".Designer.cs")).ToArray();
        var settingsDesignerPath = Path.Combine(resourceDirectory, "Settings.Designer.cs");
        var otherSources = new[]
        {
            Path.Combine(resourceDirectory, "Unrelated.cs"),
            Path.Combine(resourceDirectory, "Unrelated.Designer.cs"),
            settingsDesignerPath,
            Path.Combine("Other", "Unrelated.Designer.cs"),
            Path.Combine(resourceDirectory, "Nested", "Unrelated.Designer.cs"),
            Path.Combine("..", "Shared", "Linked.Designer.cs")
        };
 
        // Pair detection needs real sibling files, but only in the isolated workspace.
        Directory.CreateDirectory(Path.Combine(workspace.Path, resourceDirectory));
        foreach (var path in neutralResources.Concat(designers).Append(Path.Combine(resourceDirectory, "WithoutDesigner.resx"))
            .Append(settingsDesignerPath).Append(Path.Combine(resourceDirectory, "Settings.settings")))
        {
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, path), string.Empty);
        }
 
        // Evaluate the real metadata rules against explicit items, including files that
        // are not in the project yet, without modifying the shared repository.
        var project = new XDocument(new XElement("Project",
            new XElement("PropertyGroup", configuration.Root!.Elements("PropertyGroup").Elements("ResxCodeGenerator").Select(property => new XElement(property))),
            new XElement("PropertyGroup", source.Root!.Elements("PropertyGroup").Elements("ResxCodeGenerator").Select(property => new XElement(property))),
            new XElement("ItemGroup",
                neutralResources.Concat(otherResources).Select(path => new XElement("EmbeddedResource", new XAttribute("Include", path))),
                designers.Concat(otherSources).Select(path => new XElement("Compile", new XAttribute("Include", path)))),
            new XElement("ItemGroup", source.Root.Elements("ItemGroup").Elements()
                .Where(item => item.Element("Generator") is not null || item.Element("AutoGen") is not null)
                .Select(item => new XElement(item)))));
        var settingsDesigner = project.Root!.Elements("ItemGroup").Elements("Compile")
            .Single(item => item.Attribute("Include")?.Value == settingsDesignerPath);
        settingsDesigner.Add(new XElement("DependentUpon", "Settings.settings"));
        settingsDesigner.Add(new XElement("DesignTime", "True"));
        settingsDesigner.Add(new XElement("AutoGen", "True"));
        var linkedResource = project.Root!.Elements("ItemGroup").Elements("EmbeddedResource")
            .Single(item => item.Attribute("Include")?.Value == otherResources[^1]);
        linkedResource.Add(new XElement("Link", Path.Combine(resourceDirectory, "Linked.resx")));
        linkedResource.Add(new XElement("LogicalName", "Shared.Linked.resources"));
        var projectPath = Path.Combine(workspace.Path, "Resources.proj");
        project.Save(projectPath);
        var script = Path.Combine(workspace.Path, "evaluate.ps1");
        await File.WriteAllTextAsync(script, """
            & $env:TEST_DOTNET msbuild $env:TEST_PROJECT -nologo -getItem:EmbeddedResource,Compile
            exit $LASTEXITCODE
            """);
        using var command = new PowerShellCommand(script, output)
            .WithWorkingDirectory(workspace.Path)
            .WithTimeout(TimeSpan.FromMinutes(2))
            .WithEnvironmentVariable("TEST_DOTNET", Path.Combine(RepoRoot.Path, ".dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"))
            .WithEnvironmentVariable("TEST_PROJECT", projectPath)
            .WithEnvironmentVariable("MSBUILDTERMINALLOGGER", "false");
        var result = await command.ExecuteAsync();
        result.EnsureSuccessful();
 
        using var document = JsonDocument.Parse(result.Output);
        var items = document.RootElement.GetProperty("Items");
        var resources = items.GetProperty("EmbeddedResource").EnumerateArray().ToArray();
        Assert.Equal(neutralResources.Concat(otherResources), resources.Select(item => item.GetProperty("Identity").GetString()));
        foreach (var resource in resources.Take(neutralResources.Length))
        {
            var name = Path.GetFileNameWithoutExtension(resource.GetProperty("Identity").GetString()!);
            Assert.Equal(generator, resource.GetProperty("Generator").GetString());
            Assert.Equal($"{name}.Designer.cs", resource.GetProperty("LastGenOutput").GetString());
            Assert.Equal("Designer", resource.GetProperty("SubType").GetString());
            Assert.Equal("Resx", resource.GetProperty("XlfSourceFormat").GetString());
            Assert.Equal("EmbeddedResource", resource.GetProperty("XlfOutputItem").GetString());
        }
        foreach (var resource in resources.Skip(neutralResources.Length))
        {
            Assert.False(resource.TryGetProperty("Generator", out _));
            Assert.False(resource.TryGetProperty("LastGenOutput", out _));
            Assert.False(resource.TryGetProperty("SubType", out _));
        }
        Assert.Equal(Path.Combine(resourceDirectory, "Linked.resx"), resources[^1].GetProperty("Link").GetString());
        Assert.Equal("Shared.Linked.resources", resources[^1].GetProperty("LogicalName").GetString());
 
        var compileItems = items.GetProperty("Compile").EnumerateArray().ToArray();
        Assert.Equal(designers.Concat(otherSources), compileItems.Select(item => item.GetProperty("Identity").GetString()));
        foreach (var designer in compileItems.Take(designers.Length))
        {
            var name = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(designer.GetProperty("Identity").GetString()!));
            Assert.Equal($"{name}.resx", designer.GetProperty("DependentUpon").GetString());
            Assert.Equal("True", designer.GetProperty("DesignTime").GetString());
            Assert.Equal("True", designer.GetProperty("AutoGen").GetString());
        }
        foreach (var sourceItem in compileItems.Skip(designers.Length))
        {
            if (sourceItem.GetProperty("Identity").GetString() == settingsDesignerPath)
            {
                Assert.Equal("Settings.settings", sourceItem.GetProperty("DependentUpon").GetString());
                Assert.Equal("True", sourceItem.GetProperty("DesignTime").GetString());
                Assert.Equal("True", sourceItem.GetProperty("AutoGen").GetString());
            }
            else
            {
                Assert.False(sourceItem.TryGetProperty("DependentUpon", out _));
                Assert.False(sourceItem.TryGetProperty("DesignTime", out _));
                Assert.False(sourceItem.TryGetProperty("AutoGen", out _));
            }
        }
    }
}