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;