From b9d21348b088636e783a768dd91d82b6376da816 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 26 Nov 2024 14:41:18 -0800 Subject: [PATCH 01/12] sln-remove: Support for slnx --- .../commands/dotnet-sln/remove/Program.cs | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 1e4ce2e23b95..444b0ecd42c4 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -4,56 +4,64 @@ using System.CommandLine; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Sln.Internal; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Common; +using Microsoft.VisualStudio.SolutionPersistence; +using Microsoft.VisualStudio.SolutionPersistence.Model; namespace Microsoft.DotNet.Tools.Sln.Remove { internal class RemoveProjectFromSolutionCommand : CommandBase { private readonly string _fileOrDirectory; - private readonly IReadOnlyCollection _arguments; + private readonly IReadOnlyCollection _projects; public RemoveProjectFromSolutionCommand(ParseResult parseResult) : base(parseResult) { _fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument); - _arguments = (parseResult.GetValue(SlnRemoveParser.ProjectPathArgument) ?? Array.Empty()).ToList().AsReadOnly(); + _projects = (parseResult.GetValue(SlnRemoveParser.ProjectPathArgument) ?? Array.Empty()).ToList().AsReadOnly(); - SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _arguments, SlnArgumentValidator.CommandType.Remove); + SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Remove); } public override int Execute() { - SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory); + string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory); + if (_projects.Count == 0) + { + throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove); + } - var baseDirectory = PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory); - var relativeProjectPaths = _arguments.Select(p => + IEnumerable fullProjectPaths = _projects.Select(project => { - var fullPath = Path.GetFullPath(p); - return Path.GetRelativePath( - baseDirectory, - Directory.Exists(fullPath) ? - MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : - fullPath - ); + var fullPath = Path.GetFullPath(project); + return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath; }); - bool slnChanged = false; - foreach (var path in relativeProjectPaths) + try { - slnChanged |= slnFile.RemoveProject(path); + RemoveProjectsAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).Wait(); + return 0; } + catch (Exception ex) + { + throw new GracefulException(ex.Message, ex); + } + } - slnFile.RemoveEmptyConfigurationSections(); - - slnFile.RemoveEmptySolutionFolders(); + private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable projectPaths, CancellationToken cancellationToken) + { + ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath); + SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken); - if (slnChanged) + foreach (var project in projectPaths) { - slnFile.Write(); + SolutionProjectModel projectModel = solution.FindProject(project); + solution.RemoveProject(projectModel); } - return 0; + await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } } } From 74d6c77a0078ab4f24f3913c5f20b9c37712edb7 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 2 Dec 2024 14:54:04 -0800 Subject: [PATCH 02/12] WIP --- .../dotnet/commands/dotnet-sln/remove/Program.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 444b0ecd42c4..30735728740f 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -2,10 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Common; +using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.VisualStudio.SolutionPersistence; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -44,7 +47,7 @@ public override int Execute() RemoveProjectsAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).Wait(); return 0; } - catch (Exception ex) + catch (Exception ex) when (ex is not GracefulException) { throw new GracefulException(ex.Message, ex); } @@ -55,9 +58,14 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath); SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken); - foreach (var project in projectPaths) + foreach (var projectPath in projectPaths) { - SolutionProjectModel projectModel = solution.FindProject(project); + // Open project instance to see if it is a valid project + ProjectRootElement projectRootElement = ProjectRootElement.Open(projectPath); + ProjectInstance projectInstance = new ProjectInstance(projectRootElement); + string projectInstanceId = projectInstance.GetProjectId(); + + SolutionProjectModel? projectModel = (SolutionProjectModel?) solution.FindItemById(new Guid(projectInstanceId)); solution.RemoveProject(projectModel); } From 40e0a9fd26b82b3d8d27e8496a7c6de65a7f05a4 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 5 Dec 2024 13:59:54 -0800 Subject: [PATCH 03/12] Added edge cases --- .../commands/dotnet-sln/remove/Program.cs | 49 ++++++++++++++----- test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 47 ++++++++++-------- 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 30735728740f..8f09bfc134d9 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.VisualStudio.SolutionPersistence; using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12; namespace Microsoft.DotNet.Tools.Sln.Remove { @@ -36,19 +37,26 @@ public override int Execute() throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove); } - IEnumerable fullProjectPaths = _projects.Select(project => - { - var fullPath = Path.GetFullPath(project); - return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath; - }); - try { - RemoveProjectsAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).Wait(); + var relativeProjectPaths = _projects.Select(p => + { + var fullPath = Path.GetFullPath(p); + return Path.GetRelativePath( + Path.GetDirectoryName(solutionFileFullPath), + Directory.Exists(fullPath) + ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName + : fullPath); + }); + RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).Wait(); return 0; } catch (Exception ex) when (ex is not GracefulException) { + if (ex is SolutionException || ex.InnerException is SolutionException) + { + throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message); + } throw new GracefulException(ex.Message, ex); } } @@ -58,17 +66,32 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath); SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken); + // set UTF8 BOM encoding for .sln + if (serializer is ISolutionSerializer v12Serializer) + { + solution.SerializerExtension = v12Serializer.CreateModelExtension(new() + { + Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true) + }); + } + foreach (var projectPath in projectPaths) { - // Open project instance to see if it is a valid project - ProjectRootElement projectRootElement = ProjectRootElement.Open(projectPath); - ProjectInstance projectInstance = new ProjectInstance(projectRootElement); - string projectInstanceId = projectInstance.GetProjectId(); + var project = solution.FindProject(projectPath); + if (project != null) + { + solution.RemoveProject(project); - SolutionProjectModel? projectModel = (SolutionProjectModel?) solution.FindItemById(new Guid(projectInstanceId)); - solution.RemoveProject(projectModel); + Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectPath); + } + else + { + Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectNotFoundInTheSolution, projectPath); + } } + // TODO: Remove empty solution folders + await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } } diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index b54349830c48..e2db8964e394 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -52,6 +52,9 @@ dotnet solution remove [...] [options] {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal "; @@ -314,27 +317,31 @@ public void WhenInvalidSolutionIsPassedItPrintsErrorAndUsage(string solutionComm .WithWorkingDirectory(projectDirectory) .Execute(solutionCommand, "InvalidSolution.sln", "remove", projectToRemove); cmd.Should().Fail(); - cmd.StdErr.Should().Be(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, "InvalidSolution.sln", LocalizableStrings.FileHeaderMissingError)); + cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, "InvalidSolution.sln", "*")); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionCommand, string solutionExtension) { - var projectDirectory = _testAssetsManager + var projectDirectoryRoot = _testAssetsManager .CopyTestAsset("InvalidSolution", identifier: $"{solutionCommand}") .WithSource() .Path; + var projectDirectory = solutionExtension == ".sln" + ? Path.Join(projectDirectoryRoot, "Sln") + : Path.Join(projectDirectoryRoot, "Slnx"); + var solutionPath = Path.Combine(projectDirectory, "InvalidSolution.sln"); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) .Execute(solutionCommand, "remove", projectToRemove); cmd.Should().Fail(); - cmd.StdErr.Should().Be(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, solutionPath, LocalizableStrings.FileHeaderMissingError)); + cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, solutionPath, "*")); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } @@ -408,7 +415,7 @@ public void WhenPassedAReferenceNotInSlnItPrintsStatus(string solutionCommand) var contentBefore = File.ReadAllText(solutionPath); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", "referenceDoesNotExistInSln.csproj"); + .Execute(solutionCommand, "App.sln", "remove", "referenceDoesNotExistInSln.csproj"); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectNotFoundInTheSolution, "referenceDoesNotExistInSln.csproj")); File.ReadAllText(solutionPath) @@ -432,7 +439,7 @@ public void WhenPassedAReferenceItRemovesTheReferenceButNotOtherReferences(strin var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectToRemove)); @@ -457,7 +464,7 @@ public void WhenSolutionItemsExistInFolderParentFoldersAreNotRemoved(string solu var projectToRemove = Path.Combine("ConsoleApp1", "ConsoleApp1.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectToRemove)); @@ -484,7 +491,7 @@ public void WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCom var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); string outputText = string.Format(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectToRemove); @@ -513,7 +520,7 @@ public void WhenPassedMultipleReferencesAndOneOfThemDoesNotExistItRemovesTheOneT var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", "idontexist.csproj", projectToRemove, "idontexisteither.csproj"); + .Execute(solutionCommand, "App.sln", "remove", "idontexist.csproj", projectToRemove, "idontexisteither.csproj"); cmd.Should().Pass(); string outputText = $@"{string.Format(CommonLocalizableStrings.ProjectNotFoundInTheSolution, "idontexist.csproj")} @@ -544,7 +551,7 @@ public void WhenReferenceIsRemovedBuildConfigsAreAlsoRemoved(string solutionComm var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); File.ReadAllText(solutionPath) @@ -567,7 +574,7 @@ public void WhenDirectoryContainingProjectIsGivenProjectIsRemoved(string solutio var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", "Lib"); + .Execute(solutionCommand, "App.sln", "remove", "Lib"); cmd.Should().Pass(); File.ReadAllText(solutionPath) @@ -587,7 +594,7 @@ public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string soluti var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", directoryToRemove); + .Execute(solutionCommand, "App.sln", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -609,7 +616,7 @@ public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", directoryToRemove); + .Execute(solutionCommand, "App.sln", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -635,7 +642,7 @@ public void WhenReferenceIsRemovedSlnBuilds(string solutionCommand) var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); new DotnetCommand(Log) @@ -705,7 +712,7 @@ public void WhenFinalReferenceIsRemovedEmptySectionsAreRemoved(string solutionCo var libPath = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", libPath, appPath); + .Execute(solutionCommand, "App.sln", "remove", libPath, appPath); cmd.Should().Pass(); var solutionContents = File.ReadAllText(solutionPath); @@ -728,7 +735,7 @@ public void WhenNestedProjectIsRemovedItsSolutionFoldersAreRemoved(string soluti var projectToRemove = Path.Combine("src", "NotLastProjInSrc", "NotLastProjInSrc.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); File.ReadAllText(solutionPath) @@ -750,7 +757,7 @@ public void WhenFinalNestedProjectIsRemovedSolutionFoldersAreRemoved(string solu var projectToRemove = Path.Combine("src", "Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); File.ReadAllText(solutionPath) @@ -772,7 +779,7 @@ public void WhenProjectIsRemovedThenDependenciesOnProjectAreAlsoRemoved(string s var projectToRemove = Path.Combine("Second", "Second.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", projectToRemove); + .Execute(solutionCommand, "App.sln", "remove", projectToRemove); cmd.Should().Pass(); File.ReadAllText(solutionPath) From 4c50641fab13c366c1685cfde22b1addcbee1f88 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 5 Dec 2024 15:20:16 -0800 Subject: [PATCH 04/12] Fix tests' edge cases --- .../commands/dotnet-sln/remove/Program.cs | 18 +++++++++++++++++- test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 8f09bfc134d9..2b6f6e64d2e5 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -57,6 +57,11 @@ public override int Execute() { throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message); } + // TODO: Check + if (ex.InnerException is GracefulException) + { + throw ex.InnerException; + } throw new GracefulException(ex.Message, ex); } } @@ -81,7 +86,6 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< if (project != null) { solution.RemoveProject(project); - Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectPath); } else @@ -91,6 +95,18 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< } // TODO: Remove empty solution folders + HashSet emptySolutionFolders = solution.SolutionFolders.ToHashSet(); + foreach (var item in solution.SolutionItems) + { + if (item.Parent != null) + { + emptySolutionFolders.Remove(item.Parent); + } + } + foreach (var emptySolutionFolder in emptySolutionFolders) + { + solution.RemoveFolder(emptySolutionFolder); + } await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index e2db8964e394..ac83c9733ee3 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -64,6 +64,17 @@ dotnet solution remove [...] [options] VisualStudioVersion = 15.0.26006.2 MinimumVisualStudioVersion = 10.0.40219.1 Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal "; @@ -317,7 +328,7 @@ public void WhenInvalidSolutionIsPassedItPrintsErrorAndUsage(string solutionComm .WithWorkingDirectory(projectDirectory) .Execute(solutionCommand, "InvalidSolution.sln", "remove", projectToRemove); cmd.Should().Fail(); - cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, "InvalidSolution.sln", "*")); + cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, Path.Combine(projectDirectory, "InvalidSolution.sln"), "*")); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } @@ -474,7 +485,7 @@ public void WhenSolutionItemsExistInFolderParentFoldersAreNotRemoved(string solu .BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems); } - [Theory] + [Theory(Skip = "vs-solutionpersistence does not allow duplicate references.")] [InlineData("sln")] [InlineData("solution")] public void WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCommand) From 903e327713fa13c6f5e35c24c3ef432f25cd07f5 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 9 Dec 2024 15:18:19 -0800 Subject: [PATCH 05/12] Fix all tests --- .../commands/dotnet-sln/remove/Program.cs | 40 ++++++++++++++----- test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 3 ++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 2b6f6e64d2e5..c7166b4241f5 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -20,6 +20,30 @@ internal class RemoveProjectFromSolutionCommand : CommandBase private readonly string _fileOrDirectory; private readonly IReadOnlyCollection _projects; + private int CountNonFolderDescendants(SolutionModel solution, SolutionFolderModel item, Dictionary cached) + { + if (cached.ContainsKey(item)) + { + return cached[item]; + } + int count = 0; + var children = solution.SolutionItems.Where(i => i.Parent == item); + foreach (var child in children) + { + if (child is SolutionFolderModel folderModel) + { + count += CountNonFolderDescendants(solution, folderModel, cached); + } + else + { + count++; + } + } + count += (item.Files?.Count ?? 0); + cached.Add(item, count); + return count; + } + public RemoveProjectFromSolutionCommand(ParseResult parseResult) : base(parseResult) { _fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument); @@ -94,18 +118,16 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< } } - // TODO: Remove empty solution folders - HashSet emptySolutionFolders = solution.SolutionFolders.ToHashSet(); - foreach (var item in solution.SolutionItems) + Dictionary nonFolderDescendantsCount = new(); + foreach (var item in solution.SolutionFolders) { - if (item.Parent != null) - { - emptySolutionFolders.Remove(item.Parent); - } + CountNonFolderDescendants(solution, item, nonFolderDescendantsCount); } - foreach (var emptySolutionFolder in emptySolutionFolders) + + var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0); + foreach (var folder in emptyFolders) { - solution.RemoveFolder(emptySolutionFolder); + solution.RemoveFolder(folder.Key); } await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index ac83c9733ee3..6b3e248f3301 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -166,6 +166,9 @@ dotnet solution remove [...] [options] {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal "; From 8e48a6a10e53f40706426e814da4f4f46bcc52cf Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Dec 2024 09:49:33 -0800 Subject: [PATCH 06/12] sln-remove: Update tests --- .../commands/dotnet-sln/remove/Program.cs | 18 +- .../App.slnx | 15 + .../ExpectedSlnContentsAfterRemove.sln | 33 ++ .../ExpectedSlnContentsAfterRemove.slnx | 11 + ...ectedSlnContentsAfterRemoveAllProjects.sln | 17 + ...ctedSlnContentsAfterRemoveAllProjects.slnx | 7 + ...edSlnContentsAfterRemoveLastNestedProj.sln | 33 ++ ...dSlnContentsAfterRemoveLastNestedProj.slnx | 11 + ...pectedSlnContentsAfterRemoveNestedProj.sln | 55 ++ ...ectedSlnContentsAfterRemoveNestedProj.slnx | 18 + ...ojectInSolutionWithNestedSolutionItems.sln | 43 ++ ...jectInSolutionWithNestedSolutionItems.slnx | 11 + ...entsAfterRemoveProjectWithDependencies.sln | 33 ++ ...ntsAfterRemoveProjectWithDependencies.slnx | 6 + .../App.slnx | 25 + .../TestAppWithSlnAndCsprojToRemove/App.slnx | 12 + .../App.slnx | 18 + .../App.slnx | 10 + test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 522 ++++++------------ 19 files changed, 543 insertions(+), 355 deletions(-) create mode 100644 test/TestAssets/TestProjects/SlnFileWithSolutionItemsInNestedFolders/App.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.slnx create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.sln create mode 100644 test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.slnx create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnAndCsprojInSubDirToRemove/App.slnx create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnAndCsprojToRemove/App.slnx create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnAndLastCsprojInSubDirToRemove/App.slnx create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnProjectDependencyToRemove/App.slnx diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index c7166b4241f5..a96295141c39 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -26,20 +26,14 @@ private int CountNonFolderDescendants(SolutionModel solution, SolutionFolderMode { return cached[item]; } - int count = 0; + int count = item.Files?.Count ?? 0; var children = solution.SolutionItems.Where(i => i.Parent == item); foreach (var child in children) { - if (child is SolutionFolderModel folderModel) - { - count += CountNonFolderDescendants(solution, folderModel, cached); - } - else - { - count++; - } + count += child is SolutionFolderModel folderModel + ? CountNonFolderDescendants(solution, folderModel, cached) + : 1; } - count += (item.Files?.Count ?? 0); cached.Add(item, count); return count; } @@ -124,10 +118,10 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< CountNonFolderDescendants(solution, item, nonFolderDescendantsCount); } - var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0); + var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0).Select(i => i.Key); foreach (var folder in emptyFolders) { - solution.RemoveFolder(folder.Key); + solution.RemoveFolder(folder); } await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); diff --git a/test/TestAssets/TestProjects/SlnFileWithSolutionItemsInNestedFolders/App.slnx b/test/TestAssets/TestProjects/SlnFileWithSolutionItemsInNestedFolders/App.slnx new file mode 100644 index 000000000000..01759c8694e4 --- /dev/null +++ b/test/TestAssets/TestProjects/SlnFileWithSolutionItemsInNestedFolders/App.slnx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.sln new file mode 100644 index 000000000000..5d4f59ecb34f --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.sln @@ -0,0 +1,33 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App\App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.slnx new file mode 100644 index 000000000000..9d9f6a7f5faa --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemove.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.sln new file mode 100644 index 000000000000..bddb84ff3a0c --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.sln @@ -0,0 +1,17 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.slnx new file mode 100644 index 000000000000..70f4bc532c1a --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveAllProjects.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.sln new file mode 100644 index 000000000000..6a9c1d3893db --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.sln @@ -0,0 +1,33 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.slnx new file mode 100644 index 000000000000..1d64de45b461 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveLastNestedProj.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.sln new file mode 100644 index 000000000000..5b07912ba6f1 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.sln @@ -0,0 +1,55 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7B86CE74-F620-4B32-99FE-82D40F8D6BF2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{EAB71280-AF32-4531-8703-43CDBA261AA3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib", "src\Lib\Lib.csproj", "{84A45D44-B677-492D-A6DA-B3A71135AB8E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 + {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.ActiveCfg = Debug|x64 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.Build.0 = Debug|x64 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.ActiveCfg = Debug|x86 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.Build.0 = Debug|x86 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.Build.0 = Release|Any CPU + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.ActiveCfg = Release|x64 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.Build.0 = Release|x64 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.ActiveCfg = Release|x86 + {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EAB71280-AF32-4531-8703-43CDBA261AA3} = {7B86CE74-F620-4B32-99FE-82D40F8D6BF2} + {84A45D44-B677-492D-A6DA-B3A71135AB8E} = {EAB71280-AF32-4531-8703-43CDBA261AA3} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.slnx new file mode 100644 index 000000000000..22f8b69d5140 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveNestedProj.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.sln new file mode 100644 index 000000000000..d3d281e422d5 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.sln @@ -0,0 +1,43 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.705 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NewFolder1", "NewFolder1", "{F39E429A-F91C-44F9-9025-EA6E41C99637}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NewFolder2", "NewFolder2", "{1B19AA22-7DB3-4A0F-8B89-2B80040B2BF0}" + ProjectSection(SolutionItems) = preProject + TextFile1.txt = TextFile1.txt + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NestedSolution", "NestedSolution", "{2128FC1C-7B60-4BC4-9010-D7A97E1805EA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NestedFolder", "NestedFolder", "{7BC6A99C-7321-47A2-8CA5-B394195BD2B8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NestedFolder", "NestedFolder", "{F6B0D958-42FF-4123-9668-A77A4ABFE5BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp2", "ConsoleApp2\ConsoleApp2.csproj", "{A264F7DB-E1EF-4F99-96BE-C08F29CA494F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1B19AA22-7DB3-4A0F-8B89-2B80040B2BF0} = {F39E429A-F91C-44F9-9025-EA6E41C99637} + {7BC6A99C-7321-47A2-8CA5-B394195BD2B8} = {2128FC1C-7B60-4BC4-9010-D7A97E1805EA} + {F6B0D958-42FF-4123-9668-A77A4ABFE5BA} = {7BC6A99C-7321-47A2-8CA5-B394195BD2B8} + {A264F7DB-E1EF-4F99-96BE-C08F29CA494F} = {F6B0D958-42FF-4123-9668-A77A4ABFE5BA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D36A0DD6-BD9C-4B57-9441-693B33DB7C2D} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.slnx new file mode 100644 index 000000000000..bc8d9b27891c --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.sln b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.sln new file mode 100644 index 000000000000..2301710e0ec7 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.sln @@ -0,0 +1,33 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27110.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "App\App.csproj", "{BB02B949-F6BD-4872-95CB-96A05B1FE026}" + ProjectSection(ProjectDependencies) = postProject + {D53E177A-8ECF-43D5-A01E-98B884D53CA6} = {D53E177A-8ECF-43D5-A01E-98B884D53CA6} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "First", "First\First.csproj", "{D53E177A-8ECF-43D5-A01E-98B884D53CA6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Release|Any CPU.Build.0 = Release|Any CPU + {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F6D9A973-1CFD-41C9-84F2-1471C0FE67DF} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.slnx b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.slnx new file mode 100644 index 000000000000..c38dc0b63f21 --- /dev/null +++ b/test/TestAssets/TestProjects/SolutionFilesTemplates/ExpectedSlnContentsAfterRemoveProjectWithDependencies.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojInSubDirToRemove/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojInSubDirToRemove/App.slnx new file mode 100644 index 000000000000..07b484553c0a --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojInSubDirToRemove/App.slnx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojToRemove/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojToRemove/App.slnx new file mode 100644 index 000000000000..6fcaebf060fe --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnAndCsprojToRemove/App.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnAndLastCsprojInSubDirToRemove/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnAndLastCsprojInSubDirToRemove/App.slnx new file mode 100644 index 000000000000..22f8b69d5140 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnAndLastCsprojInSubDirToRemove/App.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnProjectDependencyToRemove/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnProjectDependencyToRemove/App.slnx new file mode 100644 index 000000000000..c8df49699642 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnProjectDependencyToRemove/App.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index 6b3e248f3301..66526edcf843 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -4,6 +4,9 @@ using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Tools; using Microsoft.DotNet.Tools.Common; +using Microsoft.VisualStudio.SolutionPersistence; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; namespace Microsoft.DotNet.Cli.Sln.Remove.Tests { @@ -22,238 +25,6 @@ dotnet solution remove [...] [options] Options: -?, -h, --help Show command line help."; - private const string ExpectedSlnContentsAfterRemove = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26006.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App\App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}"" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal -"; - - private const string ExpectedSlnContentsAfterRemoveAllProjects = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26006.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal -"; - - private const string ExpectedSlnContentsAfterRemoveNestedProj = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26006.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}"" -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""src"", ""src"", ""{7B86CE74-F620-4B32-99FE-82D40F8D6BF2}"" -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""Lib"", ""Lib"", ""{EAB71280-AF32-4531-8703-43CDBA261AA3}"" -EndProject -Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Lib"", ""src\Lib\Lib.csproj"", ""{84A45D44-B677-492D-A6DA-B3A71135AB8E}"" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.ActiveCfg = Debug|x64 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.Build.0 = Debug|x64 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.ActiveCfg = Debug|x86 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.Build.0 = Debug|x86 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.Build.0 = Release|Any CPU - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.ActiveCfg = Release|x64 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.Build.0 = Release|x64 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.ActiveCfg = Release|x86 - {84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {EAB71280-AF32-4531-8703-43CDBA261AA3} = {7B86CE74-F620-4B32-99FE-82D40F8D6BF2} - {84A45D44-B677-492D-A6DA-B3A71135AB8E} = {EAB71280-AF32-4531-8703-43CDBA261AA3} - EndGlobalSection -EndGlobal -"; - - private const string ExpectedSlnContentsAfterRemoveLastNestedProj = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26006.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}"" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86 - {7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal -"; - - private const string ExpectedSlnContentsAfterRemoveProjectWithDependencies = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27110.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""App"", ""App\App.csproj"", ""{BB02B949-F6BD-4872-95CB-96A05B1FE026}"" - ProjectSection(ProjectDependencies) = postProject - {D53E177A-8ECF-43D5-A01E-98B884D53CA6} = {D53E177A-8ECF-43D5-A01E-98B884D53CA6} - EndProjectSection -EndProject -Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""First"", ""First\First.csproj"", ""{D53E177A-8ECF-43D5-A01E-98B884D53CA6}"" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB02B949-F6BD-4872-95CB-96A05B1FE026}.Release|Any CPU.Build.0 = Release|Any CPU - {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D53E177A-8ECF-43D5-A01E-98B884D53CA6}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {F6D9A973-1CFD-41C9-84F2-1471C0FE67DF} - EndGlobalSection -EndGlobal -"; - - private const string ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems = @" -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.705 -MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""NewFolder1"", ""NewFolder1"", ""{F39E429A-F91C-44F9-9025-EA6E41C99637}"" -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""NewFolder2"", ""NewFolder2"", ""{1B19AA22-7DB3-4A0F-8B89-2B80040B2BF0}"" - ProjectSection(SolutionItems) = preProject - TextFile1.txt = TextFile1.txt - EndProjectSection -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""NestedSolution"", ""NestedSolution"", ""{2128FC1C-7B60-4BC4-9010-D7A97E1805EA}"" -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""NestedFolder"", ""NestedFolder"", ""{7BC6A99C-7321-47A2-8CA5-B394195BD2B8}"" -EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""NestedFolder"", ""NestedFolder"", ""{F6B0D958-42FF-4123-9668-A77A4ABFE5BA}"" -EndProject -Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ConsoleApp2"", ""ConsoleApp2\ConsoleApp2.csproj"", ""{A264F7DB-E1EF-4F99-96BE-C08F29CA494F}"" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A264F7DB-E1EF-4F99-96BE-C08F29CA494F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {1B19AA22-7DB3-4A0F-8B89-2B80040B2BF0} = {F39E429A-F91C-44F9-9025-EA6E41C99637} - {7BC6A99C-7321-47A2-8CA5-B394195BD2B8} = {2128FC1C-7B60-4BC4-9010-D7A97E1805EA} - {F6B0D958-42FF-4123-9668-A77A4ABFE5BA} = {7BC6A99C-7321-47A2-8CA5-B394195BD2B8} - {A264F7DB-E1EF-4F99-96BE-C08F29CA494F} = {F6B0D958-42FF-4123-9668-A77A4ABFE5BA} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D36A0DD6-BD9C-4B57-9441-693B33DB7C2D} - EndGlobalSection -EndGlobal -"; - public GivenDotnetSlnRemove(ITestOutputHelper log) : base(log) { } @@ -277,10 +48,10 @@ public void WhenHelpOptionIsPassedItPrintsUsage(string solutionCommand, string h public void WhenTooManyArgumentsArePassedItPrintsError(string solutionCommand) { var cmd = new DotnetCommand(Log) - .Execute(solutionCommand, "one.sln", "two.sln", "three.sln", "remove"); + .Execute(solutionCommand, "one.sln", "two.sln", "three.slnx", "remove"); cmd.Should().Fail(); cmd.StdErr.Should().BeVisuallyEquivalentTo($@"{string.Format(CommandLineValidation.LocalizableStrings.UnrecognizedCommandOrArgument, "two.sln")} -{string.Format(CommandLineValidation.LocalizableStrings.UnrecognizedCommandOrArgument, "three.sln")}"); +{string.Format(CommandLineValidation.LocalizableStrings.UnrecognizedCommandOrArgument, "three.slnx")}"); } [Theory] @@ -298,11 +69,13 @@ public void WhenNoCommandIsPassedItPrintsError(string solutionCommand, string co [Theory] [InlineData("sln", "idontexist.sln")] + [InlineData("sln", "idontexist.slnx")] [InlineData("sln", "ihave?invalidcharacters")] [InlineData("sln", "ihaveinv@lidcharacters")] [InlineData("sln", "ihaveinvalid/characters")] [InlineData("sln", "ihaveinvalidchar\\acters")] [InlineData("solution", "idontexist.sln")] + [InlineData("solution", "idontexist.slnx")] [InlineData("solution", "ihave?invalidcharacters")] [InlineData("solution", "ihaveinv@lidcharacters")] [InlineData("solution", "ihaveinvalid/characters")] @@ -317,9 +90,11 @@ public void WhenNonExistingSolutionIsPassedItPrintsErrorAndUsage(string solution } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenInvalidSolutionIsPassedItPrintsErrorAndUsage(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenInvalidSolutionIsPassedItPrintsErrorAndUsage(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("InvalidSolution", identifier: $"{solutionCommand}GivenDotnetSlnRemove") @@ -329,15 +104,17 @@ public void WhenInvalidSolutionIsPassedItPrintsErrorAndUsage(string solutionComm var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "InvalidSolution.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"InvalidSolution{solutionExtension}", "remove", projectToRemove); cmd.Should().Fail(); - cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, Path.Combine(projectDirectory, "InvalidSolution.sln"), "*")); + cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, Path.Combine(projectDirectory, $"InvalidSolution{solutionExtension}"), "*")); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } [Theory] [InlineData("sln", ".sln")] [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionCommand, string solutionExtension) { var projectDirectoryRoot = _testAssetsManager @@ -349,7 +126,7 @@ public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionC ? Path.Join(projectDirectoryRoot, "Sln") : Path.Join(projectDirectoryRoot, "Slnx"); - var solutionPath = Path.Combine(projectDirectory, "InvalidSolution.sln"); + var solutionPath = Path.Combine(projectDirectory, $"InvalidSolution{solutionExtension}"); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) @@ -360,9 +137,11 @@ public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionC } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenNoProjectIsPassedItPrintsErrorAndUsage(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenNoProjectIsPassedItPrintsErrorAndUsage(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojFiles", identifier: $"{solutionCommand}GivenDotnetSlnRemove") @@ -371,7 +150,7 @@ public void WhenNoProjectIsPassedItPrintsErrorAndUsage(string solutionCommand) var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove"); + .Execute(solutionCommand, $"App{solutionExtension}", "remove"); cmd.Should().Fail(); cmd.StdErr.Should().Be(CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); @@ -416,20 +195,22 @@ public void WhenMoreThanOneSolutionExistsInTheDirectoryItPrintsErrorAndUsage(str } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenPassedAReferenceNotInSlnItPrintsStatus(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenPassedAReferenceNotInSlnItPrintsStatus(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndExistingCsprojReferences", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); var contentBefore = File.ReadAllText(solutionPath); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", "referenceDoesNotExistInSln.csproj"); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", "referenceDoesNotExistInSln.csproj"); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectNotFoundInTheSolution, "referenceDoesNotExistInSln.csproj")); File.ReadAllText(solutionPath) @@ -437,61 +218,67 @@ public void WhenPassedAReferenceNotInSlnItPrintsStatus(string solutionCommand) } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenPassedAReferenceItRemovesTheReferenceButNotOtherReferences(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenPassedAReferenceItRemovesTheReferenceButNotOtherReferences(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndExistingCsprojReferences", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectToRemove)); - slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(1); - slnFile.Projects[0].FilePath.Should().Be(Path.Combine("App", "App.csproj")); + solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution.SolutionProjects.Count.Should().Be(1); + solution.SolutionProjects.Single().FilePath.Should().Be(Path.Combine("App", "App.csproj")); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenSolutionItemsExistInFolderParentFoldersAreNotRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenSolutionItemsExistInFolderParentFoldersAreNotRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager - .CopyTestAsset("SlnFileWithSolutionItemsInNestedFolders", identifier: $"{solutionCommand}") + .CopyTestAsset("SlnFileWithSolutionItemsInNestedFolders", identifier: $"{solutionCommand}{solutionExtension}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); var projectToRemove = Path.Combine("ConsoleApp1", "ConsoleApp1.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); cmd.StdOut.Should().Be(string.Format(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectToRemove)); - slnFile = SlnFile.Read(solutionPath); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems{solutionExtension}"); File.ReadAllText(solutionPath) .Should() - .BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveProjectInSolutionWithNestedSolutionItems); + .BeVisuallyEquivalentTo(templateContents); } [Theory(Skip = "vs-solutionpersistence does not allow duplicate references.")] [InlineData("sln")] [InlineData("solution")] - public void WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCommand) + public async Task WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCommand) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndDuplicateProjectReferences", identifier: $"{solutionCommand}") @@ -512,29 +299,35 @@ public void WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCom outputText += Environment.NewLine + outputText; cmd.StdOut.Should().BeVisuallyEquivalentTo(outputText); - slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(1); - slnFile.Projects[0].FilePath.Should().Be(Path.Combine("App", "App.csproj")); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution.SolutionProjects.Count.Should().Be(1); + solution.SolutionProjects.Single().FilePath.Should().Be(Path.Combine("App", "App.csproj")); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenPassedMultipleReferencesAndOneOfThemDoesNotExistItRemovesTheOneThatExists(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenPassedMultipleReferencesAndOneOfThemDoesNotExistItRemovesTheOneThatExists(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndExistingCsprojReferences", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", "idontexist.csproj", projectToRemove, "idontexisteither.csproj"); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", "idontexist.csproj", projectToRemove, "idontexisteither.csproj"); cmd.Should().Pass(); string outputText = $@"{string.Format(CommonLocalizableStrings.ProjectNotFoundInTheSolution, "idontexist.csproj")} @@ -543,62 +336,76 @@ public void WhenPassedMultipleReferencesAndOneOfThemDoesNotExistItRemovesTheOneT cmd.StdOut.Should().BeVisuallyEquivalentTo(outputText); - slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(1); - slnFile.Projects[0].FilePath.Should().Be(Path.Combine("App", "App.csproj")); + solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution.SolutionProjects.Count.Should().Be(1); + solution.SolutionProjects.Single().FilePath.Should().Be(Path.Combine("App", "App.csproj")); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenReferenceIsRemovedBuildConfigsAreAlsoRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenReferenceIsRemovedBuildConfigsAreAlsoRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemove{solutionExtension}"); File.ReadAllText(solutionPath) - .Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemove); + .Should().BeVisuallyEquivalentTo(templateContents); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenDirectoryContainingProjectIsGivenProjectIsRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenDirectoryContainingProjectIsGivenProjectIsRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", "Lib"); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", "Lib"); cmd.Should().Pass(); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemove{solutionExtension}"); File.ReadAllText(solutionPath) - .Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemove); + .Should().BeVisuallyEquivalentTo(templateContents); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") @@ -608,7 +415,7 @@ public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string soluti var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", directoryToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -618,9 +425,11 @@ public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string soluti } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") @@ -630,7 +439,7 @@ public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", directoryToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -640,33 +449,38 @@ public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenReferenceIsRemovedSlnBuilds(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenReferenceIsRemovedSlnBuilds(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager - .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") + .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}{solutionExtension}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute($"restore", "App.sln") + .Execute($"restore", $"App{solutionExtension}") .Should().Pass(); new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute("build", "App.sln", "--configuration", "Release", "/p:ProduceReferenceAssembly=false") + .Execute("build", $"App{solutionExtension}", "--configuration", "Release", "/p:ProduceReferenceAssembly=false") .Should().Pass(); var reasonString = "should be built in release mode, otherwise it means build configurations are missing from the sln file"; @@ -709,95 +523,108 @@ public void WhenProjectIsRemovedSolutionHasUTF8BOM(string solutionCommand) } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenFinalReferenceIsRemovedEmptySectionsAreRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public async Task WhenFinalReferenceIsRemovedEmptySectionsAreRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(2); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); + + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution.SolutionProjects.Count.Should().Be(2); var appPath = Path.Combine("App", "App.csproj"); var libPath = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", libPath, appPath); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", libPath, appPath); cmd.Should().Pass(); var solutionContents = File.ReadAllText(solutionPath); - - solutionContents.Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveAllProjects); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemoveAllProjects{solutionExtension}"); + solutionContents.Should().BeVisuallyEquivalentTo(templateContents); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenNestedProjectIsRemovedItsSolutionFoldersAreRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenNestedProjectIsRemovedItsSolutionFoldersAreRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndCsprojInSubDirToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); var projectToRemove = Path.Combine("src", "NotLastProjInSrc", "NotLastProjInSrc.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemoveNestedProj{solutionExtension}"); File.ReadAllText(solutionPath) - .Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveNestedProj); + .Should().BeVisuallyEquivalentTo(templateContents); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenFinalNestedProjectIsRemovedSolutionFoldersAreRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenFinalNestedProjectIsRemovedSolutionFoldersAreRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnAndLastCsprojInSubDirToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); var projectToRemove = Path.Combine("src", "Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemoveLastNestedProj{solutionExtension}"); File.ReadAllText(solutionPath) - .Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveLastNestedProj); + .Should().BeVisuallyEquivalentTo(templateContents); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenProjectIsRemovedThenDependenciesOnProjectAreAlsoRemoved(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenProjectIsRemovedThenDependenciesOnProjectAreAlsoRemoved(string solutionCommand, string solutionExtension) { var projectDirectory = _testAssetsManager .CopyTestAsset("TestAppWithSlnProjectDependencyToRemove", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "App.sln"); + var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); var projectToRemove = Path.Combine("Second", "Second.csproj"); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "App.sln", "remove", projectToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", projectToRemove); cmd.Should().Pass(); + var templateContents = GetSolutionFileTemplateContents($"ExpectedSlnContentsAfterRemoveProjectWithDependencies{solutionExtension}"); File.ReadAllText(solutionPath) - .Should().BeVisuallyEquivalentTo(ExpectedSlnContentsAfterRemoveProjectWithDependencies); + .Should().BeVisuallyEquivalentTo(templateContents); } [Theory] @@ -822,5 +649,14 @@ public void WhenSolutionIsPassedAsProjectItPrintsSuggestionAndUsage(string solut ); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } + + private string GetSolutionFileTemplateContents(string templateFileName) + { + var templateContentDirectory = _testAssetsManager + .CopyTestAsset("SolutionFilesTemplates", identifier: "SolutionFilesTemplates") + .WithSource() + .Path; + return File.ReadAllText(Path.Join(templateContentDirectory, templateFileName)); + } } } From d732efcd818d6223a6a6dc4537854f358a134533 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Dec 2024 10:18:51 -0800 Subject: [PATCH 07/12] sln-list: Update tests --- test/dotnet-sln.Tests/GivenDotnetSlnList.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnList.cs b/test/dotnet-sln.Tests/GivenDotnetSlnList.cs index 682a1e45c495..f462ff1e6042 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnList.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnList.cs @@ -240,9 +240,11 @@ public void WhenProjectsPresentInTheReadonlySolutionItListsThem(string solutionC } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenProjectsInSolutionFoldersPresentInTheSolutionItListsSolutionFolderPaths(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenProjectsInSolutionFoldersPresentInTheSolutionItListsSolutionFolderPaths(string solutionCommand, string solutionExtension) { string[] expectedOutput = { $"{CommandLocalizableStrings.SolutionFolderHeader}", $"{new string('-', CommandLocalizableStrings.SolutionFolderHeader.Length)}", @@ -255,7 +257,7 @@ public void WhenProjectsInSolutionFoldersPresentInTheSolutionItListsSolutionFold var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "list", "--solution-folders"); + .Execute(solutionCommand, $"App{solutionExtension}", "list", "--solution-folders"); cmd.Should().Pass(); cmd.StdOut.Should().ContainAll(expectedOutput); } From 2a3b921f7a1a1486814be6f832f772ae8cb02e9a Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Dec 2024 16:32:50 -0800 Subject: [PATCH 08/12] Nit --- src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs | 1 - test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index a96295141c39..37886d28ab20 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -5,7 +5,6 @@ using Microsoft.Build.Construction; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli; -using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Common; using Microsoft.Extensions.EnvironmentAbstractions; diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index 66526edcf843..c59d5e6a9c39 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Tools; using Microsoft.DotNet.Tools.Common; using Microsoft.VisualStudio.SolutionPersistence; @@ -286,8 +285,9 @@ public async Task WhenDuplicateReferencesArePresentItRemovesThemAll(string solut .Path; var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(3); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution.SolutionProjects.Count.Should().Be(3); var projectToRemove = Path.Combine("Lib", "Lib.csproj"); var cmd = new DotnetCommand(Log) @@ -299,8 +299,7 @@ public async Task WhenDuplicateReferencesArePresentItRemovesThemAll(string solut outputText += Environment.NewLine + outputText; cmd.StdOut.Should().BeVisuallyEquivalentTo(outputText); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); - SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(1); solution.SolutionProjects.Single().FilePath.Should().Be(Path.Combine("App", "App.csproj")); } From 64cb697ff9c6de713604e4126947ec4c0adcd2da Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 23 Dec 2024 16:22:38 -0800 Subject: [PATCH 09/12] Apply suggestions from code review --- .../commands/dotnet-sln/remove/Program.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 37886d28ab20..f771a5c234b7 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -19,18 +19,22 @@ internal class RemoveProjectFromSolutionCommand : CommandBase private readonly string _fileOrDirectory; private readonly IReadOnlyCollection _projects; - private int CountNonFolderDescendants(SolutionModel solution, SolutionFolderModel item, Dictionary cached) + private int CountNonFolderDescendants( + SolutionModel solution, + SolutionFolderModel item, + Dictionary solutionItemsGroupedByParent, + Dictionary cached) { if (cached.ContainsKey(item)) { return cached[item]; } int count = item.Files?.Count ?? 0; - var children = solution.SolutionItems.Where(i => i.Parent == item); + var children = solutionItemsGroupedByParent.TryGetValue(item, out var items) ? items : Array.Empty(); foreach (var child in children) { count += child is SolutionFolderModel folderModel - ? CountNonFolderDescendants(solution, folderModel, cached) + ? CountNonFolderDescendants(solution, folderModel, solutionItemsGroupedByParent, cached) : 1; } cached.Add(item, count); @@ -111,10 +115,15 @@ private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable< } } + Dictionary solutionItemsGroupedByParent = solution.SolutionItems + .Where(i => i.Parent != null) + .GroupBy(i => i.Parent) + .ToDictionary(g => g.Key, g => g.ToArray()); + Dictionary nonFolderDescendantsCount = new(); foreach (var item in solution.SolutionFolders) { - CountNonFolderDescendants(solution, item, nonFolderDescendantsCount); + CountNonFolderDescendants(solution, item, solutionItemsGroupedByParent, nonFolderDescendantsCount); } var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0).Select(i => i.Key); From 224b9da04d403810b3d23092c316bc39715a9cca Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 25 Dec 2024 14:56:17 -0800 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: kasperk81 <83082615+kasperk81@users.noreply.github.com> --- test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index c59d5e6a9c39..42f51f4c31d3 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -230,7 +230,7 @@ public async Task WhenPassedAReferenceItRemovesTheReferenceButNotOtherReferences var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); @@ -285,7 +285,7 @@ public async Task WhenDuplicateReferencesArePresentItRemovesThemAll(string solut .Path; var solutionPath = Path.Combine(projectDirectory, "App.sln"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(3); @@ -318,7 +318,7 @@ public async Task WhenPassedMultipleReferencesAndOneOfThemDoesNotExistItRemovesT var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); @@ -354,7 +354,7 @@ public async Task WhenReferenceIsRemovedBuildConfigsAreAlsoRemoved(string soluti var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); @@ -384,7 +384,7 @@ public async Task WhenDirectoryContainingProjectIsGivenProjectIsRemoved(string s var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); @@ -461,7 +461,7 @@ public async Task WhenReferenceIsRemovedSlnBuilds(string solutionCommand, string var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); @@ -535,7 +535,7 @@ public async Task WhenFinalReferenceIsRemovedEmptySectionsAreRemoved(string solu var solutionPath = Path.Combine(projectDirectory, $"App{solutionExtension}"); - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); solution.SolutionProjects.Count.Should().Be(2); From 9ee83e660617ddca70e24db251866a5ba5038e82 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 7 Jan 2025 14:41:27 -0800 Subject: [PATCH 11/12] Nit --- src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index f771a5c234b7..45a2550b0fb7 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -72,13 +72,12 @@ public override int Execute() RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).Wait(); return 0; } - catch (Exception ex) when (ex is not GracefulException) + catch (Exception ex) when (ex is not GracefulException && ex.InnerException is not GracefulException) { if (ex is SolutionException || ex.InnerException is SolutionException) { throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message); } - // TODO: Check if (ex.InnerException is GracefulException) { throw ex.InnerException; From ed19aac8bb151b6c6fd045d580fdbc759d584392 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 7 Jan 2025 16:18:29 -0800 Subject: [PATCH 12/12] Nit --- src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 45a2550b0fb7..766ce64d9b7c 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -72,7 +72,7 @@ public override int Execute() RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).Wait(); return 0; } - catch (Exception ex) when (ex is not GracefulException && ex.InnerException is not GracefulException) + catch (Exception ex) when (ex is not GracefulException) { if (ex is SolutionException || ex.InnerException is SolutionException) {