Skip to content

Commit 9015c45

Browse files
committed
Label inline patches with the project's framework, not the runner's
RuntimeMoniker read AppContext.TargetFrameworkName, which is the entry assembly's TargetFrameworkAttribute. In a hosted test run the entry assembly is the runner - testhost is net8.0, ReSharperTestRunner is netcoreapp3.0 - so every leg of a multi-targeted run stamped the runner's one moniker, and the queue could neither keep the legs' variants apart nor settle one without the other. buildTransitive/DiffEngine.targets now stamps the consuming project's own $(TargetFramework) into its runtimeconfig, per leg, the same way the bundled viewer path already travels. RuntimeMoniker reads that first and falls back to the running runtime's version for consumers without the targets. .NET Framework keeps the attribute path: it has no runtimeconfig to carry a stamp, and every net4x moniker runs on the one CLR. The tests project stamps itself the way a consumer's build does, so the suite runs the configured path end to end, and BuildTargetsTests ties the targets XML to the C# constants reading it.
1 parent 9dbe2d6 commit 9015c45

8 files changed

Lines changed: 80 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ nul
1313
/coverage
1414
BenchmarkDotNet.Artifacts/
1515
native/build/
16+
/src/TestResults

docs/inline.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ For the producing side — a test library with a failing inline snapshot:
7777

7878
* `DiffRunner.AddInlineAsync(patch)` queues a patch with whatever owns the port, launching the bundled viewer when nothing does. Returns `Queued`, `Disabled` (build servers, continuous testing and AI CLIs included), or `NoViewerFound` — the caller's cue to stage files and fall back to a text diff.
7979
* `DiffRunner.SettleInline(sourceFile, line)` drops the pending entry for a call site, for when a previously failing test passes. Unknown entries and an absent owner are no-ops, so call it freely. The settle carries the running framework, so a multi-targeted run only settles its own variant of a conflicted entry.
80-
* `AddInlineAsync` stamps `patch.Framework` with the running process's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. Callers may also set `patch.TestName`, which the viewer uses to group and label the queue; without it, items are labeled by call site.
80+
* `AddInlineAsync` stamps `patch.Framework` with the consuming project's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. The value is the `$(TargetFramework)` the package's build targets stamp into the project's runtimeconfig, read back rather than asked of the process — in a hosted test run the entry assembly is the runner (testhost, ReSharperTestRunner), whose framework is not the project's — with the running runtime's version as the fallback for consumers without the targets. Callers may also set `patch.TestName`, which the viewer uses to group and label the queue; without it, items are labeled by call site.
8181
* Set `patch.OriginalExpression` from `CallerArgumentExpression` where the language supplies one, and `patch.OriginalValue` — the previous expected argument's value — where it does not. One of the two is what stops a patch rewriting the wrong call site when the file has moved since the run. `patch.MemberName` from `CallerMemberName` narrows it further, and is supported everywhere including F#.
8282
* Setting `DiffEngine_InlineViewer` to `false` reports `NoViewerFound` without probing, which is how a user opts into reviewing in their IDE instead of a window.
8383

docs/mdsource/inline.source.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ For the producing side — a test library with a failing inline snapshot:
7070

7171
* `DiffRunner.AddInlineAsync(patch)` queues a patch with whatever owns the port, launching the bundled viewer when nothing does. Returns `Queued`, `Disabled` (build servers, continuous testing and AI CLIs included), or `NoViewerFound` — the caller's cue to stage files and fall back to a text diff.
7272
* `DiffRunner.SettleInline(sourceFile, line)` drops the pending entry for a call site, for when a previously failing test passes. Unknown entries and an absent owner are no-ops, so call it freely. The settle carries the running framework, so a multi-targeted run only settles its own variant of a conflicted entry.
73-
* `AddInlineAsync` stamps `patch.Framework` with the running process's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. Callers may also set `patch.TestName`, which the viewer uses to group and label the queue; without it, items are labeled by call site.
73+
* `AddInlineAsync` stamps `patch.Framework` with the consuming project's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. The value is the `$(TargetFramework)` the package's build targets stamp into the project's runtimeconfig, read back rather than asked of the process — in a hosted test run the entry assembly is the runner (testhost, ReSharperTestRunner), whose framework is not the project's — with the running runtime's version as the fallback for consumers without the targets. Callers may also set `patch.TestName`, which the viewer uses to group and label the queue; without it, items are labeled by call site.
7474
* Set `patch.OriginalExpression` from `CallerArgumentExpression` where the language supplies one, and `patch.OriginalValue` — the previous expected argument's value — where it does not. One of the two is what stops a patch rewriting the wrong call site when the file has moved since the run. `patch.MemberName` from `CallerMemberName` narrows it further, and is supported everywhere including F#.
7575
* Setting `DiffEngine_InlineViewer` to `false` reports `NoViewerFound` without probing, which is how a user opts into reviewing in their IDE instead of a window.
7676

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <summary>
2+
/// buildTransitive/DiffEngine.targets writes values that C# constants read back, and nothing
3+
/// else holds the two halves together: a key renamed in one place would not fail to compile, it
4+
/// would silently stop the stamp arriving. The targets file is linked into the test output, so
5+
/// this runs from source rather than from a packed nupkg.
6+
/// </summary>
7+
public class BuildTargetsTests
8+
{
9+
[Test]
10+
public async Task CarriesTheKeysTheLibraryReads()
11+
{
12+
var targets = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "DiffEngine.targets"));
13+
await Assert.That(targets).Contains(RuntimeMoniker.Key);
14+
await Assert.That(targets).Contains(BundledViewerDirectory.Key);
15+
}
16+
}

src/DiffEngine.Tests/DiffEngine.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
<PackageReference Include="Argon" />
1414
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
1515
<ProjectReference Include="..\DiffEngine\DiffEngine.csproj" />
16+
<!-- The stamp consumers get from buildTransitive/DiffEngine.targets, so the suite runs the
17+
configured RuntimeMoniker path rather than only its fallback. -->
18+
<RuntimeHostConfigurationOption Include="DiffEngine.TargetFramework" Value="$(TargetFramework)" />
19+
<!-- For BuildTargetsTests, which ties the targets XML to the C# constants reading it. -->
20+
<None Include="..\DiffEngine\buildTransitive\DiffEngine.targets" Link="DiffEngine.targets" CopyToOutputDirectory="PreserveNewest" />
1621
<Compile Remove="DefinitionsTest.cs" Condition="'$(TargetFramework)' == 'net9.0'" />
1722
<PackageReference Include="TUnit" />
1823
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />

src/DiffEngine.Tests/RuntimeMonikerTests.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,23 @@ public async Task Maps(string frameworkName, string expected) =>
2424
public async Task UnknownIsNull(string? frameworkName) =>
2525
await Assert.That(RuntimeMoniker.Map(frameworkName)).IsNull();
2626

27+
#if !NETFRAMEWORK
28+
/// <summary>
29+
/// The tests project stamps DiffEngine.TargetFramework the way the shipped targets stamp a
30+
/// consumer, so this asserts the whole configured path: MSBuild item to runtimeconfig to
31+
/// AppContext. Without the stamp the suite would only ever run the runtime-version fallback,
32+
/// and a broken key would not fail anything.
33+
/// </summary>
34+
[Test]
35+
public async Task StampReachesAppContext() =>
36+
await Assert.That(AppContext.GetData(RuntimeMoniker.Key)).IsEqualTo("net10.0");
37+
#endif
38+
2739
/// <summary>
2840
/// Runs on both test frameworks, so both socket-era families pin their own moniker. The
29-
/// .NET Framework side only asserts the family: the exact minor is the test host's business.
41+
/// .NET Core side takes the configured path, since the csproj stamps the key the way a
42+
/// consumer's build does; the .NET Framework side only asserts the family, because the exact
43+
/// minor is the test host's business.
3044
/// </summary>
3145
[Test]
3246
public async Task CurrentMatchesThisRuntime() =>

src/DiffEngine/Inline/RuntimeMoniker.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,35 @@
44
/// </summary>
55
static class RuntimeMoniker
66
{
7+
public const string Key = "DiffEngine.TargetFramework";
8+
79
/// <summary>
8-
/// Null when the framework cannot be determined, which downstream treats as an unlabeled
9-
/// origin rather than guessing.
10+
/// The consuming project's own $(TargetFramework) when buildTransitive/DiffEngine.targets
11+
/// stamped it into the runtimeconfig, otherwise the version of the runtime actually
12+
/// executing. Never AppContext.TargetFrameworkName: that is the entry assembly's
13+
/// TargetFrameworkAttribute, and in a hosted test run the entry assembly is the runner -
14+
/// testhost is net8.0, ReSharperTestRunner is netcoreapp3.0 - so every leg of a
15+
/// multi-targeted run stamped the runner's one moniker, and the queue could neither keep the
16+
/// legs' variants apart nor settle one without the other. The runtime fallback covers
17+
/// consumers without the targets, such as linked-source builds: each leg launches on its own
18+
/// runtime, so it still tells apart what the label exists to tell apart, at the cost of
19+
/// naming the rolled-forward runtime rather than the target.
20+
/// <para>
21+
/// .NET Framework keeps the attribute path, and with it null when nothing can be determined:
22+
/// it has no runtimeconfig to carry a stamp, its Environment.Version is the CLR's own
23+
/// (4.0.30319) whatever the target, and every net4x moniker runs on that one CLR.
24+
/// </para>
1025
/// </summary>
11-
public static string? Current { get; } = Map(FrameworkName());
26+
public static string? Current { get; } =
27+
#if NETFRAMEWORK
28+
Map(FrameworkName());
29+
#else
30+
AppContext.GetData(Key) is string { Length: > 0 } configured
31+
? configured
32+
: $"net{Environment.Version.Major}.{Environment.Version.Minor}";
33+
#endif
1234

35+
#if NETFRAMEWORK
1336
static string? FrameworkName() =>
1437
#if NET462
1538
// AppContext.TargetFrameworkName arrived in 4.7.1; this is what it reads there anyway.
@@ -18,6 +41,7 @@ static class RuntimeMoniker
1841
AppContext.TargetFrameworkName ??
1942
#endif
2043
Assembly.GetEntryAssembly()?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName;
44+
#endif
2145

2246
// Internal seam for tests; the input shape is "{identifier},Version=v{version}[,Profile=...]".
2347
internal static string? Map(string? frameworkName)

src/DiffEngine/buildTransitive/DiffEngine.targets

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,18 @@
3131
</AssemblyAttribute>
3232
</ItemGroup>
3333

34+
<!--
35+
The consuming project's own target framework, one stamp per multi-target leg. RuntimeMoniker
36+
prefers this over anything the process can tell it: in a hosted test run the entry assembly is
37+
the runner - testhost, ReSharperTestRunner - compiled for its own framework rather than the
38+
project's, so every leg of a multi-targeted run would otherwise carry the same label. Not
39+
mirrored into assembly metadata for .NET Framework the way the viewer path is: metadata lands
40+
on every stamped assembly, class libraries included, so a reader could not tell the test
41+
project's stamp from a dependency's.
42+
-->
43+
<ItemGroup>
44+
<RuntimeHostConfigurationOption Include="DiffEngine.TargetFramework"
45+
Value="$(TargetFramework)" />
46+
</ItemGroup>
47+
3448
</Project>

0 commit comments

Comments
 (0)