Skip to content

Commit 564b0b9

Browse files
psmulovicsCopilot
andcommitted
Add create-solution-filters command for converting multiple solutions to monorepo with filters
Implements issue #49 to add a new command that converts multiple .sln files into a single consolidated solution with individual .slnf solution filter files. Command syntax: please create-solution-filters --from <glob-pattern> --target <target-solution> Behavior: - Creates or uses existing target solution - Adds all projects from source solutions under solution folders - Generates .slnf files for each original solution as filters - Deletes source solution files after consolidation - Supports --dry-run flag Includes comprehensive unit tests covering: - Two solution consolidation - Multiple projects per solution - Single solution handling - Existing target solution reuse Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 13bf5ff commit 564b0b9

2 files changed

Lines changed: 495 additions & 0 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// Morgan Stanley makes this available to you under the Apache License,
2+
// Version 2.0 (the "License"). You may obtain a copy of the License at
3+
//
4+
// http://www.apache.org/licenses/LICENSE-2.0.
5+
//
6+
// See the NOTICE file distributed with this work for additional information
7+
// regarding copyright ownership. Unless agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations
11+
// under the License.
12+
13+
using System;
14+
using System.Collections.Generic;
15+
using System.IO;
16+
using System.Linq;
17+
using System.Text.Json;
18+
using System.Threading.Tasks;
19+
using FluentAssertions;
20+
using Xunit;
21+
using Xunit.Abstractions;
22+
using static DotNetPlease.Helpers.DotNetCliHelper;
23+
using static DotNetPlease.Helpers.MSBuildHelper;
24+
25+
namespace DotNetPlease.Commands;
26+
27+
public class CreateSolutionFiltersTests : TestFixtureBase
28+
{
29+
[Theory, CombinatorialData]
30+
public async Task It_consolidates_two_solutions_into_one_with_filters(bool dryRun)
31+
{
32+
// Arrange
33+
var solution1Path = GetFullPath("Solution1/Solution1.sln");
34+
CreateSolution(solution1Path);
35+
var project1Path = GetFullPath("Solution1/Project1/Project1.csproj");
36+
CreateProject(project1Path);
37+
AddProjectToSolution(project1Path, solution1Path);
38+
39+
var solution2Path = GetFullPath("Solution2/Solution2.sln");
40+
CreateSolution(solution2Path);
41+
var project2Path = GetFullPath("Solution2/Project2/Project2.csproj");
42+
CreateProject(project2Path);
43+
AddProjectToSolution(project2Path, solution2Path);
44+
45+
var targetSolutionPath = GetFullPath("Consolidated.sln");
46+
47+
if (dryRun) CreateSnapshot();
48+
49+
// Act
50+
await RunAndAssertSuccess(
51+
"create-solution-filters",
52+
"--from", "Solution*/Solution*.sln",
53+
"--target", "Consolidated.sln",
54+
DryRunOption(dryRun));
55+
56+
// Assert
57+
if (dryRun)
58+
{
59+
VerifySnapshot();
60+
return;
61+
}
62+
63+
File.Exists(targetSolutionPath).Should().BeTrue("Target solution should be created");
64+
65+
var projectsInTarget = GetProjectsFromSolution(targetSolutionPath);
66+
projectsInTarget.Should().Contain(project1Path, "Project1 should be in target");
67+
projectsInTarget.Should().Contain(project2Path, "Project2 should be in target");
68+
69+
File.Exists(solution1Path).Should().BeFalse("Source solution 1 should be deleted");
70+
File.Exists(solution2Path).Should().BeFalse("Source solution 2 should be deleted");
71+
72+
var filter1Path = Path.ChangeExtension(solution1Path, ".slnf");
73+
var filter2Path = Path.ChangeExtension(solution2Path, ".slnf");
74+
File.Exists(filter1Path).Should().BeTrue("Solution1.slnf should be created");
75+
File.Exists(filter2Path).Should().BeTrue("Solution2.slnf should be created");
76+
77+
VerifyFilterFile(filter1Path, "Solution1", targetSolutionPath, new[] { project1Path });
78+
VerifyFilterFile(filter2Path, "Solution2", targetSolutionPath, new[] { project2Path });
79+
}
80+
81+
[Theory, CombinatorialData]
82+
public async Task It_uses_existing_target_solution(bool dryRun)
83+
{
84+
// Arrange
85+
var targetSolutionPath = GetFullPath("Consolidated.sln");
86+
CreateSolution(targetSolutionPath);
87+
88+
var solution1Path = GetFullPath("Solution1/Solution1.sln");
89+
CreateSolution(solution1Path);
90+
var project1Path = GetFullPath("Solution1/Project1/Project1.csproj");
91+
CreateProject(project1Path);
92+
AddProjectToSolution(project1Path, solution1Path);
93+
94+
var solution2Path = GetFullPath("Solution2/Solution2.sln");
95+
CreateSolution(solution2Path);
96+
var project2Path = GetFullPath("Solution2/Project2/Project2.csproj");
97+
CreateProject(project2Path);
98+
AddProjectToSolution(project2Path, solution2Path);
99+
100+
if (dryRun) CreateSnapshot();
101+
102+
// Act
103+
await RunAndAssertSuccess(
104+
"create-solution-filters",
105+
"--from", "Solution*/Solution*.sln",
106+
"--target", "Consolidated.sln",
107+
DryRunOption(dryRun));
108+
109+
// Assert
110+
if (dryRun)
111+
{
112+
VerifySnapshot();
113+
return;
114+
}
115+
116+
var projectsInTarget = GetProjectsFromSolution(targetSolutionPath);
117+
projectsInTarget.Should().Contain(project1Path);
118+
projectsInTarget.Should().Contain(project2Path);
119+
}
120+
121+
[Theory, CombinatorialData]
122+
public async Task It_handles_multiple_projects_per_solution(bool dryRun)
123+
{
124+
// Arrange
125+
var solution1Path = GetFullPath("Solution1/Solution1.sln");
126+
CreateSolution(solution1Path);
127+
var project1APath = GetFullPath("Solution1/ProjectA/ProjectA.csproj");
128+
var project1BPath = GetFullPath("Solution1/ProjectB/ProjectB.csproj");
129+
CreateProject(project1APath);
130+
CreateProject(project1BPath);
131+
AddProjectToSolution(project1APath, solution1Path);
132+
AddProjectToSolution(project1BPath, solution1Path);
133+
134+
var solution2Path = GetFullPath("Solution2/Solution2.sln");
135+
CreateSolution(solution2Path);
136+
var project2APath = GetFullPath("Solution2/ProjectC/ProjectC.csproj");
137+
var project2BPath = GetFullPath("Solution2/ProjectD/ProjectD.csproj");
138+
CreateProject(project2APath);
139+
CreateProject(project2BPath);
140+
AddProjectToSolution(project2APath, solution2Path);
141+
AddProjectToSolution(project2BPath, solution2Path);
142+
143+
var targetSolutionPath = GetFullPath("Consolidated.sln");
144+
145+
if (dryRun) CreateSnapshot();
146+
147+
// Act
148+
await RunAndAssertSuccess(
149+
"create-solution-filters",
150+
"--from", "Solution*/Solution*.sln",
151+
"--target", "Consolidated.sln",
152+
DryRunOption(dryRun));
153+
154+
// Assert
155+
if (dryRun)
156+
{
157+
VerifySnapshot();
158+
return;
159+
}
160+
161+
var projectsInTarget = GetProjectsFromSolution(targetSolutionPath);
162+
projectsInTarget.Should().Contain(new[] { project1APath, project1BPath, project2APath, project2BPath });
163+
164+
VerifyFilterFile(Path.ChangeExtension(solution1Path, ".slnf"), "Solution1", targetSolutionPath, new[] { project1APath, project1BPath });
165+
VerifyFilterFile(Path.ChangeExtension(solution2Path, ".slnf"), "Solution2", targetSolutionPath, new[] { project2APath, project2BPath });
166+
}
167+
168+
[Theory, CombinatorialData]
169+
public async Task It_handles_single_solution(bool dryRun)
170+
{
171+
// Arrange
172+
var solution1Path = GetFullPath("Solution1/Solution1.sln");
173+
CreateSolution(solution1Path);
174+
var project1Path = GetFullPath("Solution1/Project1/Project1.csproj");
175+
CreateProject(project1Path);
176+
AddProjectToSolution(project1Path, solution1Path);
177+
178+
var targetSolutionPath = GetFullPath("Consolidated.sln");
179+
180+
if (dryRun) CreateSnapshot();
181+
182+
// Act
183+
await RunAndAssertSuccess(
184+
"create-solution-filters",
185+
"--from", "Solution*/Solution*.sln",
186+
"--target", "Consolidated.sln",
187+
DryRunOption(dryRun));
188+
189+
// Assert
190+
if (dryRun)
191+
{
192+
VerifySnapshot();
193+
return;
194+
}
195+
196+
File.Exists(targetSolutionPath).Should().BeTrue();
197+
var projectsInTarget = GetProjectsFromSolution(targetSolutionPath);
198+
projectsInTarget.Should().Contain(project1Path);
199+
200+
var filterPath = Path.ChangeExtension(solution1Path, ".slnf");
201+
File.Exists(filterPath).Should().BeTrue();
202+
VerifyFilterFile(filterPath, "Solution1", targetSolutionPath, new[] { project1Path });
203+
}
204+
205+
private void VerifyFilterFile(string filterPath, string expectedFilterName, string targetSolutionPath, string[] expectedProjectPaths)
206+
{
207+
File.Exists(filterPath).Should().BeTrue($"Filter file {filterPath} should exist");
208+
209+
var json = File.ReadAllText(filterPath);
210+
var filter = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
211+
212+
filter.Should().NotBeNull();
213+
filter.Should().ContainKey("version");
214+
((JsonElement)filter!["defaultFilter"]).GetString().Should().Be(expectedFilterName);
215+
filter.Should().ContainKey("filters");
216+
217+
var filtersObj = (JsonElement)filter["filters"];
218+
filtersObj.TryGetProperty(expectedFilterName, out var filterDef).Should().BeTrue();
219+
220+
var targetDir = Path.GetDirectoryName(targetSolutionPath)!;
221+
var expectedRelativePath = Path.GetRelativePath(targetDir, targetSolutionPath);
222+
223+
filterDef.TryGetProperty("path", out var pathElement).Should().BeTrue();
224+
pathElement.GetString().Should().Be(expectedRelativePath);
225+
226+
filterDef.TryGetProperty("includes", out var includesElement).Should().BeTrue();
227+
var includes = includesElement.EnumerateArray().Select(e => e.GetString()).ToList();
228+
229+
var expectedIncludes = expectedProjectPaths
230+
.Select(p => Path.GetRelativePath(targetDir, p))
231+
.OrderBy(p => p)
232+
.ToList();
233+
234+
includes.Should().BeEquivalentTo(expectedIncludes);
235+
}
236+
237+
public CreateSolutionFiltersTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
238+
{
239+
}
240+
}

0 commit comments

Comments
 (0)