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;