Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/DiffEngine.Tests/BundledViewerRootTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// <summary>
/// Which stamped path the bundled viewer is looked for under.
/// <para>
/// 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.
/// </para>
/// </summary>
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();
}
}
77 changes: 55 additions & 22 deletions src/DiffEngine/Viewer/BundledViewerDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,46 +34,79 @@ static class BundledViewerDirectory
return null;
}

/// <summary>
/// The first candidate naming a directory that is on this machine.
/// <para>
/// 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.
/// </para>
/// </summary>
internal static string? FirstUsable(IEnumerable<string?> 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
/// <summary>
/// .NET Framework has no runtimeconfig to carry the path, so it is read from the metadata
/// attribute the targets add to the consuming assembly.
/// <para>
/// 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.
/// </para>
/// </summary>
static string? FindRoot()
static string? FindRoot() =>
FirstUsable(Roots());

static IEnumerable<string?> 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;
Expand Down
Loading