File: RustDebugArgsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Rust.Tests\Aspire.Hosting.Rust.Tests.csproj (Aspire.Hosting.Rust.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREEXTENSION001 // SupportsDebuggingAnnotation is experimental
 
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp.Model;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.Rust.Tests;
 
public class RustDebugArgsTests
{
    /// <summary>
    /// Builds a Rust app in run mode with an IDE attached, then returns the resource and its app-model arguments.
    /// </summary>
    private static async Task<(List<string> Args, RustAppResource Resource)> GetArgsAsync(
        Action<IResourceBuilder<RustAppResource>> configure,
        string[]? supportedLaunchConfigurations = null)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var runSessionInfo = new RunSessionInfo
        {
            ProtocolsSupported = ["test"],
            SupportedLaunchConfigurations = supportedLaunchConfigurations ?? ["rust"]
        };
 
        builder.Configuration["DEBUG_SESSION_INFO"] = JsonSerializer.Serialize(runSessionInfo);
        builder.Configuration["DEBUG_SESSION_PORT"] = "5678";
 
        var rust = builder.AddRustApp("api", AppContext.BaseDirectory);
        configure(rust);
 
        using var app = builder.Build();
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(rust.Resource, app.Services);
 
        return (args, rust.Resource);
    }
 
    [Fact]
    public async Task DebugSessionKeepsCargoToolArgumentsInTheAppModel()
    {
        // The `cargo run ... --` prefix stays in the app model and dashboard. DCP withholds it from the
        // debugged binary because the "rust" launch configuration owns the tool invocation.
        var (args, resource) = await GetArgsAsync(
            rust => rust.WithArgs("--login", "user", "--output", "out.yaml"));
 
        Assert.Equal(["run", "--", "--login", "user", "--output", "out.yaml"], args);
 
        var debugAnnotation = resource.Annotations.OfType<SupportsDebuggingAnnotation>().Last();
        Assert.True(resource.HasLaunchToolArgsOwnedBy(debugAnnotation));
    }
 
    [Fact]
    public async Task ProgramArgumentsCanStartWithSeparator()
    {
        // The user's leading separator is a program argument and stays after the separator the integration owns.
        var (args, _) = await GetArgsAsync(rust => rust.WithArgs("--", "--login", "user"));
 
        Assert.Equal(["run", "--", "--", "--login", "user"], args);
    }
 
    [Fact]
    public async Task CargoBuildOptionsLeadProgramArguments()
    {
        var (args, _) = await GetArgsAsync(rust => rust
            .WithCargoReleaseBuild()
            .WithCargoFeatures("tls-ring")
            .WithCargoArgs("--bin", "server")
            .WithCargoArgs("--locked")
            .WithArgs("--port", "8080"));
 
        Assert.Equal(
            ["run", "--features", "tls-ring", "--release", "--bin", "server", "--locked", "--", "--port", "8080"],
            args);
    }
 
    [Fact]
    public async Task NoProgramArgumentsStillIncludeCargoSeparator()
    {
        var (args, _) = await GetArgsAsync(static _ => { });
 
        Assert.Equal(["run", "--"], args);
    }
 
    [Fact]
    public async Task CargoArgumentsRegisteredAfterProgramArgumentsAreStillApplied()
    {
        // Cargo arguments are held in annotations enumerated when arguments are evaluated rather
        // than when AddRustApp runs, so registration position does not matter for them. Options-derived
        // arguments such as --release are emitted before explicit WithCargoArgs values because the
        // callback that reads those options is registered by AddRustApp.
        var (args, _) = await GetArgsAsync(
            rust => rust.WithArgs("--port", "8080").WithCargoArgs("--locked").WithCargoReleaseBuild(),
            supportedLaunchConfigurations: ["project"]);
 
        Assert.Equal(["run", "--release", "--locked", "--", "--port", "8080"], args);
    }
 
    [Fact]
    public async Task CargoToolArgumentsRemainOwnedWhenRegisteredAfterProgramArguments()
    {
        var (args, resource) = await GetArgsAsync(
            rust => rust.WithArgs("--port", "8080").WithCargoArgs("--locked").WithCargoReleaseBuild());
 
        Assert.Equal(["run", "--release", "--locked", "--", "--port", "8080"], args);
 
        var debugAnnotation = resource.Annotations.OfType<SupportsDebuggingAnnotation>().Last();
        Assert.True(resource.HasLaunchToolArgsOwnedBy(debugAnnotation));
    }
 
    [Fact]
    public async Task RunArgsRetainCargoArgumentsWhenIdeCannotDebugRust()
    {
        // Without an IDE that supports the "rust" launch configuration the resource runs as
        // `cargo run ... -- <program args>`, so the cargo prefix must survive.
        var (args, _) = await GetArgsAsync(
            rust => rust.WithCargoReleaseBuild().WithArgs("--port", "8080"),
            supportedLaunchConfigurations: ["project"]);
 
        Assert.Equal(["run", "--release", "--", "--port", "8080"], args);
    }
}