File: ResolverDiagnosticsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.RemoteHost.Tests\Aspire.Hosting.RemoteHost.Tests.csproj (Aspire.Hosting.RemoteHost.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.Reflection;
using Aspire.Hosting.RemoteHost.CodeGeneration;
using Aspire.Hosting.RemoteHost.Language;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
/// <summary>
/// Coverage for the diagnostics surfaced by the discovery resolvers when type loading fails or
/// when a "code generation" assembly is loaded but contributes no implementations. These tests
/// guard the user-facing fix for https://github.com/microsoft/aspire/issues/16729 — a previously
/// silent ReflectionTypeLoadException that produced a downstream "no code generator found" /
/// "no language support found" error with no actionable diagnostic.
/// </summary>
public class ResolverDiagnosticsTests
{
    [Fact]
    public void CodeGeneratorResolver_LogsWarning_WhenAssemblyTypeLoadFails()
    {
        var logger = new RecordingLogger<CodeGeneratorResolver>();
        var stub = new TypeLoadFailingAssembly("Aspire.Hosting.CodeGeneration.Synthetic");
 
        using var services = new ServiceCollection().BuildServiceProvider();
        var resolver = new CodeGeneratorResolver(services, () => (IReadOnlyList<Assembly>)[stub], logger);
 
        Assert.Null(resolver.GetCodeGenerator("anything"));
 
        var warning = logger.Entries.SingleOrDefault(e => e.Level == LogLevel.Warning && e.Message.Contains("could not be loaded"));
        Assert.NotNull(warning);
        Assert.Contains("LoaderExceptions", warning!.Message);
        Assert.Contains(stub.GetName().Name!, warning.Message);
        Assert.Contains("synthetic loader exception", warning.Message);
    }
 
    [Fact]
    public void LanguageSupportResolver_LogsWarning_WhenAssemblyTypeLoadFails()
    {
        var logger = new RecordingLogger<LanguageSupportResolver>();
        var stub = new TypeLoadFailingAssembly("Aspire.Hosting.CodeGeneration.Synthetic");
 
        using var services = new ServiceCollection().BuildServiceProvider();
        var resolver = new LanguageSupportResolver(services, () => (IReadOnlyList<Assembly>)[stub], logger);
 
        Assert.Null(resolver.GetLanguageSupport("anything"));
 
        var warning = logger.Entries.SingleOrDefault(e => e.Level == LogLevel.Warning && e.Message.Contains("could not be loaded"));
        Assert.NotNull(warning);
        Assert.Contains("LoaderExceptions", warning!.Message);
        Assert.Contains(stub.GetName().Name!, warning.Message);
        Assert.Contains("synthetic loader exception", warning.Message);
    }
 
    [Fact]
    public void CodeGeneratorResolver_LogsWarning_WhenCodeGenerationAssemblyContributesNothing()
    {
        var logger = new RecordingLogger<CodeGeneratorResolver>();
        // The marker name is what triggers the "did not contribute any" diagnostic.
        var empty = new EmptyNamedAssembly("Aspire.Hosting.CodeGeneration.Synthetic");
 
        using var services = new ServiceCollection().BuildServiceProvider();
        var resolver = new CodeGeneratorResolver(services, () => (IReadOnlyList<Assembly>)[empty], logger);
 
        Assert.Null(resolver.GetCodeGenerator("anything"));
 
        var warning = logger.Entries.SingleOrDefault(e => e.Level == LogLevel.Warning && e.Message.Contains("did not contribute"));
        Assert.NotNull(warning);
        Assert.Contains("ICodeGenerator", warning!.Message);
    }
 
    [Fact]
    public void CodeGeneratorResolver_DoesNotLogContributionWarning_ForArbitraryAssembly()
    {
        var logger = new RecordingLogger<CodeGeneratorResolver>();
        var arbitrary = new EmptyNamedAssembly("My.Custom.Integration");
 
        using var services = new ServiceCollection().BuildServiceProvider();
        _ = new CodeGeneratorResolver(services, () => (IReadOnlyList<Assembly>)[arbitrary], logger).GetCodeGenerator("anything");
 
        Assert.DoesNotContain(logger.Entries, e => e.Level == LogLevel.Warning && e.Message.Contains("did not contribute"));
    }
 
    private sealed class TypeLoadFailingAssembly : Assembly
    {
        private readonly AssemblyName _name;
 
        public TypeLoadFailingAssembly(string name) => _name = new AssemblyName(name);
 
        public override AssemblyName GetName() => _name;
 
        public override Type[] GetTypes()
            => throw new ReflectionTypeLoadException(
                [null, typeof(string)],
                [new FileLoadException("synthetic loader exception: simulated Aspire.TypeSystem mismatch")]);
    }
 
    private sealed class EmptyNamedAssembly : Assembly
    {
        private readonly AssemblyName _name;
 
        public EmptyNamedAssembly(string name) => _name = new AssemblyName(name);
 
        public override AssemblyName GetName() => _name;
 
        public override Type[] GetTypes() => [typeof(string)];
    }
}