From a081a11588fa9047850c1a101de1dff46670e8d8 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 10 Oct 2025 11:04:02 -0700 Subject: [PATCH 1/5] Add noProfileSwitch to run command in DotNetCliRunner --- src/Aspire.Cli/DotNet/DotNetCliRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index e99b01a6991..87de0e4927c 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -248,7 +248,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] cliArgs = isSingleFile switch { false => [watchOrRunCommand, nonInteractiveSwitch, verboseSwitch, noBuildSwitch, noProfileSwitch, "--project", projectFile.FullName, "--", .. args], - true => ["run", projectFile.FullName, "--", ..args] + true => ["run", noProfileSwitch, projectFile.FullName, "--", ..args] }; // Inject DOTNET_CLI_USE_MSBUILD_SERVER when noBuild == false - we copy the From 79c67e07331cac4fedfeba2f710e05953826b10f Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 10 Oct 2025 11:45:47 -0700 Subject: [PATCH 2/5] Filter out empty CLI arguments in RunAsync method --- src/Aspire.Cli/DotNet/DotNetCliRunner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 87de0e4927c..043a58f90aa 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -248,9 +248,11 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] cliArgs = isSingleFile switch { false => [watchOrRunCommand, nonInteractiveSwitch, verboseSwitch, noBuildSwitch, noProfileSwitch, "--project", projectFile.FullName, "--", .. args], - true => ["run", noProfileSwitch, projectFile.FullName, "--", ..args] + true => ["run", noProfileSwitch, 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? finalEnv = env; From e6c2ded2862d761c0317858fc32597367c4b3b6b Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 10 Oct 2025 11:48:59 -0700 Subject: [PATCH 3/5] Add tests for RunAsync method to validate NoLaunchProfile handling and argument filtering --- .../DotNet/DotNetCliRunnerTests.cs | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) diff --git a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs index 2d752faed93..de016893748 100644 --- a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs +++ b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs @@ -863,6 +863,341 @@ public async Task AddProjectReferenceAsync_ExecutesCorrectCommand() 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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + var options = new DotNetCliRunnerInvocationOptions() + { + NoLaunchProfile = true + }; + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + var runner = new AssertingDotNetCliRunner( + logger, + provider, + new AspireCliTelemetry(), + provider.GetRequiredService(), + provider.GetRequiredService(), + interactionService, + executionContext, + new NullDiskCache(), + (args, _, _, _, _, _) => + { + // For single-file .cs files, should include --no-launch-profile + Assert.Contains("run", args); + Assert.Contains("--no-launch-profile", args); + Assert.Contains(appHostFile.FullName, args); + }, + 0 + ); + + var exitCode = await runner.RunAsync( + projectFile: appHostFile, + watch: false, + noBuild: false, + args: [], + env: new Dictionary(), + 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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + var options = new DotNetCliRunnerInvocationOptions() + { + NoLaunchProfile = false + }; + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + var runner = new AssertingDotNetCliRunner( + logger, + provider, + new AspireCliTelemetry(), + provider.GetRequiredService(), + provider.GetRequiredService(), + interactionService, + executionContext, + new NullDiskCache(), + (args, _, _, _, _, _) => + { + // For single-file .cs files, should NOT include --no-launch-profile when false + Assert.Contains("run", args); + Assert.DoesNotContain("--no-launch-profile", args); + Assert.Contains(appHostFile.FullName, args); + }, + 0 + ); + + var exitCode = await runner.RunAsync( + projectFile: appHostFile, + watch: false, + noBuild: false, + args: [], + env: new Dictionary(), + null, + options, + CancellationToken.None + ); + + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task RunAsyncFiltersOutEmptyAndWhitespaceArguments() + { + using var workspace = TemporaryWorkspace.Create(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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + // Use watch=true and NoLaunchProfile=false to ensure some empty strings are generated + var options = new DotNetCliRunnerInvocationOptions() + { + NoLaunchProfile = false, + Debug = false + }; + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + var runner = new AssertingDotNetCliRunner( + logger, + provider, + new AspireCliTelemetry(), + provider.GetRequiredService(), + provider.GetRequiredService(), + interactionService, + executionContext, + new NullDiskCache(), + (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, + args: [], + env: new Dictionary(), + 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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + 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(), + provider.GetRequiredService(), + 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.Contains("run", args); + Assert.Contains(appHostFile.FullName, args); + Assert.Contains("--", args); + }, + 0 + ); + + var exitCode = await runner.RunAsync( + projectFile: appHostFile, + watch: false, + noBuild: false, + args: [], + env: new Dictionary(), + null, + options, + CancellationToken.None + ); + + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task RunAsyncIncludesAllNonEmptyFlagsWhenEnabled() + { + using var workspace = TemporaryWorkspace.Create(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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + var options = new DotNetCliRunnerInvocationOptions() + { + NoLaunchProfile = true, + Debug = true + }; + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + var runner = new AssertingDotNetCliRunner( + logger, + provider, + new AspireCliTelemetry(), + provider.GetRequiredService(), + provider.GetRequiredService(), + interactionService, + executionContext, + new NullDiskCache(), + (args, _, _, _, _, _) => + { + // With watch=true and Debug=true, should include --verbose + Assert.Contains("watch", args); + Assert.Contains("--non-interactive", args); + Assert.Contains("--verbose", args); + Assert.Contains("--no-launch-profile", args); + Assert.Contains("--project", args); + Assert.Contains(projectFile.FullName, 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, + noBuild: false, + args: [], + env: new Dictionary(), + null, + options, + CancellationToken.None + ); + + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task RunAsyncCorrectlyHandlesWatchWithoutDebug() + { + using var workspace = TemporaryWorkspace.Create(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); + var provider = services.BuildServiceProvider(); + var logger = provider.GetRequiredService>(); + var interactionService = provider.GetRequiredService(); + + var options = new DotNetCliRunnerInvocationOptions() + { + NoLaunchProfile = true, + Debug = false // No debug, so no --verbose + }; + + var executionContext = CreateExecutionContext(workspace.WorkspaceRoot); + var runner = new AssertingDotNetCliRunner( + logger, + provider, + new AspireCliTelemetry(), + provider.GetRequiredService(), + provider.GetRequiredService(), + interactionService, + executionContext, + new NullDiskCache(), + (args, _, _, _, _, _) => + { + // With watch=true but Debug=false, should NOT include --verbose + Assert.Contains("watch", args); + Assert.Contains("--non-interactive", args); + Assert.DoesNotContain("--verbose", args); + Assert.Contains("--no-launch-profile", args); + Assert.Contains("--project", args); + Assert.Contains(projectFile.FullName, 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, + noBuild: false, + args: [], + env: new Dictionary(), + null, + options, + CancellationToken.None + ); + + Assert.Equal(0, exitCode); + } } internal sealed class AssertingDotNetCliRunner( From dc022a895579ccc2f5ccc475aa16baa7428c97ef Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 10 Oct 2025 17:00:33 -0700 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Damian Edwards --- src/Aspire.Cli/DotNet/DotNetCliRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 043a58f90aa..d267234b9a4 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -248,7 +248,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] cliArgs = isSingleFile switch { false => [watchOrRunCommand, nonInteractiveSwitch, verboseSwitch, noBuildSwitch, noProfileSwitch, "--project", projectFile.FullName, "--", .. args], - true => ["run", noProfileSwitch, projectFile.FullName, "--", .. args] + true => ["run", noProfileSwitch, "--file", projectFile.FullName, "--", .. args] }; cliArgs = [.. cliArgs.Where(arg => !string.IsNullOrWhiteSpace(arg))]; From 76d640cb53071027d949b45a43260d8332d25544 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 10 Oct 2025 21:04:09 -0700 Subject: [PATCH 5/5] Refactor RunAsync tests to improve argument assertions and remove unnecessary whitespace --- .../DotNet/DotNetCliRunnerTests.cs | 119 ++++++++++-------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs index de016893748..d96879bb302 100644 --- a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs +++ b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs @@ -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] @@ -380,7 +380,7 @@ public async Task RunAsyncSetsVersionCheckDisabledWhenUpdateNotificationsFeature provider.GetRequiredService(), provider.GetRequiredService(), new NullDiskCache(), - (args, env, _, _, _, _) => + (args, env, _, _, _, _) => { Assert.NotNull(env); Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED")); @@ -428,7 +428,7 @@ public async Task RunAsyncDoesNotSetVersionCheckDisabledWhenUpdateNotificationsF provider.GetRequiredService(), provider.GetRequiredService(), new NullDiskCache(), - (args, env, _, _, _, _) => + (args, env, _, _, _, _) => { // When the feature is enabled (default), the version check env var should NOT be set if (env != null) @@ -478,7 +478,7 @@ public async Task RunAsyncDoesNotOverrideUserProvidedVersionCheckDisabledValue() provider.GetRequiredService(), provider.GetRequiredService(), new NullDiskCache(), - (args, env, _, _, _, _) => + (args, env, _, _, _, _) => { Assert.NotNull(env); Assert.True(env.ContainsKey("ASPIRE_VERSION_CHECK_DISABLED")); @@ -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>(); var interactionService = provider.GetRequiredService(); @@ -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); @@ -659,7 +662,7 @@ 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); @@ -667,13 +670,13 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFile() 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); @@ -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); @@ -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); @@ -768,7 +771,7 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestor public async Task GetSolutionProjectsAsync_ParsesOutputCorrectly() { using var workspace = TemporaryWorkspace.Create(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."); @@ -825,7 +828,7 @@ public async Task GetSolutionProjectsAsync_ParsesOutputCorrectly() public async Task AddProjectReferenceAsync_ExecutesCorrectCommand() { using var workspace = TemporaryWorkspace.Create(outputHelper); - + var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj")); await File.WriteAllTextAsync(projectFile.FullName, "Not a real project file."); @@ -871,7 +874,10 @@ public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost() 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>(); var interactionService = provider.GetRequiredService(); @@ -894,9 +900,13 @@ public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost() (args, _, _, _, _, _) => { // For single-file .cs files, should include --no-launch-profile - Assert.Contains("run", args); - Assert.Contains("--no-launch-profile", args); - Assert.Contains(appHostFile.FullName, args); + 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 ); @@ -922,7 +932,10 @@ public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenN 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>(); var interactionService = provider.GetRequiredService(); @@ -945,9 +958,12 @@ public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenN (args, _, _, _, _, _) => { // For single-file .cs files, should NOT include --no-launch-profile when false - Assert.Contains("run", args); - Assert.DoesNotContain("--no-launch-profile", args); - Assert.Contains(appHostFile.FullName, args); + Assert.Collection(args, + arg => Assert.Equal("run", arg), + arg => Assert.Equal("--file", arg), + arg => Assert.Equal(appHostFile.FullName, arg), + arg => Assert.Equal("--", arg) + ); }, 0 ); @@ -1027,7 +1043,10 @@ public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost() 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>(); var interactionService = provider.GetRequiredService(); @@ -1054,11 +1073,14 @@ public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost() { Assert.False(string.IsNullOrWhiteSpace(arg), $"Found empty or whitespace argument in args: [{string.Join(", ", args)}]"); } - + // Ensure the correct arguments are present - Assert.Contains("run", args); - Assert.Contains(appHostFile.FullName, args); - Assert.Contains("--", args); + Assert.Collection(args, + arg => Assert.Equal("run", arg), + arg => Assert.Equal("--file", arg), + arg => Assert.Equal(appHostFile.FullName, arg), + arg => Assert.Equal("--", arg) + ); }, 0 ); @@ -1108,18 +1130,15 @@ public async Task RunAsyncIncludesAllNonEmptyFlagsWhenEnabled() (args, _, _, _, _, _) => { // With watch=true and Debug=true, should include --verbose - Assert.Contains("watch", args); - Assert.Contains("--non-interactive", args); - Assert.Contains("--verbose", args); - Assert.Contains("--no-launch-profile", args); - Assert.Contains("--project", args); - Assert.Contains(projectFile.FullName, 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)}]"); - } + 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 ); @@ -1169,18 +1188,14 @@ public async Task RunAsyncCorrectlyHandlesWatchWithoutDebug() (args, _, _, _, _, _) => { // With watch=true but Debug=false, should NOT include --verbose - Assert.Contains("watch", args); - Assert.Contains("--non-interactive", args); - Assert.DoesNotContain("--verbose", args); - Assert.Contains("--no-launch-profile", args); - Assert.Contains("--project", args); - Assert.Contains(projectFile.FullName, 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)}]"); - } + 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 );