Skip to content

Commit 9fdb032

Browse files
committed
Nest snapshots under razor parents and Content items
DependentUpon always pointed at <name>.cs. A Blazor test class sits in <name>.razor.cs, or in the <name>.razor itself, so nothing nested. The parent extension is now probed for, guarded on the project holding razor files so other projects pay nothing for it. The razor and web SDKs claim extensions like .json and .config as Content and remove them from None. Those items only exist once the SDK props have been evaluated, so Content nesting is applied from Verify.targets, and only where nothing has already set a parent. The snapshot glob also gained the default item excludes, so copies under bin and obj are no longer pulled into the project. VerifyTests/Verify.Bunit#108
1 parent 5bacd6b commit 9fdb032

11 files changed

Lines changed: 343 additions & 9 deletions

readme.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -934,26 +934,41 @@ This readme will not discuss definitive list of details for proper setup of the
934934

935935
## Project inclusion for `*.received.*` and `*.verified.*` files
936936

937-
Verify comes with default MSBuild includes for snapshot files (`*.received.*` and `*.verified.*`) that nests those files under the test that produced them. C#, VB and F# projects are supported.
937+
Verify comes with default MSBuild includes for snapshot files (`*.received.*` and `*.verified.*`) that nests those files under the test that produced them. C#, VB and F# projects are supported. In Blazor projects, snapshots nest under the `.razor.cs` code-behind of the component under test, or under the `.razor` file when that component has no code-behind.
938938

939939
<!-- snippet: Verify.AfterMicrosoftNetSdk.props -->
940940
<a id='snippet-Verify.AfterMicrosoftNetSdk.props'></a>
941941
```props
942942
<?xml version="1.0" encoding="utf-8"?>
943943
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
944+
<!--
945+
Probing for a Blazor parent costs a file system hit per snapshot file, so it is only done in
946+
projects that contain razor files. A code behind is included in that, since a test class can sit
947+
in one without the project holding the matching .razor.
948+
-->
949+
<ItemGroup Condition="('$(DisableVerifyFileNesting)' != 'true') And $(Language) == 'C#'">
950+
<VerifyRazorFile Include="**\*.razor;**\*.razor.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
951+
</ItemGroup>
952+
<PropertyGroup Condition="('$(DisableVerifyFileNesting)' != 'true')">
953+
<VerifyRazorFileCount>@(VerifyRazorFile->Count())</VerifyRazorFileCount>
954+
</PropertyGroup>
944955
<ItemGroup Condition="('$(DisableVerifyFileNesting)' != 'true')">
945-
<None Include="**\*.received.*;**\*.verified.*" Condition="$(Language) == 'C#'">
956+
<None Include="**\*.received.*;**\*.verified.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" Condition="$(Language) == 'C#'">
946957
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0].Split('(')[0])</ParentFile>
947-
<DependentUpon>%(ParentFile).cs</DependentUpon>
958+
<ParentExtension>.cs</ParentExtension>
959+
<!-- A Blazor component keeps its code in a .razor.cs code-behind, or in the .razor file itself -->
960+
<ParentExtension Condition="'$(VerifyRazorFileCount)' != '0' And Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).razor.cs')">.razor.cs</ParentExtension>
961+
<ParentExtension Condition="'$(VerifyRazorFileCount)' != '0' And '%(ParentExtension)' == '.cs' And !Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).cs') And Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).razor')">.razor</ParentExtension>
962+
<DependentUpon>%(ParentFile)%(ParentExtension)</DependentUpon>
948963
</None>
949-
<None Include="**\*.received.*;**\*.verified.*" Condition="$(Language) == 'VB'">
964+
<None Include="**\*.received.*;**\*.verified.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" Condition="$(Language) == 'VB'">
950965
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0].Split('(')[0])</ParentFile>
951966
<DependentUpon>%(ParentFile).vb</DependentUpon>
952967
</None>
953968
</ItemGroup>
954969
</Project>
955970
```
956-
<sup><a href='/src/Verify/buildTransitive/Verify.AfterMicrosoftNetSdk.props#L1-L13' title='Snippet source file'>snippet source</a> | <a href='#snippet-Verify.AfterMicrosoftNetSdk.props' title='Start of snippet'>anchor</a></sup>
971+
<sup><a href='/src/Verify/buildTransitive/Verify.AfterMicrosoftNetSdk.props#L1-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-Verify.AfterMicrosoftNetSdk.props' title='Start of snippet'>anchor</a></sup>
957972
<!-- endSnippet -->
958973

959974
To opt out of this feature, include the following in the project file:

readme.source.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ This readme will not discuss definitive list of details for proper setup of the
383383

384384
## Project inclusion for `*.received.*` and `*.verified.*` files
385385

386-
Verify comes with default MSBuild includes for snapshot files (`*.received.*` and `*.verified.*`) that nests those files under the test that produced them. C#, VB and F# projects are supported.
386+
Verify comes with default MSBuild includes for snapshot files (`*.received.*` and `*.verified.*`) that nests those files under the test that produced them. C#, VB and F# projects are supported. In Blazor projects, snapshots nest under the `.razor.cs` code-behind of the component under test, or under the `.razor` file when that component has no code-behind.
387387

388388
snippet: Verify.AfterMicrosoftNetSdk.props
389389

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
None
2+
Tests.Simple.received.txt: Tests.cs
3+
Tests.Simple.verified.json: Tests.cs
4+
Tests.Simple.verified.txt: Tests.cs
5+
Content
6+
none
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
None
2+
CodeBehindTests.Component.verified.html: CodeBehindTests.razor.cs
3+
CodeBehindTests.Component.verified.txt: CodeBehindTests.razor.cs
4+
Content
5+
none
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
None
2+
ComponentTests.Simple.verified.html: ComponentTests.razor.cs
3+
NoCodeBehindTests.Simple.verified.html: NoCodeBehindTests.razor
4+
PlainTests.Simple.verified.txt: PlainTests.cs
5+
Sub/SubComponentTests.Simple.verified.html: SubComponentTests.razor.cs
6+
Content
7+
ComponentTests.Simple.verified.json: ComponentTests.razor.cs
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
None
2+
ComponentTests.Simple.verified.html: not nested
3+
Content
4+
ComponentTests.Simple.verified.json: not nested
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
None
2+
Tests.Simple.verified.txt: Tests.vb
3+
Content
4+
none
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
None
2+
Tests.Simple.verified.txt: Tests.cs
3+
Content
4+
Tests.Explicit.verified.json: Other.cs
5+
Tests.Simple.verified.config: Tests.cs
6+
Tests.Simple.verified.json: Tests.cs
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
#if NET10_0
2+
3+
using System.Text.Json;
4+
5+
// Snapshot nesting is applied during evaluation, because evaluated items are what Solution Explorer
6+
// reads. So these tests evaluate a project and inspect its items, rather than building it.
7+
public class FileNestingTests
8+
{
9+
[Fact]
10+
public async Task CSharpProject() =>
11+
await Verify(
12+
await Evaluate(
13+
CSharp("Microsoft.NET.Sdk"),
14+
"Tests.cs",
15+
"Tests.Simple.verified.txt",
16+
// outside the razor and web SDKs, json snapshots are None like any other extension
17+
"Tests.Simple.verified.json",
18+
"Tests.Simple.received.txt",
19+
// a snapshot copied to the intermediate directory is not part of the project
20+
"obj/Debug/net10.0/Tests.Simple.verified.txt"));
21+
22+
[Fact]
23+
public async Task RazorProject() =>
24+
await Verify(
25+
await Evaluate(
26+
CSharp("Microsoft.NET.Sdk.Razor"),
27+
// a component with a code behind: snapshots nest under the code behind
28+
"ComponentTests.razor",
29+
"ComponentTests.razor.cs",
30+
"ComponentTests.Simple.verified.html",
31+
// the razor and web SDKs claim json as Content rather than None
32+
"ComponentTests.Simple.verified.json",
33+
// a component without a code behind: the snapshot nests under the razor file
34+
"NoCodeBehindTests.razor",
35+
"NoCodeBehindTests.Simple.verified.html",
36+
// a component in a sub directory
37+
"Sub/SubComponentTests.razor",
38+
"Sub/SubComponentTests.razor.cs",
39+
"Sub/SubComponentTests.Simple.verified.html",
40+
// a plain test class in a razor project still nests under the cs file
41+
"PlainTests.cs",
42+
"PlainTests.Simple.verified.txt"));
43+
44+
// A test class can sit in a code behind whose component lives in the project under test, so the
45+
// project holding the test has the .razor.cs but not the .razor.
46+
[Fact]
47+
public async Task ProjectWithOnlyACodeBehind() =>
48+
await Verify(
49+
await Evaluate(
50+
CSharp("Microsoft.NET.Sdk"),
51+
"CodeBehindTests.razor.cs",
52+
"CodeBehindTests.Component.verified.html",
53+
"CodeBehindTests.Component.verified.txt"));
54+
55+
[Fact]
56+
public async Task RazorProjectWithNestingDisabled() =>
57+
await Verify(
58+
await Evaluate(
59+
CSharp(
60+
"Microsoft.NET.Sdk.Razor",
61+
properties: "<DisableVerifyFileNesting>true</DisableVerifyFileNesting>"),
62+
"ComponentTests.razor",
63+
"ComponentTests.razor.cs",
64+
"ComponentTests.Simple.verified.html",
65+
"ComponentTests.Simple.verified.json"));
66+
67+
[Fact]
68+
public async Task WebProject() =>
69+
await Verify(
70+
await Evaluate(
71+
CSharp(
72+
"Microsoft.NET.Sdk.Web",
73+
body:
74+
"""
75+
<ItemGroup>
76+
<Content Update="Tests.Explicit.verified.json" DependentUpon="Other.cs" />
77+
</ItemGroup>
78+
"""),
79+
"Tests.cs",
80+
"Other.cs",
81+
// a web project has no razor files, so no parent is probed for
82+
"Tests.Simple.verified.json",
83+
"Tests.Simple.verified.config",
84+
"Tests.Simple.verified.txt",
85+
// an explicitly nested snapshot keeps the parent it was given
86+
"Tests.Explicit.verified.json"));
87+
88+
[Fact]
89+
public async Task VisualBasicProject() =>
90+
await Verify(
91+
await Evaluate(
92+
VisualBasic(),
93+
"Tests.vb",
94+
"Tests.Simple.verified.txt"));
95+
96+
static (string file, string content) CSharp(string sdk, string properties = "", string body = "") =>
97+
("TestProject.csproj", ProjectContent(sdk, properties, body));
98+
99+
static (string file, string content) VisualBasic() =>
100+
("TestProject.vbproj", ProjectContent("Microsoft.NET.Sdk", "", ""));
101+
102+
static string ProjectContent(string sdk, string properties, string body) =>
103+
$"""
104+
<Project Sdk="{sdk}">
105+
<PropertyGroup>
106+
<TargetFramework>net10.0</TargetFramework>
107+
{properties}
108+
</PropertyGroup>
109+
{body}
110+
</Project>
111+
""";
112+
113+
static async Task<string> Evaluate((string file, string content) project, params string[] files)
114+
{
115+
using var directory = new TempDirectory();
116+
var path = directory.Path;
117+
118+
// The package imports these two through the buildTransitive convention, which lands them
119+
// either side of the SDK. Directory.Build.props and .targets are those same two points.
120+
await WriteFile(path, "Directory.Build.props", Import("Verify.props"));
121+
await WriteFile(path, "Directory.Build.targets", Import("Verify.targets"));
122+
await WriteFile(path, project.file, project.content);
123+
124+
foreach (var file in files)
125+
{
126+
await WriteFile(path, file, "");
127+
}
128+
129+
return Format(await RunMsBuild(Path.Combine(path, project.file)));
130+
}
131+
132+
static string Import(string file)
133+
{
134+
var full = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "buildTransitive", file);
135+
return $"""
136+
<Project>
137+
<Import Project="{full}" />
138+
</Project>
139+
""";
140+
}
141+
142+
static async Task WriteFile(string directory, string relativePath, string content)
143+
{
144+
var path = Path.Combine(directory, relativePath);
145+
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
146+
await File.WriteAllTextAsync(path, content);
147+
}
148+
149+
static string Format(string json)
150+
{
151+
using var document = JsonDocument.Parse(json);
152+
var items = document.RootElement.GetProperty("Items");
153+
var builder = new StringBuilder();
154+
string[] types = ["None", "Content"];
155+
foreach (var type in types)
156+
{
157+
builder.AppendLine(type);
158+
159+
var snapshots = Snapshots(items, type);
160+
if (snapshots.Count == 0)
161+
{
162+
builder.AppendLine(" none");
163+
continue;
164+
}
165+
166+
foreach (var snapshot in snapshots)
167+
{
168+
builder.AppendLine($" {snapshot.Key}: {Parent(snapshot)}");
169+
}
170+
}
171+
172+
return builder.ToString();
173+
}
174+
175+
// Verify adds its own item for a snapshot on top of the one the SDK globbed, so the same file
176+
// can appear more than once. Any disagreement between those items is a bug.
177+
static string Parent(IEnumerable<(string Identity, string? Parent)> snapshot)
178+
{
179+
var parents = snapshot
180+
.Select(_ => _.Parent)
181+
.Where(_ => _ != null)
182+
.Distinct()
183+
.ToList();
184+
if (parents.Count == 0)
185+
{
186+
return "not nested";
187+
}
188+
189+
return string.Join(" and ", parents);
190+
}
191+
192+
static List<IGrouping<string, (string Identity, string? Parent)>> Snapshots(JsonElement items, string type)
193+
{
194+
if (!items.TryGetProperty(type, out var ofType))
195+
{
196+
return [];
197+
}
198+
199+
return ofType
200+
.EnumerateArray()
201+
.Select(_ => (
202+
// MSBuild uses the platform separator, and these paths are part of the snapshot
203+
Identity: _.GetProperty("Identity").GetString()!.Replace('\\', '/'),
204+
Parent: _.TryGetProperty("DependentUpon", out var parent) ? parent.GetString() : null))
205+
.Where(_ => _.Identity.Contains(".verified.") ||
206+
_.Identity.Contains(".received."))
207+
.GroupBy(_ => _.Identity)
208+
.OrderBy(_ => _.Key, StringComparer.Ordinal)
209+
.ToList();
210+
}
211+
212+
static async Task<string> RunMsBuild(string projectPath)
213+
{
214+
var startInfo = new ProcessStartInfo
215+
{
216+
FileName = "dotnet",
217+
RedirectStandardOutput = true,
218+
RedirectStandardError = true,
219+
UseShellExecute = false,
220+
CreateNoWindow = true
221+
};
222+
223+
var arguments = startInfo.ArgumentList;
224+
arguments.Add("msbuild");
225+
arguments.Add(projectPath);
226+
arguments.Add("-getItem:None");
227+
arguments.Add("-getItem:Content");
228+
arguments.Add("-nologo");
229+
230+
using var process = Process.Start(startInfo)!;
231+
232+
var outputTask = process.StandardOutput.ReadToEndAsync();
233+
var errorTask = process.StandardError.ReadToEndAsync();
234+
235+
await process.WaitForExitAsync();
236+
237+
var output = await outputTask;
238+
var error = await errorTask;
239+
240+
if (process.ExitCode == 0 &&
241+
output.TrimStart().StartsWith('{'))
242+
{
243+
return output;
244+
}
245+
246+
throw new($"Evaluation of {projectPath} failed:{Environment.NewLine}{output}{Environment.NewLine}{error}");
247+
}
248+
}
249+
250+
#endif

src/Verify/buildTransitive/Verify.AfterMicrosoftNetSdk.props

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<!--
4+
Probing for a Blazor parent costs a file system hit per snapshot file, so it is only done in
5+
projects that contain razor files. A code behind is included in that, since a test class can sit
6+
in one without the project holding the matching .razor.
7+
-->
8+
<ItemGroup Condition="('$(DisableVerifyFileNesting)' != 'true') And $(Language) == 'C#'">
9+
<VerifyRazorFile Include="**\*.razor;**\*.razor.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
10+
</ItemGroup>
11+
<PropertyGroup Condition="('$(DisableVerifyFileNesting)' != 'true')">
12+
<VerifyRazorFileCount>@(VerifyRazorFile->Count())</VerifyRazorFileCount>
13+
</PropertyGroup>
314
<ItemGroup Condition="('$(DisableVerifyFileNesting)' != 'true')">
4-
<None Include="**\*.received.*;**\*.verified.*" Condition="$(Language) == 'C#'">
15+
<None Include="**\*.received.*;**\*.verified.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" Condition="$(Language) == 'C#'">
516
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0].Split('(')[0])</ParentFile>
6-
<DependentUpon>%(ParentFile).cs</DependentUpon>
17+
<ParentExtension>.cs</ParentExtension>
18+
<!-- A Blazor component keeps its code in a .razor.cs code-behind, or in the .razor file itself -->
19+
<ParentExtension Condition="'$(VerifyRazorFileCount)' != '0' And Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).razor.cs')">.razor.cs</ParentExtension>
20+
<ParentExtension Condition="'$(VerifyRazorFileCount)' != '0' And '%(ParentExtension)' == '.cs' And !Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).cs') And Exists('$(MSBuildProjectDirectory)\%(RelativeDir)%(ParentFile).razor')">.razor</ParentExtension>
21+
<DependentUpon>%(ParentFile)%(ParentExtension)</DependentUpon>
722
</None>
8-
<None Include="**\*.received.*;**\*.verified.*" Condition="$(Language) == 'VB'">
23+
<None Include="**\*.received.*;**\*.verified.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" Condition="$(Language) == 'VB'">
924
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0].Split('(')[0])</ParentFile>
1025
<DependentUpon>%(ParentFile).vb</DependentUpon>
1126
</None>

0 commit comments

Comments
 (0)