File: EFOutputDiffingTests.cs
Web Access
Project: src\tests\Aspire.Hosting.EntityFrameworkCore.Tests\Aspire.Hosting.EntityFrameworkCore.Tests.csproj (Aspire.Hosting.EntityFrameworkCore.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.EntityFrameworkCore.Tests.TestServices;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Utils;
using Aspire.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
#pragma warning disable ASPIREPIPELINES005 // Tests opt into the experimental output diffing API.
 
namespace Aspire.Hosting.EntityFrameworkCore.Tests;
 
public class EFOutputDiffingTests(ITestOutputHelper output)
{
    [Theory]
    [InlineData(true, false, false)]
    [InlineData(false, true, false)]
    [InlineData(true, true, false)]
    [InlineData(true, true, true)]
    public async Task ActualGenerationGraphWaitsForBaselineAndPreservesSerialization(bool script, bool bundle, bool separateProjects)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: workspace.Path, step: "inspect");
        var project = builder.AddDotnetProject("api", new Projects.ServiceA().ProjectPath);
        var secondProject = separateProjects ? builder.AddDotnetProject("other", new Projects.ServiceB().ProjectPath) : project;
        var first = project.AddEFMigrations("first", "FirstContext");
        var second = secondProject.AddEFMigrations("second", "SecondContext");
        foreach (var migration in new[] { first, second })
        {
            if (script)
            {
                migration.PublishAsMigrationScript();
            }
            if (bundle)
            {
                migration.PublishAsMigrationBundle(targetRuntime: "linux-x64", publishContainer: true);
            }
        }
        builder.WithOutputDiffing();
        IReadOnlyList<PipelineStep> graph = [];
        builder.Pipeline.AddStep("inspect", _ => Task.CompletedTask);
        builder.Pipeline.AddPipelineConfiguration(context =>
        {
            graph = context.Steps.ToArray();
            return Task.CompletedTask;
        });
        using var app = builder.Build();
        await ExecuteAsync(app);
        var generations = graph.Where(step => step.Name.Contains("-generate-migration-", StringComparison.Ordinal)).ToArray();
        Assert.Equal(script && bundle ? 4 : 2, generations.Length);
        foreach (var generation in generations)
        {
            // Each generation's independently selected closure must include the baseline, not the comparison target.
            Assert.True(Reaches(graph, generation.Name, "publish-and-diff-baseline"), generation.Name);
            Assert.False(Reaches(graph, generation.Name, "publish-and-diff"));
        }
        var firstLast = bundle ? "first-generate-migration-bundle" : "first-generate-migration-script";
        var secondHead = script ? "second-generate-migration-script" : "second-generate-migration-bundle";
        Assert.True(Reaches(graph, secondHead, firstLast));
        if (bundle)
        {
            Assert.True(Reaches(graph, "build-first", "first-generate-migration-bundle"));
            Assert.True(Reaches(graph, "build-second", "second-generate-migration-bundle"));
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ActualEfWriterRunsAfterBaselineAndRetainsChangedArtifact(bool bundle)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        var artifact = Path.Combine(workspace.Path, "efmigrations", bundle ? "migrations" : "migrations.sql");
        Directory.CreateDirectory(Path.GetDirectoryName(artifact)!);
        await File.WriteAllTextAsync(artifact, "old migration", TestContext.Current.CancellationToken);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: workspace.Path, step: "publish-and-diff");
        var project = builder.AddDotnetProject("api", new Projects.ServiceA().ProjectPath);
        var migration = project.AddEFMigrations("migrations");
        if (bundle)
        {
            migration.PublishAsMigrationBundle(targetRuntime: "linux-x64");
        }
        else
        {
            migration.PublishAsMigrationScript();
        }
        var captured = 0;
        builder.Services.AddSingleton<IPipelineOutputFileHasher>(new TestPipelineOutputFileHasher(async (file, ct) =>
        {
            Assert.Equal(artifact, file.FullName);
            using var reader = File.OpenText(file.FullName);
            var content = await reader.ReadToEndAsync(ct);
            if (Interlocked.CompareExchange(ref captured, 1, 0) == 0)
            {
                Assert.Equal("old migration", content);
            }
            return System.Text.Encoding.UTF8.GetBytes(content);
        }));
        var tool = new TestEfTool
        {
            EmitArtifactAsync = arguments =>
            {
                Assert.Equal(1, Volatile.Read(ref captured));
                Assert.Equal(artifact, arguments[Array.IndexOf(arguments, "--output") + 1]);
                return File.WriteAllTextAsync(artifact, "new migration", TestContext.Current.CancellationToken);
            }
        };
        migration.Resource.ToolResource = tool.Resource;
        builder.WithOutputDiffing();
        using var app = builder.Build();
        Assert.Equal("old migration", await File.ReadAllTextAsync(artifact, TestContext.Current.CancellationToken));
 
        var failure = await Assert.ThrowsAsync<DistributedApplicationException>(() => ExecuteAsync(app));
 
        Assert.Equal("Pipeline output changed:" + Environment.NewLine + "  Changed: " + Path.GetRelativePath(workspace.Path, artifact), failure.Message);
        Assert.Equal("new migration", await File.ReadAllTextAsync(artifact, TestContext.Current.CancellationToken));
        Assert.Single(tool.Invocations);
    }
 
    private static bool Reaches(IReadOnlyList<PipelineStep> steps, string from, string target)
    {
        var visited = new HashSet<string>(StringComparer.Ordinal);
        bool Visit(string name) => name == target || (visited.Add(name) &&
            steps.Single(step => step.Name == name).DependsOnSteps.Any(Visit));
        return Visit(from);
    }
 
    private static Task ExecuteAsync(DistributedApplication app) => app.Services.GetRequiredService<IDistributedApplicationPipeline>().ExecuteAsync(
        new PipelineContext(app.Services.GetRequiredService<DistributedApplicationModel>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>(), app.Services,
            NullLogger.Instance, TestContext.Current.CancellationToken));
}
 
#pragma warning restore ASPIREPIPELINES005