Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 20 additions & 0 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,26 @@ private async Task<int> InitializeExistingSolutionAsync(InitContext initContext,
{
var solutionFile = initContext.SelectedSolutionFile!;

// Verify that the solution directory does not contain project files.
// If the solution and a project file are in the same directory, the AppHost
// and ServiceDefaults directories would be created inside that project which
// is not supported.
var solutionDirectory = solutionFile.Directory!;
var projectFilesInSolutionDir = solutionDirectory.EnumerateFiles("*.*proj")
.Where(f => DotNetAppHostProject.s_projectExtensions.Contains(f.Extension, StringComparer.OrdinalIgnoreCase))
.ToList();

if (projectFilesInSolutionDir.Count > 0)
{
InteractionService.DisplayError(
string.Format(
CultureInfo.CurrentCulture,
InitCommandStrings.SolutionAndProjectInSameDirectory,
solutionFile.Name,
projectFilesInSolutionDir[0].Name));
Comment thread
JamesNK marked this conversation as resolved.
Outdated
return ExitCodeConstants.FailedToCreateNewProject;
}

initContext.GetSolutionProjectsOutputCollector = new OutputCollector();
var (getSolutionExitCode, solutionProjects) = await InteractionService.ShowStatusAsync("Reading solution...", async () =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Projects/DotNetAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal sealed class DotNetAppHostProject : IAppHostProject
private readonly Diagnostics.FileLoggerProvider _fileLoggerProvider;

private static readonly string[] s_detectionPatterns = ["*.csproj", "*.fsproj", "*.vbproj", "apphost.cs"];
private static readonly string[] s_projectExtensions = [".csproj", ".fsproj", ".vbproj"];
internal static readonly string[] s_projectExtensions = [".csproj", ".fsproj", ".vbproj"];
Comment thread
JamesNK marked this conversation as resolved.
Outdated

public DotNetAppHostProject(
IDotNetCliRunner runner,
Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/InitCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,7 @@
<data name="ResolvingTemplateVersion" xml:space="preserve">
<value>Resolving template version...</value>
</data>
<data name="SolutionAndProjectInSameDirectory" xml:space="preserve">
<value>The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory.</value>
</data>
</root>
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,103 @@ namespace Aspire.Cli.Tests.Commands;

public class InitCommandTests(ITestOutputHelper outputHelper)
{
[Theory]
[InlineData("Test.csproj")]
[InlineData("Test.fsproj")]
[InlineData("Test.vbproj")]
public async Task InitCommand_WhenSolutionAndProjectInSameDirectory_ReturnsError(string projectFileName)
{
// Arrange
using var workspace = TemporaryWorkspace.Create(outputHelper);

// Create a solution file and a project file in the same directory
var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln"));
File.WriteAllText(solutionFile.FullName, "Fake solution file");

var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, projectFileName));
File.WriteAllText(projectFile.FullName, "<Project />");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DotNetCliRunnerFactory = (sp) =>
{
var runner = new TestDotNetCliRunner();
// GetSolutionProjectsAsync should not be called because the check
// happens before reading solution projects
runner.GetSolutionProjectsAsyncCallback = (_, _, _) =>
{
throw new InvalidOperationException("GetSolutionProjectsAsync should not be called when solution and project are in the same directory.");
};
return runner;
};
});

var serviceProvider = services.BuildServiceProvider();
var initCommand = serviceProvider.GetRequiredService<InitCommand>();

// Act
var parseResult = initCommand.Parse("init");
var exitCode = await parseResult.InvokeAsync().DefaultTimeout();

// Assert
Assert.Equal(ExitCodeConstants.FailedToCreateNewProject, exitCode);
}

[Fact]
public async Task InitCommand_WhenSolutionDirectoryHasNoProjectFiles_Proceeds()
{
// Arrange
using var workspace = TemporaryWorkspace.Create(outputHelper);

// Create a solution file only (no project files in the same directory)
var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln"));
File.WriteAllText(solutionFile.FullName, "Fake solution file");

var getSolutionProjectsCalled = false;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DotNetCliRunnerFactory = (sp) =>
{
var runner = new TestDotNetCliRunner();
runner.GetSolutionProjectsAsyncCallback = (_, _, _) =>
{
getSolutionProjectsCalled = true;
// Return success with no projects - the test verifies the check passed
return (0, Array.Empty<FileInfo>());
};
runner.NewProjectAsyncCallback = (_, _, outputPath, _, _) =>
{
// Create the expected directories so the code can find them
var appHostDir = Path.Combine(outputPath, "Test.AppHost");
var serviceDefaultsDir = Path.Combine(outputPath, "Test.ServiceDefaults");
Directory.CreateDirectory(appHostDir);
Directory.CreateDirectory(serviceDefaultsDir);
File.WriteAllText(Path.Combine(appHostDir, "Test.AppHost.csproj"), "<Project />");
File.WriteAllText(Path.Combine(serviceDefaultsDir, "Test.ServiceDefaults.csproj"), "<Project />");
return 0;
};
return runner;
};
options.PackagingServiceFactory = (sp) =>
{
return new TestPackagingService();
};
});

var serviceProvider = services.BuildServiceProvider();
var initCommand = serviceProvider.GetRequiredService<InitCommand>();

// Act
var parseResult = initCommand.Parse("init");
var exitCode = await parseResult.InvokeAsync().DefaultTimeout();

// Assert - the command should have proceeded past the directory check and created projects
Assert.True(getSolutionProjectsCalled, "GetSolutionProjectsAsync should have been called when no project files are in the solution directory.");
Assert.Equal(ExitCodeConstants.Success, exitCode);
Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.AppHost", "Test.AppHost.csproj")));
Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.ServiceDefaults", "Test.ServiceDefaults.csproj")));
}

[Fact]
public void InitContext_RequiredAppHostFramework_ReturnsHighestTfm()
{
Expand Down
Loading