Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Aspire.Cli/DotNet/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,11 @@ public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild,
string[] cliArgs = isSingleFile switch
{
false => [watchOrRunCommand, noBuildSwitch, noProfileSwitch, "--project", projectFile.FullName, "--", .. args],
true => ["run", projectFile.FullName, "--", ..args]
true => ["run", noProfileSwitch, "--file", projectFile.FullName, "--", .. args]
};


cliArgs = [.. cliArgs.Where(arg => !string.IsNullOrWhiteSpace(arg))];

// Inject DOTNET_CLI_USE_MSBUILD_SERVER when noBuild == false - we copy the
// dictionary here because we don't want to mutate the input.
IDictionary<string, string>? finalEnv = env;
Expand Down
207 changes: 194 additions & 13 deletions tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static Aspire.Cli.CliExecutionContext CreateExecutionContext(DirectoryIn
var settingsDirectory = workingDirectory.CreateSubdirectory(".aspire");
var hivesDirectory = settingsDirectory.CreateSubdirectory("hives");
var cacheDirectory = new DirectoryInfo(Path.Combine(workingDirectory.FullName, ".aspire", "cache"));
return new CliExecutionContext(workingDirectory, hivesDirectory, cacheDirectory);
return new CliExecutionContext(workingDirectory, hivesDirectory, cacheDirectory);
}

[Fact]
Expand Down Expand Up @@ -380,7 +380,7 @@ public async Task RunAsyncSetsVersionCheckDisabledWhenUpdateNotificationsFeature
provider.GetRequiredService<IInteractionService>(),
provider.GetRequiredService<CliExecutionContext>(),
new NullDiskCache(),
(args, env, _, _, _, _) =>
(args, env, _, _, _, _) =>
{
Assert.NotNull(env);
Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED"));
Expand Down Expand Up @@ -428,7 +428,7 @@ public async Task RunAsyncDoesNotSetVersionCheckDisabledWhenUpdateNotificationsF
provider.GetRequiredService<IInteractionService>(),
provider.GetRequiredService<CliExecutionContext>(),
new NullDiskCache(),
(args, env, _, _, _, _) =>
(args, env, _, _, _, _) =>
{
// When the feature is enabled (default), the version check env var should NOT be set
if (env != null)
Expand Down Expand Up @@ -478,7 +478,7 @@ public async Task RunAsyncDoesNotOverrideUserProvidedVersionCheckDisabledValue()
provider.GetRequiredService<IInteractionService>(),
provider.GetRequiredService<CliExecutionContext>(),
new NullDiskCache(),
(args, env, _, _, _, _) =>
(args, env, _, _, _, _) =>
{
Assert.NotNull(env);
Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED"));
Expand Down Expand Up @@ -570,7 +570,10 @@ public async Task AddPackageAsyncUseFilesSwitchForSingleFileAppHost()
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);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
Expand All @@ -596,14 +599,14 @@ public async Task AddPackageAsyncUseFilesSwitchForSingleFileAppHost()
Assert.Contains(appHostFile.FullName, args);
Assert.Contains("Aspire.Hosting.Redis@9.2.0", args);
Assert.Contains("--no-restore", args);

// Verify the order: add package PackageName --file FilePath --version Version --no-restore
var addIndex = Array.IndexOf(args, "add");
var packageIndex = Array.IndexOf(args, "package");
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(addIndex < packageIndex);
Assert.True(packageIndex < fileIndex);
Assert.True(fileIndex < filePathIndex);
Expand Down Expand Up @@ -659,21 +662,21 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFile()
Assert.Contains("9.2.0", args);
Assert.Contains("--source", args);
Assert.Contains("https://api.nuget.org/v3/index.json", args);

// Verify the order: add ProjectFile package PackageName --version Version --source Source
var addIndex = Array.IndexOf(args, "add");
var projectIndex = Array.IndexOf(args, projectFile.FullName);
var packageIndex = Array.IndexOf(args, "package");
var packageNameIndex = Array.IndexOf(args, "Aspire.Hosting.Redis");
var versionFlagIndex = Array.IndexOf(args, "--version");
var versionValueIndex = Array.IndexOf(args, "9.2.0");

Assert.True(addIndex < projectIndex);
Assert.True(projectIndex < packageIndex);
Assert.True(packageIndex < packageNameIndex);
Assert.True(packageNameIndex < versionFlagIndex);
Assert.True(versionFlagIndex < versionValueIndex);

// Should NOT contain --file or the @version format
Assert.DoesNotContain("--file", args);
Assert.DoesNotContain("Aspire.Hosting.Redis@9.2.0", args);
Expand Down Expand Up @@ -727,7 +730,7 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestor
Assert.Contains("--version", args);
Assert.Contains("9.2.0", args);
Assert.Contains("--no-restore", args);

// Verify the order: add ProjectFile package PackageName --version Version --no-restore
var addIndex = Array.IndexOf(args, "add");
var projectIndex = Array.IndexOf(args, projectFile.FullName);
Expand All @@ -736,14 +739,14 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestor
var versionFlagIndex = Array.IndexOf(args, "--version");
var versionValueIndex = Array.IndexOf(args, "9.2.0");
var noRestoreIndex = Array.IndexOf(args, "--no-restore");

Assert.True(addIndex < projectIndex);
Assert.True(projectIndex < packageIndex);
Assert.True(packageIndex < packageNameIndex);
Assert.True(packageNameIndex < versionFlagIndex);
Assert.True(versionFlagIndex < versionValueIndex);
Assert.True(versionValueIndex < noRestoreIndex);

// Should NOT contain --file, --source, or the @version format
Assert.DoesNotContain("--file", args);
Assert.DoesNotContain("--source", args);
Expand All @@ -763,6 +766,184 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestor

Assert.Equal(0, exitCode);
}

[Fact]
public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.Create(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, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();

var options = new DotNetCliRunnerInvocationOptions()
{
NoLaunchProfile = true
};

var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = new AssertingDotNetCliRunner(
logger,
provider,
new AspireCliTelemetry(),
provider.GetRequiredService<IConfiguration>(),
provider.GetRequiredService<IFeatures>(),
interactionService,
executionContext,
new NullDiskCache(),
(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("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0
);

var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None
);

Assert.Equal(0, exitCode);
}

[Fact]
public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenNotSpecified()
{
using var workspace = TemporaryWorkspace.Create(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, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();

var options = new DotNetCliRunnerInvocationOptions()
{
NoLaunchProfile = false
};

var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = new AssertingDotNetCliRunner(
logger,
provider,
new AspireCliTelemetry(),
provider.GetRequiredService<IConfiguration>(),
provider.GetRequiredService<IFeatures>(),
interactionService,
executionContext,
new NullDiskCache(),
(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("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0
);

var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None
);

Assert.Equal(0, exitCode);
}

[Fact]
public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost()
{
using var workspace = TemporaryWorkspace.Create(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, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();

var options = new DotNetCliRunnerInvocationOptions()
{
NoLaunchProfile = false // This will generate an empty string for noProfileSwitch
};

var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = new AssertingDotNetCliRunner(
logger,
provider,
new AspireCliTelemetry(),
provider.GetRequiredService<IConfiguration>(),
provider.GetRequiredService<IFeatures>(),
interactionService,
executionContext,
new NullDiskCache(),
(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("--file", arg),
arg => Assert.Equal(appHostFile.FullName, arg),
arg => Assert.Equal("--", arg)
);
},
0
);

var exitCode = await runner.RunAsync(
projectFile: appHostFile,
watch: false,
noBuild: false,
args: [],
env: new Dictionary<string, string>(),
null,
options,
CancellationToken.None
);

Assert.Equal(0, exitCode);
}
}

internal sealed class AssertingDotNetCliRunner(
Expand Down
Loading