// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.DotNet;
using Aspire.Cli.Interaction;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Hosting;
using Aspire.Tests;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Net.Sockets;
namespace Aspire.Cli.Tests.DotNet;
public class DotNetCliRunnerTests(ITestOutputHelper outputHelper)
{
private static Aspire.Cli.CliExecutionContext CreateExecutionContext(DirectoryInfo workingDirectory)
{
return TestExecutionContextHelper.CreateExecutionContext(workingDirectory);
}
private static string GetDotNetExecutablePath()
{
var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (!string.IsNullOrWhiteSpace(dotnetRoot))
{
return Path.Combine(dotnetRoot, OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet");
}
return "dotnet";
}
[Fact]
public async Task DotNetCliCorrectlyAppliesNoLaunchProfileArgumentWhenSpecifiedInOptions()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true,
LaunchProfile = "E2E"
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
Assert.Equal(
["run", "--no-launch-profile", "--project", projectFile.FullName, "--", "--operation", "inspect"],
args);
},
42);
// This is what we are really testing here - that RunAsync reads
// the NoLaunchProfile property from the invocation options and
// correctly applies it to the command-line arguments for the
// dotnet run command.
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
}
[Theory]
[InlineData("AppHost.csproj", false)]
[InlineData("AppHost.csproj", true)]
[InlineData("apphost.cs", false)]
public async Task RunAsyncAppliesSelectedLaunchProfile(string appHostFileName, bool watch)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, appHostFileName));
await File.WriteAllTextAsync(appHostFile.FullName, "Not a real AppHost.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
string[] expectedArgs = appHostFile.Extension == ".cs"
? ["run", "--launch-profile=--no-build", $"/p:{KnownConfigNames.SuppressCliRunHook}=true", "--file", appHostFile.FullName, "--"]
: watch
? ["watch", "--non-interactive", "--launch-profile=--no-build", "--project", appHostFile.FullName, "--"]
: ["run", "--launch-profile=--no-build", "--project", appHostFile.FullName, "--"];
Assert.Equal(expectedArgs, args);
},
42);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch,
noBuild: false,
noRestore: false,
args: [],
env: null,
backchannelCompletionSource: null,
new ProcessInvocationOptions { LaunchProfile = "--no-build" },
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
}
[Fact]
public async Task RunAsyncPreservesApplicationArgumentBoundaries()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var appHostArguments = new[] { "--custom", "value with spaces", "", "literal \"quote\"", @"C:\tools\backslash\path" };
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) => Assert.Equal(
["run", "--project", projectFile.FullName, "--", .. appHostArguments],
args),
42);
var exitCode = await runner.RunAsync(
projectFile,
watch: false,
noBuild: false,
noRestore: false,
appHostArguments,
env: null,
backchannelCompletionSource: null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
}
[Fact]
public async Task RunAppHostCommandAsyncUsesNativeCommandWithoutRunDelimiter()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
var appHostCommand = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "Debug", "net10.0", "AppHost");
var runWorkingDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "run-cwd"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(fileName, args, env, workingDirectory, _) =>
{
Assert.Equal(appHostCommand, fileName);
Assert.Collection(args,
arg => Assert.Equal("--operation", arg),
arg => Assert.Equal("inspect", arg));
Assert.DoesNotContain("--", args);
Assert.Equal(runWorkingDirectory.FullName, workingDirectory.FullName);
Assert.NotNull(env);
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
},
42);
var exitCode = await runner.RunAppHostCommandAsync(
projectFile,
appHostCommand,
runWorkingDirectory,
["--operation", "inspect"],
new Dictionary<string, string>(),
null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
}
[Fact]
public async Task RunAppHostCommandAsyncResolvesDotnetMuxerCommand()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
var appHostAssembly = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "Debug", "net10.0", "AppHost.dll");
var runWorkingDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "run-cwd"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(fileName, args, _, workingDirectory, _) =>
{
Assert.Equal("dotnet", Path.GetFileNameWithoutExtension(fileName));
Assert.Collection(args,
arg => Assert.Equal("exec", arg),
arg => Assert.Equal(appHostAssembly, arg),
arg => Assert.Equal("--operation", arg),
arg => Assert.Equal("inspect", arg));
Assert.Equal(runWorkingDirectory.FullName, workingDirectory.FullName);
},
42);
var exitCode = await runner.RunAppHostCommandAsync(
projectFile,
"dotnet.exe",
runWorkingDirectory,
["exec", appHostAssembly, "--operation", "inspect"],
new Dictionary<string, string>(),
null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
}
[Fact]
public async Task BuildAsyncDoesNotInjectDotnetCliUseMsBuildServerEnvironmentVariable()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
},
0);
var exitCode = await runner.BuildAsync(projectFile, noRestore: false, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task BuildAsyncPassesCurrentAspireCliPathToMSBuild()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, env, _, _) =>
{
Assert.NotNull(env);
if (Environment.ProcessPath is { } processPath && DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(processPath))
{
Assert.Equal(processPath, env["AspireCliPath"]);
}
else
{
Assert.False(env.ContainsKey("AspireCliPath"));
}
},
0);
var exitCode = await runner.BuildAsync(projectFile, noRestore: false, new ProcessInvocationOptions(), CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public void ShouldForwardProcessPathAsAspireCliPathRejectsUnbundledFrameworkDependentCliBuild()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var cliDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "artifacts", "bin", "Aspire.Cli", "Debug", "net10.0"));
var cliPath = Path.Combine(cliDirectory.FullName, OperatingSystem.IsWindows() ? "aspire.exe" : "aspire");
File.WriteAllText(cliPath, string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, "aspire.dll"), string.Empty);
Assert.False(DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(cliPath));
}
[Fact]
[SkipOnPlatform(TestPlatforms.Windows, "Symlink resolution test only runs on Linux/macOS where unprivileged symlink creation is reliable.")]
public void ShouldForwardProcessPathAsAspireCliPathRejectsSymlinkToUnbundledFrameworkDependentCliBuild()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var cliDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "artifacts", "bin", "Aspire.Cli", "Debug", "net10.0"));
var cliPath = Path.Combine(cliDirectory.FullName, "aspire");
File.WriteAllText(cliPath, string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, "aspire.dll"), string.Empty);
var linkDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "bin"));
var linkPath = Path.Combine(linkDirectory.FullName, "aspire");
File.CreateSymbolicLink(linkPath, cliPath);
Assert.False(DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(linkPath));
}
[Fact]
public void ShouldForwardProcessPathAsAspireCliPathRejectsDotNetMuxerPath()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var dotnetDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "dotnet"));
var dotnetPath = Path.Combine(dotnetDirectory.FullName, OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet");
File.WriteAllText(dotnetPath, string.Empty);
Assert.False(DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(dotnetPath));
}
[Fact]
public void ShouldForwardProcessPathAsAspireCliPathAllowsFrameworkDependentCliWithInstallSidecar()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var cliDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "bin"));
var cliPath = Path.Combine(cliDirectory.FullName, OperatingSystem.IsWindows() ? "aspire.exe" : "aspire");
File.WriteAllText(cliPath, string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, "aspire.dll"), string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, ".aspire-install.json"), """{"source":"script"}""");
Assert.True(DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(cliPath));
}
[Fact]
public void ShouldForwardProcessPathAsAspireCliPathAllowsAdjacentBundleLayout()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var cliDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "bin"));
var cliPath = Path.Combine(cliDirectory.FullName, OperatingSystem.IsWindows() ? "aspire.exe" : "aspire");
File.WriteAllText(cliPath, string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, "aspire.dll"), string.Empty);
Directory.CreateDirectory(Path.Combine(cliDirectory.FullName, "dcp"));
Directory.CreateDirectory(Path.Combine(cliDirectory.FullName, "managed"));
File.WriteAllText(Path.Combine(cliDirectory.FullName, "dcp", OperatingSystem.IsWindows() ? "dcp.exe" : "dcp"), string.Empty);
File.WriteAllText(Path.Combine(cliDirectory.FullName, "managed", OperatingSystem.IsWindows() ? "aspire-managed.exe" : "aspire-managed"), string.Empty);
Assert.True(DotNetCliRunner.ShouldForwardProcessPathAsAspireCliPath(cliPath));
}
[Fact]
public async Task RestoreAsyncRunsDotnetRestoreCommand()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
Assert.Equal("restore", args[0]);
Assert.Equal(projectFile.FullName, args[1]);
},
0);
var exitCode = await runner.RestoreAsync(projectFile, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task BuildAsyncDoesNotInjectConfiguredDotnetCliUseMsBuildServer()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
services.AddSingleton<IConfiguration>(sp =>
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
["DOTNET_CLI_USE_MSBUILD_SERVER"] = "false"
});
return configBuilder.Build();
});
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
},
0);
var exitCode = await runner.BuildAsync(projectFile, noRestore: false, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task BuildAsyncIncludesNoRestoreFlagWhenNoRestoreIsTrue()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-restore is included when noRestore is true
Assert.Contains("build", args);
Assert.Contains("--no-restore", args);
},
0);
var exitCode = await runner.BuildAsync(projectFile, noRestore: true, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task BuildAsyncDoesNotIncludeNoRestoreFlagWhenNoRestoreIsFalse()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-restore is NOT included when noRestore is false
Assert.Contains("build", args);
Assert.DoesNotContain("--no-restore", args);
},
0);
var exitCode = await runner.BuildAsync(projectFile, noRestore: false, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task BuildAsyncAddsBinlogWhenBinlogDirectoryIsConfigured()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var binlogDirectory = workspace.WorkspaceRoot.CreateSubdirectory("binlogs");
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[KnownConfigNames.CliDotnetBinlogDirectory] = binlogDirectory.FullName
})
.Build();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
var binlogArgument = Assert.Single(args, arg => arg.StartsWith("/bl:", StringComparison.Ordinal));
var binlogPath = binlogArgument["/bl:".Length..];
Assert.Equal(binlogDirectory.FullName, Path.GetDirectoryName(binlogPath));
Assert.EndsWith(".binlog", binlogPath);
Assert.Contains("build", Path.GetFileName(binlogPath));
Assert.Contains("AppHost", Path.GetFileName(binlogPath));
},
0,
configuration: configuration);
var exitCode = await runner.BuildAsync(projectFile, noRestore: false, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task NewProjectAsyncDoesNotAddBinlogWhenBinlogDirectoryIsConfigured()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[KnownConfigNames.CliDotnetBinlogDirectory] = workspace.WorkspaceRoot.CreateSubdirectory("binlogs").FullName
})
.Build();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
Assert.DoesNotContain(args, arg => arg.StartsWith("/bl:", StringComparison.Ordinal));
},
0,
configuration: configuration);
var exitCode = await runner.NewProjectAsync("aspire", "TestProject", "/tmp/test", [], options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotInjectDotnetCliUseMsBuildServerWhenNoBuildIsFalse()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotInjectDotnetCliUseMsBuildServerWhenNoBuildIsTrue()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// When noBuild is true, the original env should be passed through unchanged
// or should be null if no env was provided
if (env != null)
{
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
}
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: true, // This should NOT inject the environment variable
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncPreservesExistingEnvironmentVariables()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.False(env.ContainsKey("DOTNET_CLI_USE_MSBUILD_SERVER"));
// Verify existing environment variable is preserved
Assert.True(env.ContainsKey("EXISTING_VAR"));
Assert.Equal("existing_value", env["EXISTING_VAR"]);
},
0);
var existingEnv = new Dictionary<string, string>
{
["EXISTING_VAR"] = "existing_value"
};
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: existingEnv,
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncPropagatesProcessProfilingContextToChildEnvironment()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ConfigurationCallback += config =>
{
config[ProfilingTelemetry.EnvironmentVariables.Enabled] = "true";
config[ProfilingTelemetry.EnvironmentVariables.SessionId] = "session-1";
};
});
using var provider = services.BuildServiceProvider();
var profilingTelemetry = provider.GetRequiredService<ProfilingTelemetry>();
using var listener = ActivityListenerHelper.Create(profilingTelemetry.ActivitySource);
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, env, _, _) =>
{
var currentActivity = Activity.Current;
Assert.NotNull(currentActivity);
Assert.Equal(ProfilingTelemetry.ActivitySourceName, currentActivity.Source.Name);
Assert.NotNull(env);
Assert.Equal("true", env[ProfilingTelemetry.EnvironmentVariables.Enabled]);
Assert.Equal("true", env[KnownConfigNames.Legacy.StartupProfilingEnabled]);
Assert.Equal("session-1", env[ProfilingTelemetry.EnvironmentVariables.SessionId]);
Assert.Equal("session-1", env[KnownConfigNames.Legacy.StartupOperationId]);
Assert.Equal(currentActivity.Id, env[ProfilingTelemetry.EnvironmentVariables.TraceParent]);
Assert.Equal(currentActivity.Id, env[KnownConfigNames.Legacy.StartupTraceParent]);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncParentsBackchannelConnectToRunOperationInsteadOfDotnetProcess()
{
const string sessionId = "backchannel-parent-session";
var startedActivities = new ConcurrentQueue<Activity>();
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var backchannel = new TestAppHostBackchannel
{
ConnectAsyncCalled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
};
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ConfigurationCallback += config =>
{
config[ProfilingTelemetry.EnvironmentVariables.Enabled] = "true";
config[ProfilingTelemetry.EnvironmentVariables.SessionId] = sessionId;
};
options.AppHostBackchannelFactory = _ => backchannel;
});
using var provider = services.BuildServiceProvider();
var profilingTelemetry = provider.GetRequiredService<ProfilingTelemetry>();
using var listener = ActivityListenerHelper.Create(profilingTelemetry.ActivitySource, onActivityStarted: startedActivities.Enqueue);
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, _, _, _) => { },
0);
var backchannelCompletionSource = new TaskCompletionSource<IAppHostCliBackchannel>(TaskCreationOptions.RunContinuationsAsynchronously);
ActivitySpanId runSpanId;
using (profilingTelemetry.StartRunCommand())
{
runSpanId = Activity.Current!.SpanId;
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>
{
[KnownConfigNames.UnixSocketPath] = "cli.sock"
},
backchannelCompletionSource,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
await backchannel.ConnectAsyncCalled.Task.DefaultTimeout();
Assert.Same(backchannel, await backchannelCompletionSource.Task.DefaultTimeout());
}
var sessionActivities = startedActivities
.Where(activity => activity.Tags.Any(tag => tag.Key == ProfilingTelemetry.Tags.ProfilingSessionId && tag.Value == sessionId))
.ToArray();
var processActivity = Assert.Single(sessionActivities, activity => activity.OperationName == ProfilingTelemetry.Activities.Process);
var backchannelActivity = Assert.Single(sessionActivities, activity => activity.OperationName == ProfilingTelemetry.Activities.BackchannelConnect);
Assert.Equal(runSpanId, backchannelActivity.ParentSpanId);
Assert.NotEqual(processActivity.SpanId, backchannelActivity.ParentSpanId);
}
[Fact]
public async Task NewProjectAsyncReturnsExitCode73WhenProjectAlreadyExists()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify the arguments are correct for dotnet new
Assert.Contains("new", args);
Assert.Contains("aspire", args);
Assert.Contains("--name", args);
Assert.Contains("TestProject", args);
Assert.Contains("--output", args);
Assert.Contains("/tmp/test", args);
},
73); // Return exit code 73 to simulate project already exists
// Act
var exitCode = await runner.NewProjectAsync("aspire", "TestProject", "/tmp/test", [], options, CancellationToken.None).DefaultTimeout();
// Assert
Assert.Equal(73, exitCode);
}
[Fact]
public async Task RunAsyncSetsVersionCheckDisabledWhenUpdateNotificationsFeatureIsDisabled()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DisabledFeatures = [KnownFeatures.UpdateNotificationsEnabled];
});
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
provider.GetRequiredService<CliExecutionContext>(),
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED"));
Assert.Equal("true", env["ASPIRE_VERSION_CHECK_DISABLED"]);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotSetVersionCheckDisabledWhenUpdateNotificationsFeatureIsEnabled()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.UpdateNotificationsEnabled];
});
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
provider.GetRequiredService<CliExecutionContext>(),
(args, env, _, _) =>
{
// When the feature is enabled (default), the version check env var should NOT be set
if (env != null)
{
Assert.False(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED"));
}
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotOverrideUserProvidedVersionCheckDisabledValue()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DisabledFeatures = [KnownFeatures.UpdateNotificationsEnabled];
});
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
provider.GetRequiredService<CliExecutionContext>(),
(args, env, _, _) =>
{
Assert.NotNull(env);
Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED"));
// Should preserve user's value, not override with "true"
Assert.Equal("false", env["ASPIRE_VERSION_CHECK_DISABLED"]);
},
0);
// User explicitly sets the environment variable to false
var userEnv = new Dictionary<string, string>
{
["ASPIRE_VERSION_CHECK_DISABLED"] = "false"
};
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: ["--operation", "inspect"],
env: userEnv,
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncKeepsExtensionLaunchedAppHostAliveUntilBackchannelDisconnects()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var launchAppHostCalledTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var backchannel = new TestAppHostBackchannel
{
ConnectAsyncCalled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
};
TestExtensionInteractionService? testExtensionInteractionService = null;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.InteractionServiceFactory = sp =>
{
testExtensionInteractionService = new TestExtensionInteractionService(sp)
{
LaunchAppHostCallback = () => launchAppHostCalledTcs.SetResult(),
};
return testExtensionInteractionService;
};
options.ConfigurationCallback = configBuilder =>
{
configBuilder["ASPIRE_EXTENSION_TOKEN"] = "extension-token";
};
options.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel
{
HasCapabilityAsyncCallback = (c, _) => Task.FromResult(c is "devkit" or "project"),
};
options.AppHostBackchannelFactory = _ => backchannel;
});
using var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var backchannelCompletionSource = new TaskCompletionSource<IAppHostCliBackchannel>(TaskCreationOptions.RunContinuationsAsynchronously);
var runTask = runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>
{
[KnownConfigNames.UnixSocketPath] = Path.Combine(workspace.WorkspaceRoot.FullName, "cli.sock")
},
backchannelCompletionSource,
options: new ProcessInvocationOptions(),
cancellationToken: CancellationToken.None);
await launchAppHostCalledTcs.Task.DefaultTimeout();
await backchannel.ConnectAsyncCalled.Task.DefaultTimeout();
Assert.Same(backchannel, await backchannelCompletionSource.Task.DefaultTimeout());
var completedTask = await Task.WhenAny(runTask, Task.Delay(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken));
Assert.NotSame(runTask, completedTask);
backchannel.DisconnectCompletionSource.SetResult();
Assert.Equal(CliExitCodes.Success, await runTask.DefaultTimeout());
}
[Fact]
public async Task RunAsyncFailsBackchannelWhenExtensionLaunchedAppHostDoesNotConnectBeforeTimeout()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.InteractionServiceFactory = sp => new TestExtensionInteractionService(sp);
options.ConfigurationCallback = configBuilder =>
{
configBuilder["ASPIRE_EXTENSION_TOKEN"] = "extension-token";
configBuilder[KnownConfigNames.CliBackchannelConnectTimeoutSeconds] = "0";
};
options.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel
{
HasCapabilityAsyncCallback = (c, _) => Task.FromResult(c is "devkit" or "project"),
};
options.AppHostBackchannelFactory = _ => new TestAppHostBackchannel
{
ConnectAsyncCallback = (_, _) => throw new SocketException((int)SocketError.ConnectionRefused)
};
});
using var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var backchannelCompletionSource = new TaskCompletionSource<IAppHostCliBackchannel>(TaskCreationOptions.RunContinuationsAsynchronously);
var exception = await Assert.ThrowsAsync<TimeoutException>(
() => runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>
{
[KnownConfigNames.UnixSocketPath] = Path.Combine(workspace.WorkspaceRoot.FullName, "cli.sock")
},
backchannelCompletionSource,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout());
Assert.Contains("Timed out waiting for AppHost backchannel", exception.Message);
}
[Fact]
public async Task RunAsyncFailsBackchannelWhenAppHostExitsWithSuccessBeforeBackchannelConnects()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DotNetCliExecutionFactoryFactory = _ => new TestProcessExecutionFactory
{
CreateExecutionCallback = (args, env, _, _) => new ExitedProcessExecution(args, env, exitCode: 0)
};
options.AppHostBackchannelFactory = _ => new TestAppHostBackchannel
{
ConnectAsyncCallback = (_, _) => throw new SocketException((int)SocketError.ConnectionRefused)
};
});
using var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var backchannelCompletionSource = new TaskCompletionSource<IAppHostCliBackchannel>(TaskCreationOptions.RunContinuationsAsynchronously);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>
{
[KnownConfigNames.UnixSocketPath] = Path.Combine(workspace.WorkspaceRoot.FullName, "cli.sock")
},
backchannelCompletionSource,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(CliExitCodes.Success, exitCode);
var exception = await Assert.ThrowsAsync<FailedToConnectBackchannelConnection>(
() => backchannelCompletionSource.Task).DefaultTimeout();
Assert.Equal("The AppHost process exited", exception.Message);
}
[Fact]
public async Task RunAsyncFailsBackchannelWithExitCodeWhenAppHostExitsUnexpectedlyBeforeBackchannelConnects()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DotNetCliExecutionFactoryFactory = _ => new TestProcessExecutionFactory
{
CreateExecutionCallback = (args, env, _, _) => new ExitedProcessExecution(args, env, exitCode: 42)
};
options.AppHostBackchannelFactory = _ => new TestAppHostBackchannel
{
ConnectAsyncCallback = (_, _) => throw new SocketException((int)SocketError.ConnectionRefused)
};
});
using var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var backchannelCompletionSource = new TaskCompletionSource<IAppHostCliBackchannel>(TaskCreationOptions.RunContinuationsAsynchronously);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>
{
[KnownConfigNames.UnixSocketPath] = Path.Combine(workspace.WorkspaceRoot.FullName, "cli.sock")
},
backchannelCompletionSource,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(42, exitCode);
var exception = await Assert.ThrowsAsync<FailedToConnectBackchannelConnection>(
() => backchannelCompletionSource.Task).DefaultTimeout();
Assert.Equal("The AppHost process exited unexpectedly with exit code 42", exception.Message);
}
[Fact]
public async Task AddPackageAsyncUseFilesSwitchForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify arguments are correct for single-file AppHost
Assert.Contains("add", args);
Assert.Contains("package", args);
Assert.Contains("--file", args);
Assert.Contains(appHostFile.FullName, args);
Assert.Contains("Aspire.Hosting.Redis@9.2.0", args);
Assert.Contains("--no-restore", args);
// Verify the order: package add PackageName --file FilePath --version Version --no-restore
var packageIndex = Array.IndexOf(args, "package");
var addIndex = Array.IndexOf(args, "add");
var fileIndex = Array.IndexOf(args, "--file");
var filePathIndex = Array.IndexOf(args, appHostFile.FullName);
var packageNameIndex = Array.IndexOf(args, "Aspire.Hosting.Redis@9.2.0");
Assert.True(packageIndex < addIndex);
Assert.True(packageIndex < fileIndex);
Assert.True(fileIndex < filePathIndex);
Assert.True(filePathIndex < packageNameIndex);
},
0);
var exitCode = await runner.AddPackageAsync(
appHostFile,
"Aspire.Hosting.Redis",
"9.2.0",
nugetSource: null,
noRestore: true, // should use --no-restore
options,
CancellationToken.None
);
Assert.Equal(0, exitCode);
}
[Fact]
public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFile()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "<Project></Project>");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify arguments are correct for .csproj file (new behavior with --version)
Assert.Contains("add", args);
Assert.Contains("package", args);
Assert.Contains(projectFile.FullName, args);
Assert.Contains("Aspire.Hosting.Redis", args);
Assert.Contains("--version", args);
Assert.Contains("9.2.0", args);
Assert.Contains("--source", args);
Assert.Contains("https://api.nuget.org/v3/index.json", args);
// Verify the order: package add PackageName --version Version --source Source --project ProjectFile
var packageIndex = Array.IndexOf(args, "package");
var addIndex = Array.IndexOf(args, "add");
var projectFlagIndex = Array.IndexOf(args, "--project");
var projectValueIndex = Array.IndexOf(args, projectFile.FullName);
var packageNameIndex = Array.IndexOf(args, "Aspire.Hosting.Redis");
var versionFlagIndex = Array.IndexOf(args, "--version");
var versionValueIndex = Array.IndexOf(args, "9.2.0");
var sourceFlagIndex = Array.IndexOf(args, "--source");
var sourceValueIndex = Array.IndexOf(args, "https://api.nuget.org/v3/index.json");
Assert.True(packageIndex < addIndex);
Assert.True(addIndex < packageNameIndex);
Assert.True(packageNameIndex < versionFlagIndex);
Assert.True(versionFlagIndex < versionValueIndex);
Assert.True(packageNameIndex < projectFlagIndex);
Assert.True(projectFlagIndex < projectValueIndex);
Assert.True(packageNameIndex < sourceFlagIndex);
Assert.True(sourceFlagIndex < sourceValueIndex);
// Should NOT contain --file or the @version format
Assert.DoesNotContain("--file", args);
Assert.DoesNotContain("Aspire.Hosting.Redis@9.2.0", args);
},
0);
var exitCode = await runner.AddPackageAsync(
projectFile,
"Aspire.Hosting.Redis",
"9.2.0",
"https://api.nuget.org/v3/index.json", // provide source, should use --source
noRestore: false,
options,
CancellationToken.None
);
Assert.Equal(0, exitCode);
}
[Fact]
public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestore()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "<Project></Project>");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify arguments are correct for .csproj file with --no-restore (no source provided)
Assert.Contains("package", args);
Assert.Contains("add", args);
Assert.Contains("Aspire.Hosting.Redis", args);
Assert.Contains("--version", args);
Assert.Contains("9.2.0", args);
Assert.Contains("--project", args);
Assert.Contains(projectFile.FullName, args);
Assert.Contains("--no-restore", args);
// Verify the order: package add PackageName --version Version --project ProjectFile --no-restore
var packageIndex = Array.IndexOf(args, "package");
var addIndex = Array.IndexOf(args, "add");
var packageNameIndex = Array.IndexOf(args, "Aspire.Hosting.Redis");
var versionFlagIndex = Array.IndexOf(args, "--version");
var versionValueIndex = Array.IndexOf(args, "9.2.0");
var projectFlagIndex = Array.IndexOf(args, "--project");
var projectValueIndex = Array.IndexOf(args, projectFile.FullName);
var noRestoreIndex = Array.IndexOf(args, "--no-restore");
Assert.True(packageIndex < addIndex);
Assert.True(addIndex < packageNameIndex);
Assert.True(packageNameIndex < versionFlagIndex);
Assert.True(versionFlagIndex < versionValueIndex);
Assert.True(packageNameIndex < projectFlagIndex);
Assert.True(projectFlagIndex < projectValueIndex);
Assert.True(versionValueIndex < noRestoreIndex);
// Should NOT contain --file, --source, or the @version format
Assert.DoesNotContain("--file", args);
Assert.DoesNotContain("--source", args);
Assert.DoesNotContain("Aspire.Hosting.Redis@9.2.0", args);
},
0);
var exitCode = await runner.AddPackageAsync(
projectFile,
"Aspire.Hosting.Redis",
"9.2.0",
nugetSource: null,
noRestore: true,
options,
CancellationToken.None
);
Assert.Equal(0, exitCode);
}
[Fact]
public async Task AddPackageAsyncWithSourceAndNoRestoreHasArgumentSourceAndNoRestore()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "<Project></Project>");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify arguments are correct for .csproj file with --source and --no-restore
Assert.Contains("package", args);
Assert.Contains("add", args);
Assert.Contains("Aspire.Hosting.Redis", args);
Assert.Contains("--version", args);
Assert.Contains("9.2.0", args);
Assert.Contains("--project", args);
Assert.Contains(projectFile.FullName, args);
Assert.Contains("--source", args);
Assert.Contains("https://api.nuget.org/v3/index.json", args);
Assert.Contains("--no-restore", args);
// Verify the order: package add PackageName --version Version --project ProjectFile --no-restore --source Source
var packageIndex = Array.IndexOf(args, "package");
var addIndex = Array.IndexOf(args, "add");
var packageNameIndex = Array.IndexOf(args, "Aspire.Hosting.Redis");
var versionFlagIndex = Array.IndexOf(args, "--version");
var versionValueIndex = Array.IndexOf(args, "9.2.0");
var projectFlagIndex = Array.IndexOf(args, "--project");
var projectValueIndex = Array.IndexOf(args, projectFile.FullName);
var sourceFlagIndex = Array.IndexOf(args, "--source");
var sourceValueIndex = Array.IndexOf(args, "https://api.nuget.org/v3/index.json");
var noRestoreIndex = Array.IndexOf(args, "--no-restore");
Assert.True(packageIndex < addIndex);
Assert.True(addIndex < packageNameIndex);
Assert.True(packageNameIndex < versionFlagIndex);
Assert.True(versionFlagIndex < versionValueIndex);
Assert.True(packageNameIndex < projectFlagIndex);
Assert.True(projectFlagIndex < projectValueIndex);
Assert.True(packageNameIndex < noRestoreIndex);
Assert.True(packageNameIndex < sourceFlagIndex);
Assert.True(sourceFlagIndex < sourceValueIndex);
// Should NOT contain --file or the @version format
Assert.DoesNotContain("--file", args);
Assert.DoesNotContain("Aspire.Hosting.Redis@9.2.0", args);
},
0);
var exitCode = await runner.AddPackageAsync(
projectFile,
"Aspire.Hosting.Redis",
"9.2.0",
"https://api.nuget.org/v3/index.json", // provide source, should use --source
noRestore: true,
options,
CancellationToken.None
);
Assert.Equal(0, exitCode);
}
[Fact]
public async Task GetSolutionProjectsAsync_ParsesOutputCorrectly()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
// Create a fake solution file
var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln"));
await File.WriteAllTextAsync(solutionFile.FullName, "Not a real solution file.");
// Create project files
var project1Dir = workspace.WorkspaceRoot.CreateSubdirectory("Project1");
var project1File = new FileInfo(Path.Combine(project1Dir.FullName, "Project1.csproj"));
await File.WriteAllTextAsync(project1File.FullName, "Not a real project file.");
var project2Dir = workspace.WorkspaceRoot.CreateSubdirectory("Project2");
var project2File = new FileInfo(Path.Combine(project2Dir.FullName, "Project2.csproj"));
await File.WriteAllTextAsync(project2File.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions
{
StandardOutputCallback = (line) => outputHelper.WriteLine($"stdout: {line}")
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
// Simulate dotnet sln list output
invocationOptions.StandardOutputCallback?.Invoke("Project(s)");
invocationOptions.StandardOutputCallback?.Invoke("----------");
invocationOptions.StandardOutputCallback?.Invoke($"Project1{Path.DirectorySeparatorChar}Project1.csproj");
invocationOptions.StandardOutputCallback?.Invoke($"Project2{Path.DirectorySeparatorChar}Project2.csproj");
},
0);
var (exitCode, projects) = await runner.GetSolutionProjectsAsync(solutionFile, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.Equal(2, projects.Count);
Assert.Contains(projects, p => p.Name == "Project1.csproj");
Assert.Contains(projects, p => p.Name == "Project2.csproj");
}
[Fact]
public async Task AddProjectReferenceAsync_ExecutesCorrectCommand()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var referencedProject = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Service.csproj"));
await File.WriteAllTextAsync(referencedProject.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
Assert.Contains("add", args);
Assert.Contains(projectFile.FullName, args);
Assert.Contains("reference", args);
Assert.Contains(referencedProject.FullName, args);
},
0);
var exitCode = await runner.AddProjectReferenceAsync(projectFile, referencedProject, options, CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// For single-file .cs files, should include --no-launch-profile
Assert.Collection(args,
arg => Assert.Equal("run", arg),
arg => Assert.Equal("--no-launch-profile", arg),
arg => Assert.Equal($"/p:{KnownConfigNames.SuppressCliRunHook}=true", arg),
arg => Assert.Equal("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenNotSpecified()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// For single-file .cs files, should NOT include --no-launch-profile when false
Assert.Collection(args,
arg => Assert.Equal("run", arg),
arg => Assert.Equal($"/p:{KnownConfigNames.SuppressCliRunHook}=true", arg),
arg => Assert.Equal("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncSuppressesCliRunHookForProjectAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
Dictionary<string, string>? capturedEnv = null;
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, env, _, _) =>
{
capturedEnv = env?.ToDictionary();
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.NotNull(capturedEnv);
Assert.Equal("true", capturedEnv[KnownConfigNames.SuppressCliRunHook]);
}
[Fact]
public async Task RunAsyncSuppressesCliRunHookForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
Dictionary<string, string>? capturedEnv = null;
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, env, _, _) =>
{
capturedEnv = env?.ToDictionary();
},
0);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.NotNull(capturedEnv);
Assert.Equal("true", capturedEnv[KnownConfigNames.SuppressCliRunHook]);
}
[Fact]
public async Task RunAsyncDoesNotIncludeNoBuildFlagForSingleFileAppHostWhenNoBuildIsTrue()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
Assert.Collection(args,
arg => Assert.Equal("run", arg),
arg => Assert.Equal($"/p:{KnownConfigNames.SuppressCliRunHook}=true", arg),
arg => Assert.Equal("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
Assert.DoesNotContain("--no-build", args);
},
0);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: true,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
new ProcessInvocationOptions(),
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncFiltersOutEmptyAndWhitespaceArguments()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
// Use watch=true and NoLaunchProfile=false to ensure some empty strings are generated
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false,
Debug = false
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify no empty or whitespace-only arguments exist
foreach (var arg in args)
{
Assert.False(string.IsNullOrWhiteSpace(arg), $"Found empty or whitespace argument in args: [{string.Join(", ", args)}]");
}
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: true, // This will generate empty strings for verboseSwitch when Debug=false
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false // This will generate an empty string for noProfileSwitch
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// Verify no empty or whitespace-only arguments exist in single-file AppHost scenario
foreach (var arg in args)
{
Assert.False(string.IsNullOrWhiteSpace(arg), $"Found empty or whitespace argument in args: [{string.Join(", ", args)}]");
}
// Ensure the correct arguments are present
Assert.Collection(args,
arg => Assert.Equal("run", arg),
arg => Assert.Equal($"/p:{KnownConfigNames.SuppressCliRunHook}=true", arg),
arg => Assert.Equal("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0);
var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncIncludesAllNonEmptyFlagsWhenEnabled()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true,
Debug = true
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// With watch=true and Debug=true, should include --verbose
Assert.Collection(args,
arg => Assert.Equal("watch", arg),
arg => Assert.Equal("--non-interactive", arg),
arg => Assert.Equal("--verbose", arg),
arg => Assert.Equal("--no-launch-profile", arg),
arg => Assert.Equal("--project", arg),
arg => Assert.Equal(projectFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: true,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncCorrectlyHandlesWatchWithoutDebug()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true,
Debug = false // No debug, so no --verbose
};
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, _) =>
{
// With watch=true but Debug=false, should NOT include --verbose
Assert.Collection(args,
arg => Assert.Equal("watch", arg),
arg => Assert.Equal("--non-interactive", arg),
arg => Assert.Equal("--no-launch-profile", arg),
arg => Assert.Equal("--project", arg),
arg => Assert.Equal(projectFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: true,
noBuild: false,
noRestore: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task GetProjectItemsAndPropertiesAsync_UsesBuild_ForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
// Verify that "build" command is used for single-file app host
Assert.Contains("build", args);
Assert.DoesNotContain("msbuild", args);
// Provide valid JSON output
invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}");
},
0);
await runner.GetProjectItemsAndPropertiesAsync(
appHostFile,
["PackageReference"],
["AspireHostingSDKVersion"],
targets: [],
options,
CancellationToken.None
);
}
[Fact]
public async Task GetProjectItemsAndPropertiesAsyncSuppressesCliRunHook()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
Dictionary<string, string>? capturedEnv = null;
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, env, _, invocationOptions) =>
{
capturedEnv = env?.ToDictionary();
invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}");
},
0);
await runner.GetProjectItemsAndPropertiesAsync(
projectFile,
["PackageReference"],
["AspireHostingSDKVersion"],
targets: [],
new ProcessInvocationOptions(),
CancellationToken.None
);
Assert.NotNull(capturedEnv);
Assert.Equal("true", capturedEnv[KnownConfigNames.SuppressCliRunHook]);
}
[Fact]
public async Task GetAppHostInformationAsync_UsesAspireHostingAppHostPackageVersion()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
Assert.Contains("-getItem:PackageReference,AspireProjectOrPackageReference,PackageVersion", args);
invocationOptions.StandardOutputCallback?.Invoke("""
{
"Properties": {
"MSBuildVersion": "17.0.0",
"IsAspireHost": "true"
},
"Items": {
"PackageReference": [
{
"Identity": "Aspire.Hosting.AppHost",
"Version": "13.0.0"
}
]
}
}
""");
},
0);
var result = await runner.GetAppHostInformationAsync(projectFile, new ProcessInvocationOptions(), CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.True(result.IsAspireHost);
Assert.Equal("13.0.0", result.AspireHostingVersion);
}
[Fact]
public async Task GetAppHostInformationAsync_UsesAspireHostingAppHostCentralPackageVersion()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(_, _, _, invocationOptions) =>
{
invocationOptions.StandardOutputCallback?.Invoke("""
{
"Properties": {
"MSBuildVersion": "17.0.0",
"IsAspireHost": "true"
},
"Items": {
"PackageVersion": [
{
"Identity": "Aspire.Hosting.AppHost",
"Version": "13.0.1"
}
]
}
}
""");
},
0);
var result = await runner.GetAppHostInformationAsync(projectFile, new ProcessInvocationOptions(), CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.True(result.IsAspireHost);
Assert.Equal("13.0.1", result.AspireHostingVersion);
}
[Fact]
public async Task GetProjectItemsAndPropertiesAsync_UsesMsBuild_ForCsProjFile()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, invocationOptions) =>
{
// Verify that "msbuild" command is used for .csproj files
Assert.Contains("msbuild", args);
Assert.DoesNotContain("build", args);
Assert.NotNull(env);
Assert.Equal("1", env["DOTNET_CLI_TELEMETRY_OPTOUT"]);
Assert.Equal("1", env["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE"]);
// Provide valid JSON output
invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}");
},
0);
await runner.GetProjectItemsAndPropertiesAsync(
projectFile,
["PackageReference"],
["AspireHostingSDKVersion"],
targets: [],
options,
CancellationToken.None
);
}
[Fact]
public async Task GetProjectItemsAndPropertiesAsync_EmitsTargetSwitch_WhenTargetsRequested()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
// The Run* properties need ComputeRunArguments to have executed before
// -getProperty reads them, so the runner must add -t:ComputeRunArguments.
Assert.Contains("-t:ComputeRunArguments;Build", args);
invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"RunCommand\":\"/bin/AppHost\"},\"Items\":{}}");
},
0);
var (exitCode, output) = await runner.GetProjectItemsAndPropertiesAsync(
projectFile,
items: [],
properties: ["RunCommand"],
targets: ["ComputeRunArguments", "Build"],
options,
CancellationToken.None
);
Assert.Equal(0, exitCode);
Assert.NotNull(output);
}
[Fact]
public async Task ComputeRunArgumentsPopulatesRunPropertiesForSdkProject()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "RunPropertiesApp"));
var projectFile = new FileInfo(Path.Combine(projectDirectory.FullName, "RunPropertiesApp.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<StartArguments>--from-msbuild</StartArguments>
<RunWorkingDirectory>relative-working-directory</RunWorkingDirectory>
</PropertyGroup>
</Project>
""");
await File.WriteAllTextAsync(Path.Combine(projectDirectory.FullName, "Program.cs"), "Console.WriteLine(\"Hello\");");
var processStartInfo = new ProcessStartInfo(GetDotNetExecutablePath())
{
WorkingDirectory = projectDirectory.FullName,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
processStartInfo.ArgumentList.Add("msbuild");
processStartInfo.ArgumentList.Add(projectFile.FullName);
processStartInfo.ArgumentList.Add("-restore");
processStartInfo.ArgumentList.Add("-t:ComputeRunArguments");
processStartInfo.ArgumentList.Add("-getProperty:RunCommand,RunArguments,RunWorkingDirectory");
processStartInfo.ArgumentList.Add("-nologo");
using var process = Process.Start(processStartInfo) ?? throw new InvalidOperationException("Failed to start dotnet msbuild.");
var stdout = await process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken);
var stderr = await process.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken);
await process.WaitForExitAsync(TestContext.Current.CancellationToken);
Assert.Equal(0, process.ExitCode);
using var jsonDocument = JsonDocument.Parse(stdout);
var properties = jsonDocument.RootElement.GetProperty("Properties");
Assert.False(string.IsNullOrWhiteSpace(properties.GetProperty("RunCommand").GetString()));
Assert.Equal("--from-msbuild", properties.GetProperty("RunArguments").GetString());
Assert.Equal(Path.Combine(projectDirectory.FullName, "relative-working-directory"), properties.GetProperty("RunWorkingDirectory").GetString());
Assert.True(string.IsNullOrWhiteSpace(stderr));
}
[Fact]
public async Task GetProjectItemsAndPropertiesAsync_OmitsTargetSwitch_WhenNoTargetsRequested()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
// No targets requested means no -t: switch should be added so MSBuild only
// evaluates the project (the existing fast property/item probe behavior).
Assert.DoesNotContain(args, a => a.StartsWith("-t:", StringComparison.Ordinal));
invocationOptions.StandardOutputCallback?.Invoke("{\"Properties\":{\"MSBuildVersion\":\"17.0.0\",\"AspireHostingSDKVersion\":\"9.0.0\"},\"Items\":{\"PackageReference\":[]}}");
},
0);
await runner.GetProjectItemsAndPropertiesAsync(
projectFile,
["PackageReference"],
["AspireHostingSDKVersion"],
targets: [],
options,
CancellationToken.None
);
}
[Fact]
public async Task SearchPackagesAsyncRetriesOnFailureAndSucceedsOnSecondAttempt()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var (runner, executor) = DotNetCliRunnerTestHelper.CreateWithRetry(
provider,
executionContext,
(attempt, options) =>
{
// First attempt fails, second succeeds
if (attempt == 1)
{
return (1, null);
}
return (0, "{\"version\":1,\"searchResult\":[{\"sourceName\":\"nuget.org\",\"packages\":[{\"id\":\"Aspire.Hosting.Redis\",\"latestVersion\":\"9.0.0\"}]}]}");
},
logger: logger
);
var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
"Aspire.Hosting",
exactMatch: false,
prerelease: false,
take: 100,
skip: 0,
nugetConfigFile: null,
useCache: false,
options,
CancellationToken.None
);
Assert.Equal(0, result.ExitCode);
Assert.NotNull(result.Packages);
Assert.Equal(2, executor.AttemptCount);
}
[Fact]
public async Task SearchPackagesAsyncRetriesMaxTimesAndReturnsFailure()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var (runner, executor) = DotNetCliRunnerTestHelper.CreateWithRetry(
provider,
executionContext,
(attempt, options) =>
{
// Always fail
return (1, null);
},
logger: logger
);
var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
"Aspire.Hosting",
exactMatch: false,
prerelease: false,
take: 100,
skip: 0,
nugetConfigFile: null,
useCache: false,
options,
CancellationToken.None
);
Assert.Equal(1, result.ExitCode);
Assert.Null(result.Packages);
Assert.Equal(3, executor.AttemptCount); // Should have attempted 3 times
}
[Fact]
public async Task SearchPackagesAsyncSucceedsOnFirstAttemptWithoutRetry()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var (runner, executor) = DotNetCliRunnerTestHelper.CreateWithRetry(
provider,
executionContext,
(attempt, options) =>
{
// Always succeed
return (0, "{\"version\":1,\"searchResult\":[{\"sourceName\":\"nuget.org\",\"packages\":[{\"id\":\"Aspire.Hosting.Redis\",\"latestVersion\":\"9.0.0\"}]}]}");
},
logger: logger
);
var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
"Aspire.Hosting",
exactMatch: false,
prerelease: false,
take: 100,
skip: 0,
nugetConfigFile: null,
useCache: false,
options,
CancellationToken.None
);
Assert.Equal(0, result.ExitCode);
Assert.NotNull(result.Packages);
Assert.Equal(1, executor.AttemptCount); // Should have attempted only once
}
[Fact]
public async Task SearchPackagesAsync_WithExactMatch_UsesExactMatchFlagWithoutPaginationArguments()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions { SuppressLogging = true };
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, _, invocationOptions) =>
{
Assert.Contains("package", args);
Assert.Contains("search", args);
Assert.Contains("Aspire.Hosting.Redis", args);
Assert.Contains("--exact-match", args);
Assert.DoesNotContain("--take", args);
Assert.DoesNotContain("--skip", args);
invocationOptions.StandardOutputCallback?.Invoke(
"""
{"version":1,"searchResult":[{"sourceName":"nuget.org","packages":[{"id":"Aspire.Hosting.Redis","latestVersion":"13.3.0"}]}]}
""");
},
0);
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
"Aspire.Hosting.Redis",
exactMatch: true,
prerelease: false,
take: 100,
skip: 200,
nugetConfigFile: null,
useCache: false,
options,
CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.Collection(
result.Packages!,
package =>
{
Assert.Equal("Aspire.Hosting.Redis", package.Id);
Assert.Equal("13.3.0", package.Version);
});
}
[Fact]
public async Task InstallTemplateAsync_UsesLocalPackagePathForLocalFolderSource()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var packageVersion = "13.3.0-local.1";
var packagesDirectory = workspace.WorkspaceRoot.CreateSubdirectory("packages");
var packagePath = Path.Combine(packagesDirectory.FullName, $"Aspire.ProjectTemplates.{packageVersion}.nupkg");
await File.WriteAllTextAsync(packagePath, string.Empty);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var invocationCount = 0;
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, _, workingDirectory, _) =>
{
invocationCount++;
Assert.Equal(workspace.WorkspaceRoot.FullName, workingDirectory.FullName);
Assert.Equal("new", args[0]);
switch (invocationCount)
{
case 1:
Assert.Equal("uninstall", args[1]);
Assert.Equal("Aspire.ProjectTemplates", args[2]);
Assert.DoesNotContain("--force", args);
Assert.DoesNotContain("--nuget-source", args);
break;
case 2:
Assert.Equal("install", args[1]);
Assert.Equal(packagePath, args[2]);
Assert.DoesNotContain("--force", args);
Assert.DoesNotContain("--nuget-source", args);
break;
default:
Assert.Fail($"Unexpected dotnet invocation {invocationCount}.");
break;
}
},
0);
var result = await runner.InstallTemplateAsync(
"Aspire.ProjectTemplates",
packageVersion,
nugetConfigFile: null,
nugetSource: "packages",
force: true,
new ProcessInvocationOptions(),
CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.Equal(packageVersion, result.TemplateVersion);
Assert.Equal(2, invocationCount);
}
[Theory]
[InlineData("Success: Aspire.ProjectTemplates@13.2.0-preview.1.26101.12 installed the following templates:", true, "13.2.0-preview.1.26101.12")] // New .NET 10.0 SDK format with @ separator
[InlineData("Success: Aspire.ProjectTemplates::13.2.0-preview.1.26101.12 installed the following templates:", true, "13.2.0-preview.1.26101.12")] // Old SDK format with :: separator
[InlineData("Some other output", false, null)] // Missing success line
[InlineData("Success: Aspire.ProjectTemplates installed the following templates:", false, null)] // Invalid format without version separator
public void TryParsePackageVersionFromStdout_ParsesCorrectly(string stdout, bool expectedResult, string? expectedVersion)
{
// Act
var result = DotNetCliRunner.TryParsePackageVersionFromStdout(stdout, out var version);
// Assert
Assert.Equal(expectedResult, result);
Assert.Equal(expectedVersion, version);
}
[Fact]
public async Task RunAsyncIncludesNoBuildFlagWhenNoBuildIsTrue()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-build is included when noBuild is true
Assert.Contains("run", args);
Assert.Contains("--no-build", args);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: true, // This should add --no-build
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotIncludeNoBuildFlagWhenNoBuildIsFalse()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-build is NOT included when noBuild is false
Assert.Contains("run", args);
Assert.DoesNotContain("--no-build", args);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false, // This should NOT add --no-build
noRestore: false,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncIncludesNoRestoreFlagWhenNoRestoreIsTrueAndNoBuildIsFalse()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-restore is included when noRestore is true and noBuild is false
Assert.Contains("run", args);
Assert.Contains("--no-restore", args);
Assert.DoesNotContain("--no-build", args);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: false,
noRestore: true, // This should add --no-restore
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncDoesNotIncludeNoRestoreFlagWhenNoBuildIsTrue()
{
// --no-build implies --no-restore, so we should not include --no-restore when --no-build is specified
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
// Verify that --no-restore is NOT included when noBuild is true (because --no-build implies --no-restore)
Assert.Contains("run", args);
Assert.Contains("--no-build", args);
Assert.DoesNotContain("--no-restore", args);
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: true, // --no-build implies --no-restore
noRestore: true, // This should be ignored because noBuild is true
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RunAsyncSetsCliLogFilePathEnvironmentVariable()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file.");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var options = new ProcessInvocationOptions();
Dictionary<string, string>? capturedEnv = null;
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
provider,
executionContext,
(args, env, _, _) =>
{
capturedEnv = env?.ToDictionary();
},
0);
var exitCode = await runner.RunAsync(
projectFile: projectFile,
watch: false,
noBuild: true,
noRestore: true,
args: ["--operation", "inspect"],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.NotNull(capturedEnv);
Assert.Equal(executionContext.LogFilePath, capturedEnv[KnownConfigNames.CliLogFilePath]);
}
private sealed class ExitedProcessExecution(
IReadOnlyList<string> arguments,
IDictionary<string, string>? environmentVariables,
int exitCode) : IProcessExecution
{
private bool _started;
public string FileName => "dotnet";
public IReadOnlyList<string> Arguments { get; } = arguments;
public IReadOnlyDictionary<string, string?> EnvironmentVariables { get; } = environmentVariables?.ToDictionary(kvp => kvp.Key, kvp => (string?)kvp.Value)
?? new Dictionary<string, string?>();
public int ProcessId => Environment.ProcessId;
public DateTimeOffset? StartTime => DateTimeOffset.UtcNow;
public bool HasExited => _started;
public int ExitCode => exitCode;
public Task<bool> StartAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_started = true;
return Task.FromResult(true);
}
public Task<int> WaitForExitAsync(CancellationToken cancellationToken) => Task.FromResult(exitCode);
public void Kill(bool entireProcessTree)
{
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}