From d1ee6b08652c3fa25a74059f5efd029c2412fbb0 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 22 Aug 2026 22:57:46 +1000 Subject: [PATCH] Pass over a bundled viewer path that is not on this machine On .NET Framework the path to the bundled viewer is carried as assembly metadata, and every assembly built against the package carries one - prebuilt dependencies included, where it is the package path of the machine that built them. The scan took the first non-empty value it met and stopped, so a Verify.dll from NuGet arriving before the test assembly handed back a directory that does not exist here, the RID probe missed, and inline snapshots fell back to staging with a viewer sitting in the package all along. A value naming a directory that is not here is now passed over and the scan continues, and the entry assembly - the one the targets stamped for this build - is asked before the rest. Both frameworks pick their root through the same check, so where a root has to exist is stated once. --- .../BundledViewerRootTests.cs | 31 ++++++++ .../Viewer/BundledViewerDirectory.cs | 77 +++++++++++++------ 2 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 src/DiffEngine.Tests/BundledViewerRootTests.cs diff --git a/src/DiffEngine.Tests/BundledViewerRootTests.cs b/src/DiffEngine.Tests/BundledViewerRootTests.cs new file mode 100644 index 00000000..e02a14bd --- /dev/null +++ b/src/DiffEngine.Tests/BundledViewerRootTests.cs @@ -0,0 +1,31 @@ +/// +/// Which stamped path the bundled viewer is looked for under. +/// +/// On .NET Framework there can be several: the path is carried as assembly metadata, and every +/// assembly built against the package carries one - including prebuilt dependencies, which hold +/// the package path of the machine that built them. +/// +/// +public class BundledViewerRootTests +{ + [Test] + public async Task Passes_over_a_root_that_is_not_on_this_machine() + { + var stale = Path.Combine(Path.GetTempPath(), $"BundledViewerRootTests_{Guid.NewGuid()}"); + var here = Path.GetTempPath(); + + var root = BundledViewerDirectory.FirstUsable([null, "", stale, here]); + + await Assert.That(root).IsEqualTo(here); + } + + [Test] + public async Task Finds_nothing_when_no_root_is_on_this_machine() + { + var stale = Path.Combine(Path.GetTempPath(), $"BundledViewerRootTests_{Guid.NewGuid()}"); + + var root = BundledViewerDirectory.FirstUsable([stale]); + + await Assert.That(root).IsNull(); + } +} diff --git a/src/DiffEngine/Viewer/BundledViewerDirectory.cs b/src/DiffEngine/Viewer/BundledViewerDirectory.cs index abda2325..4fe5c050 100644 --- a/src/DiffEngine/Viewer/BundledViewerDirectory.cs +++ b/src/DiffEngine/Viewer/BundledViewerDirectory.cs @@ -34,46 +34,79 @@ static class BundledViewerDirectory return null; } + /// + /// The first candidate naming a directory that is on this machine. + /// + /// A stamped path is only as good as the machine it was stamped on, so one that is not here + /// is passed over rather than taken and then found wanting. + /// + /// + internal static string? FirstUsable(IEnumerable roots) + { + foreach (var root in roots) + { + if (root is {Length: > 0} && + Directory.Exists(root)) + { + return root; + } + } + + return null; + } + #if NET6_0_OR_GREATER static string? FindRoot() => - AppContext.GetData(Key) as string; + FirstUsable([AppContext.GetData(Key) as string]); #else /// /// .NET Framework has no runtimeconfig to carry the path, so it is read from the metadata /// attribute the targets add to the consuming assembly. + /// + /// Every stamped assembly carries one, prebuilt dependencies included: a Verify.dll from + /// NuGet holds the package path of the machine that built it, which on this one is not there. + /// So the entry assembly is asked first, and the rest are still asked after it. + /// /// - static string? FindRoot() + static string? FindRoot() => + FirstUsable(Roots()); + + static IEnumerable Roots() { + var entry = Assembly.GetEntryAssembly(); + if (entry != null) + { + yield return ReadRoot(entry); + } + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - if (assembly.IsDynamic) + if (assembly.IsDynamic || + assembly == entry) { continue; } - string? value = null; - try + yield return ReadRoot(assembly); + } + } + + static string? ReadRoot(Assembly assembly) + { + try + { + foreach (var attribute in assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)) { - foreach (var attribute in assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)) + var metadata = (AssemblyMetadataAttribute) attribute; + if (metadata.Key == Key) { - var metadata = (AssemblyMetadataAttribute) attribute; - if (metadata.Key == Key) - { - value = metadata.Value; - break; - } + return metadata.Value; } } - catch - { - // A reflection only or otherwise unreadable assembly cannot carry the path - continue; - } - - if (value is {Length: > 0}) - { - return value; - } + } + catch + { + // A reflection only or otherwise unreadable assembly cannot carry the path } return null;