File: CodeGenerationDiagnosticBuilderTests.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 StreamJsonRpc;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
public class CodeGenerationDiagnosticBuilderTests
{
    [Fact]
    public void TryCreateRpcException_NonReflectionFailure_ReturnsNull()
    {
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(
            new InvalidOperationException("plain failure"),
            assemblyLoader: null);
 
        Assert.Null(result);
    }
 
    [Fact]
    public void TryCreateRpcException_TypeLoadException_ReturnsLocalRpcExceptionWithDiagnostic()
    {
        var typeLoad = new TypeLoadException("type not found")
        {
            // TypeName/Message can be empty when the JIT throws — exercise the empty path here
            // separately; for this test we want to confirm the wrapping path itself works.
        };
 
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(typeLoad, assemblyLoader: null);
 
        var localRpc = Assert.IsType<LocalRpcException>(result);
        Assert.Equal(CodeGenerationErrorCodes.IncompatibleAspireSdk, localRpc.ErrorCode);
        var diagnostic = Assert.IsType<CodeGenerationDiagnostic>(localRpc.ErrorData);
        Assert.Equal(typeof(TypeLoadException).FullName, diagnostic.OriginalExceptionType);
        Assert.False(string.IsNullOrWhiteSpace(diagnostic.RemediationHint));
        Assert.False(string.IsNullOrWhiteSpace(localRpc.Message));
        // The default message must NOT leak the .NET-specific type name.
        Assert.DoesNotContain("TypeLoadException", localRpc.Message, StringComparison.Ordinal);
    }
 
    [Fact]
    public void TryCreateRpcException_TypeLoadExceptionWithEmptyMessage_ReturnsStructuredDiagnostic()
    {
        // Repro of issue #16709: JIT-thrown TypeLoadException with no Message — we must still
        // produce a non-empty, actionable Message on the wire.
        var typeLoad = new TypeLoadException();
 
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(typeLoad, assemblyLoader: null);
 
        var localRpc = Assert.IsType<LocalRpcException>(result);
        Assert.False(string.IsNullOrWhiteSpace(localRpc.Message));
        var diagnostic = Assert.IsType<CodeGenerationDiagnostic>(localRpc.ErrorData);
        Assert.Equal(typeof(TypeLoadException).FullName, diagnostic.OriginalExceptionType);
    }
 
    [Fact]
    public void TryCreateRpcException_MissingMethodException_PopulatesMemberName()
    {
        var missing = new MissingMethodException("System.Void Aspire.Hosting.Foo.Bar()");
 
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(missing, assemblyLoader: null);
 
        var localRpc = Assert.IsType<LocalRpcException>(result);
        var diagnostic = Assert.IsType<CodeGenerationDiagnostic>(localRpc.ErrorData);
        Assert.Equal(typeof(MissingMethodException).FullName, diagnostic.OriginalExceptionType);
        Assert.False(string.IsNullOrWhiteSpace(diagnostic.MemberName));
    }
 
    [Fact]
    public void TryCreateRpcException_WrappedTypeLoadException_FindsInnerCause()
    {
        var inner = new TypeLoadException("nested");
        var outer = new InvalidOperationException("wrapper", inner);
 
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(outer, assemblyLoader: null);
 
        var localRpc = Assert.IsType<LocalRpcException>(result);
        var diagnostic = Assert.IsType<CodeGenerationDiagnostic>(localRpc.ErrorData);
        Assert.Equal(typeof(TypeLoadException).FullName, diagnostic.OriginalExceptionType);
    }
 
    [Fact]
    public void TryCreateRpcException_ReflectionTypeLoadException_FindsLoaderException()
    {
        var loader = new TypeLoadException("missing type");
        var rtle = new ReflectionTypeLoadException([null], [loader]);
 
        var result = CodeGenerationDiagnosticBuilder.TryCreateRpcException(rtle, assemblyLoader: null);
 
        var localRpc = Assert.IsType<LocalRpcException>(result);
        var diagnostic = Assert.IsType<CodeGenerationDiagnostic>(localRpc.ErrorData);
        Assert.Equal(typeof(TypeLoadException).FullName, diagnostic.OriginalExceptionType);
    }
 
    [Fact]
    public void BuildDiagnostic_CapturesRuntimeAspireHostingVersion()
    {
        // BuildDiagnostic looks for the loaded Aspire.Hosting assembly via AppDomain. Calling
        // any Aspire.Hosting type forces its assembly to be loaded so the search succeeds.
        _ = typeof(global::Aspire.Hosting.DistributedApplication);
 
        var diagnostic = CodeGenerationDiagnosticBuilder.BuildDiagnostic(
            new TypeLoadException(),
            assemblyLoader: null);
 
        Assert.False(string.IsNullOrWhiteSpace(diagnostic.RuntimeAspireHostingVersion));
    }
 
    [Fact]
    public void BuildDiagnostic_RuntimeAspireHostingVersion_DoesNotFallBackToRemoteHostAssembly()
    {
        _ = typeof(global::Aspire.Hosting.DistributedApplication);
 
        var diagnostic = CodeGenerationDiagnosticBuilder.BuildDiagnostic(
            new TypeLoadException(),
            assemblyLoader: null);
 
        var aspireHosting = AppDomain.CurrentDomain.GetAssemblies()
            .First(a => string.Equals(a.GetName().Name, "Aspire.Hosting", StringComparison.OrdinalIgnoreCase));
        var aspireHostingVersion = aspireHosting
            .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
            ?? aspireHosting.GetName().Version?.ToString();
 
        // Guards #16709 finding #3: prior code fell back to typeof(AssemblyLoader).Assembly which is
        // Aspire.Hosting.RemoteHost - a sibling, not the runtime that backed the failing codegen.
        Assert.Equal(aspireHostingVersion, diagnostic.RuntimeAspireHostingVersion);
    }
 
    [Fact]
    public void TryDescribeAssemblySkew_ExplainsReflectionLoadFailures()
    {
        var description = CodeGenerationDiagnosticBuilder.TryDescribeAssemblySkew(new TypeLoadException("Could not load type 'X'."));
 
        Assert.NotNull(description);
        Assert.Contains("aspire update --self", description);
        Assert.Contains("sdkVersion", description);
    }
 
    [Fact]
    public void TryDescribeAssemblySkew_FindsLoadFailuresNestedInTheExceptionChain()
    {
        var exception = new InvalidOperationException("outer", new MissingMethodException("inner"));
 
        Assert.NotNull(CodeGenerationDiagnosticBuilder.TryDescribeAssemblySkew(exception));
    }
 
    [Fact]
    public void TryDescribeAssemblySkew_FindsLoadFailuresInLaterAggregateChildren()
    {
        // AggregateException.InnerException only exposes the first child, so a load failure raised
        // by any later parallel operation is only reachable by fanning out InnerExceptions.
        var exception = new AggregateException(
            new InvalidOperationException("unrelated"),
            new TypeLoadException("Could not load type 'X'."));
 
        Assert.NotNull(CodeGenerationDiagnosticBuilder.TryDescribeAssemblySkew(exception));
    }
 
    [Fact]
    public void TryDescribeAssemblySkew_IsNullForUnrelatedFailures()
    {
        Assert.Null(CodeGenerationDiagnosticBuilder.TryDescribeAssemblySkew(new InvalidOperationException("boom")));
    }
}