File: MSBuildTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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 System.Text;
 
namespace Aspire.Hosting.Tests;
 
public class MSBuildTests
{
    /// <summary>
    /// Tests that when an AppHost has a ProjectReference to a library project, a warning is emitted.
    /// </summary>
    [Fact]
    public void EnsureWarningsAreEmittedWhenProjectReferencingLibraries()
    {
        var repoRoot = MSBuildUtils.GetRepoRoot();
        using var tempDirectory = new TempDirectory();
 
        CreateLibraryProject(tempDirectory.Path, "Library");
 
        var appHostDirectory = Path.Combine(tempDirectory.Path, "AppHost");
        Directory.CreateDirectory(appHostDirectory);
 
        File.WriteAllText(Path.Combine(appHostDirectory, "AppHost.csproj"),
            $"""
            <Project Sdk="Microsoft.NET.Sdk">
 
              <PropertyGroup>
                <OutputType>Exe</OutputType>
                <TargetFramework>net8.0</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
                <IsAspireHost>true</IsAspireHost>
 
                <!--
                  Test applications have their own way of referencing Aspire.Hosting.AppHost, as well as DCP and Dashboard, so we disable
                  the Aspire.AppHost.SDK targets that will automatically add these references to projects.
                -->
                <SkipAddAspireDefaultReferences Condition="'$(TestsRunningOutsideOfRepo)' != 'true'">true</SkipAddAspireDefaultReferences>
                <AspireHostingSDKVersion>9.0.0</AspireHostingSDKVersion>
              </PropertyGroup>
 
              <ItemGroup>
                <ProjectReference Include="{repoRoot}\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" IsAspireProjectResource="false" />
 
                <ProjectReference Include="..\Library\Library.csproj" />
              </ItemGroup>
 
            </Project>
            """);
 
        File.WriteAllText(Path.Combine(appHostDirectory, "AppHost.cs"),
            """
            var builder = DistributedApplication.CreateBuilder();
            builder.Build().Run();
            """);
 
        CreateDirectoryBuildFiles(appHostDirectory, repoRoot);
 
        var output = BuildProject(appHostDirectory);
 
        // Ensure a warning is emitted when an AppHost references a Library project
        Assert.Contains("warning ASPIRE004", output);
    }
 
    /// <summary>
    /// Tests that the metadata sources are emitted correctly.
    /// </summary>
    [Fact]
    public async Task ValidateMetadataSources()
    {
        var repoRoot = MSBuildUtils.GetRepoRoot();
        using var tempDirectory = new TempDirectory();
 
        CreateAppProject(tempDirectory.Path, "App");
 
        var appHostDirectory = Path.Combine(tempDirectory.Path, "AppHost");
        Directory.CreateDirectory(appHostDirectory);
 
        File.WriteAllText(Path.Combine(appHostDirectory, "AppHost.csproj"),
            $"""
            <Project Sdk="Microsoft.NET.Sdk">
 
              <PropertyGroup>
                <OutputType>Exe</OutputType>
                <TargetFramework>net8.0</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
                <IsAspireHost>true</IsAspireHost>
 
                <!--
                  Test applications have their own way of referencing Aspire.Hosting.AppHost, as well as DCP and Dashboard, so we disable
                  the Aspire.AppHost.SDK targets that will automatically add these references to projects.
                -->
                <SkipAddAspireDefaultReferences Condition="'$(TestsRunningOutsideOfRepo)' != 'true'">true</SkipAddAspireDefaultReferences>
                <AspireHostingSDKVersion>9.0.0</AspireHostingSDKVersion>
              </PropertyGroup>
 
              <ItemGroup>
                <ProjectReference Include="{repoRoot}\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" IsAspireProjectResource="false" />
                <ProjectReference Include="..\App\App.csproj" />
              </ItemGroup>
 
            </Project>
            """);
 
        File.WriteAllText(Path.Combine(appHostDirectory, "AppHost.cs"),
            """
            var builder = DistributedApplication.CreateBuilder();
            builder.Build().Run();
            """);
 
        CreateDirectoryBuildFiles(appHostDirectory, repoRoot);
 
        var output = BuildProject(appHostDirectory);
 
        var metadataDirectory = Path.Combine(appHostDirectory, "obj", "Debug", "net8.0", "Aspire", "references");
        var appHostMetadata = await File.ReadAllTextAsync(Path.Combine(metadataDirectory, "_AppHost.ProjectMetadata.g.cs"));
        var appMetadata = await File.ReadAllTextAsync(Path.Combine(metadataDirectory, "App.ProjectMetadata.g.cs"));
 
        await Verify(new
        {
            AppHost = appHostMetadata,
            App = appMetadata
        }).ScrubLinesWithReplace(line =>
            {
                var temp = tempDirectory?.Path;
                if (temp is not null)
                {
                    line = line.Replace($"/private{temp}", "{AspirePath}") // Handle macOS temp symlinks
                               .Replace(temp, "{AspirePath}")
                               .Replace(Path.DirectorySeparatorChar, '/');
                }
                return line;
            });
    }
 
    private static void CreateDirectoryBuildFiles(string basePath, string repoRoot)
    {
        File.WriteAllText(Path.Combine(basePath, "Directory.Build.props"),
        $"""
        <Project>
          <PropertyGroup>
            <SkipAspireWorkloadManifest>true</SkipAspireWorkloadManifest>
          </PropertyGroup>
 
          <Import Project="{repoRoot}\src\Aspire.Hosting.AppHost\build\Aspire.Hosting.AppHost.props" />
        </Project>
        """);
        File.WriteAllText(Path.Combine(basePath, "Directory.Build.targets"),
        $"""
        <Project>
          <Import Project="{repoRoot}\src\Aspire.Hosting.AppHost\build\Aspire.Hosting.AppHost.in.targets" />
          <Import Project="{repoRoot}\src\Aspire.AppHost.Sdk\SDK\Sdk.in.targets" />
        </Project>
        """);
    }
 
    private static void CreateLibraryProject(string basePath, string name)
    {
        var libraryDirectory = Path.Combine(basePath,  name);
        Directory.CreateDirectory(libraryDirectory);
 
        File.WriteAllText(Path.Combine(libraryDirectory, $"{name}.csproj"),
            """
            <Project Sdk="Microsoft.NET.Sdk">
 
              <PropertyGroup>
                <TargetFramework>net8.0</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
              </PropertyGroup>
 
            </Project>
            """);
        File.WriteAllText(Path.Combine(libraryDirectory, "Class1.cs"),
            """
            namespace Library;
 
            public class Class1
            {
            }
            """);
    }
 
    private static void CreateAppProject(string basePath, string name)
    {
        var appDirectory = Path.Combine(basePath, name);
        Directory.CreateDirectory(appDirectory);
 
        File.WriteAllText(Path.Combine(appDirectory, $"{name}.csproj"),
            """
            <Project Sdk="Microsoft.NET.Sdk.Web">
              <PropertyGroup>
                <TargetFramework>net8.0</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
              </PropertyGroup>
            </Project>
            """);
 
        File.WriteAllText(Path.Combine(appDirectory, "Program.cs"),
            """
            Console.WriteLine("Hello, Aspire!");
            """);
    }
 
    private static string BuildProject(string workingDirectory)
    {
        var output = new StringBuilder();
        var outputDone = new ManualResetEvent(false);
        using var process = new Process();
        // set '--disable-build-servers' so the MSBuild and Roslyn server processes don't hang around, which may hang the test in CI
        process.StartInfo = new ProcessStartInfo("dotnet", "build --disable-build-servers")
        {
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WorkingDirectory = workingDirectory
        };
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data == null)
            {
                outputDone.Set();
            }
            else
            {
                output.AppendLine(e.Data);
            }
        };
        process.Start();
        process.BeginOutputReadLine();
 
        Assert.True(process.WaitForExit(milliseconds: 180_000), "dotnet build command timed out after 3 minutes.");
        Assert.True(process.ExitCode == 0, $"Build failed: {Environment.NewLine}{output}");
 
        Assert.True(outputDone.WaitOne(millisecondsTimeout: 60_000), "Timed out waiting for output to complete.");
 
        return output.ToString();
    }
}