diff --git a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs index 1e4ce2e23b95..766ce64d9b7c 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs @@ -2,58 +2,136 @@ // 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; +using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12; namespace Microsoft.DotNet.Tools.Sln.Remove { internal class RemoveProjectFromSolutionCommand : CommandBase { private readonly string _fileOrDirectory; - private readonly IReadOnlyCollection _arguments; + private readonly IReadOnlyCollection _projects; + + 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 = solutionItemsGroupedByParent.TryGetValue(item, out var items) ? items : Array.Empty(); + foreach (var child in children) + { + count += child is SolutionFolderModel folderModel + ? CountNonFolderDescendants(solution, folderModel, solutionItemsGroupedByParent, cached) + : 1; + } + cached.Add(item, count); + return count; + } 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); + } + + try + { + 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); + } + if (ex.InnerException is GracefulException) + { + throw ex.InnerException; + } + throw new GracefulException(ex.Message, ex); + } + } + + private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable projectPaths, CancellationToken cancellationToken) + { + ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath); + SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken); - var baseDirectory = PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory); - var relativeProjectPaths = _arguments.Select(p => + // set UTF8 BOM encoding for .sln + if (serializer is ISolutionSerializer v12Serializer) { - var fullPath = Path.GetFullPath(p); - return Path.GetRelativePath( - baseDirectory, - Directory.Exists(fullPath) ? - MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : - fullPath - ); - }); + solution.SerializerExtension = v12Serializer.CreateModelExtension(new() + { + Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true) + }); + } - bool slnChanged = false; - foreach (var path in relativeProjectPaths) + foreach (var projectPath in projectPaths) { - slnChanged |= slnFile.RemoveProject(path); + var project = solution.FindProject(projectPath); + if (project != null) + { + solution.RemoveProject(project); + Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectPath); + } + else + { + Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectNotFoundInTheSolution, projectPath); + } } - slnFile.RemoveEmptyConfigurationSections(); + Dictionary solutionItemsGroupedByParent = solution.SolutionItems + .Where(i => i.Parent != null) + .GroupBy(i => i.Parent) + .ToDictionary(g => g.Key, g => g.ToArray()); - slnFile.RemoveEmptySolutionFolders(); + Dictionary nonFolderDescendantsCount = new(); + foreach (var item in solution.SolutionFolders) + { + CountNonFolderDescendants(solution, item, solutionItemsGroupedByParent, nonFolderDescendantsCount); + } - if (slnChanged) + var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0).Select(i => i.Key); + foreach (var folder in emptyFolders) { - slnFile.Write(); + solution.RemoveFolder(folder); } - return 0; + 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/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); } diff --git a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs index b54349830c48..42f51f4c31d3 100644 --- a/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs +++ b/test/dotnet-sln.Tests/GivenDotnetSlnRemove.cs @@ -1,9 +1,11 @@ // 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; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; namespace Microsoft.DotNet.Cli.Sln.Remove.Tests { @@ -22,221 +24,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 -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 -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 -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) { } @@ -260,10 +47,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] @@ -281,11 +68,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")] @@ -300,9 +89,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") @@ -312,36 +103,44 @@ 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().Be(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, "InvalidSolution.sln", LocalizableStrings.FileHeaderMissingError)); + cmd.StdErr.Should().Match(string.Format(CommonLocalizableStrings.InvalidSolutionFormatString, Path.Combine(projectDirectory, $"InvalidSolution{solutionExtension}"), "*")); cmd.StdOut.Should().BeVisuallyEquivalentToIfNotLocalized(""); } [Theory] - [InlineData("sln")] - [InlineData("solution")] - public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionCommand) + [InlineData("sln", ".sln")] + [InlineData("solution", ".sln")] + [InlineData("sln", ".slnx")] + [InlineData("solution", ".slnx")] + public void WhenInvalidSolutionIsFoundRemovePrintsErrorAndUsage(string solutionCommand, string solutionExtension) { - var projectDirectory = _testAssetsManager + var projectDirectoryRoot = _testAssetsManager .CopyTestAsset("InvalidSolution", identifier: $"{solutionCommand}") .WithSource() .Path; - var solutionPath = Path.Combine(projectDirectory, "InvalidSolution.sln"); + var projectDirectory = solutionExtension == ".sln" + ? Path.Join(projectDirectoryRoot, "Sln") + : Path.Join(projectDirectoryRoot, "Slnx"); + + var solutionPath = Path.Combine(projectDirectory, $"InvalidSolution{solutionExtension}"); 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(""); } [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") @@ -350,7 +149,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(""); @@ -395,20 +194,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, "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) @@ -416,61 +217,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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {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, "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, "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] + [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}") @@ -478,42 +285,48 @@ public void WhenDuplicateReferencesArePresentItRemovesThemAll(string solutionCom .Path; var solutionPath = Path.Combine(projectDirectory, "App.sln"); - SlnFile slnFile = SlnFile.Read(solutionPath); - slnFile.Projects.Count.Should().Be(3); + 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); 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); 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")); + 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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {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, "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")} @@ -522,62 +335,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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {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, "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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {solutionPath}."); + SolutionModel solution = await serializer.OpenAsync(solutionPath, CancellationToken.None); + + solution.SolutionProjects.Count.Should().Be(2); var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "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}") @@ -587,7 +414,7 @@ public void WhenDirectoryContainsNoProjectsItCancelsWholeOperation(string soluti var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", directoryToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -597,9 +424,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}") @@ -609,7 +438,7 @@ public void WhenDirectoryContainsMultipleProjectsItCancelsWholeOperation(string var cmd = new DotnetCommand(Log) .WithWorkingDirectory(projectDirectory) - .Execute(solutionCommand, "remove", directoryToRemove); + .Execute(solutionCommand, $"App{solutionExtension}", "remove", directoryToRemove); cmd.Should().Fail(); cmd.StdErr.Should().Be( string.Format( @@ -619,33 +448,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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {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, "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"; @@ -688,95 +522,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) ?? throw new InvalidOperationException($"Unable to get solution serializer for {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, "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, "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, "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, "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] @@ -801,5 +648,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)); + } } }