test(scripts): pin null-result and definition-agnostic caching; review polish

This commit is contained in:
Joseph Doherty
2026-08-12 16:42:27 -04:00
parent b1c3783578
commit 702de910ad
2 changed files with 82 additions and 3 deletions
@@ -37,6 +37,27 @@ public class CachingScriptMetadataResolverTests
public override int GetHashCode() => 0;
}
/// <summary>Inner resolver that finds nothing — models an assembly absent from every search path.</summary>
private sealed class NullReturningResolver : MetadataReferenceResolver
{
public int MissingCalls;
public override bool ResolveMissingAssemblies => true;
public override PortableExecutableReference? ResolveMissingAssembly(
MetadataReference definition, AssemblyIdentity referenceIdentity)
{
Interlocked.Increment(ref MissingCalls);
return null;
}
public override ImmutableArray<PortableExecutableReference> ResolveReference(
string reference, string? baseFilePath, MetadataReferenceProperties properties)
=> [];
public override bool Equals(object? other) => ReferenceEquals(this, other);
public override int GetHashCode() => 0;
}
private static readonly AssemblyIdentity SomeIdentity = new("System.Fake", new Version(1, 0, 0, 0));
private static readonly MetadataReference SomeDefinition =
MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
@@ -66,6 +87,52 @@ public class CachingScriptMetadataResolverTests
Assert.Equal(2, inner.MissingCalls);
}
/// <summary>
/// A not-found result is cached and sticky BY DESIGN: the inner resolver
/// searches a fixed set of directories that does not change over a node's
/// lifetime, so re-asking for an assembly that was not there only pays the
/// probe cost again. Pinned because caching nulls is the kind of thing a
/// future reader "fixes" without realising it is deliberate.
/// </summary>
[Fact]
public void ResolveMissingAssembly_NotFound_CachesTheNullResult()
{
var inner = new NullReturningResolver();
var sut = new CachingScriptMetadataResolver(inner);
var first = sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity);
var second = sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity);
Assert.Equal(1, inner.MissingCalls);
Assert.Null(first);
Assert.Null(second);
}
/// <summary>
/// Locks in the documented cache-key contract: the key is the assembly
/// identity ALONE, and the requesting <c>definition</c> (whose directory is a
/// search path in the inner resolver) is deliberately ignored, because every
/// ScadaBridge node resolves from one publish directory plus the shared
/// framework. If that assumption ever stops holding, this test fails and the
/// widening becomes an intentional diff rather than a silent one.
/// </summary>
[Fact]
public void ResolveMissingAssembly_SameIdentityDifferentDefinitions_ResolvesOnce()
{
var otherDefinition = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
Assert.NotEqual(
typeof(object).Assembly.Location,
typeof(Enumerable).Assembly.Location); // guard: the two definitions really are distinct files
var inner = new CountingResolver();
var sut = new CachingScriptMetadataResolver(inner);
sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity);
sut.ResolveMissingAssembly(otherDefinition, SomeIdentity);
Assert.Equal(1, inner.MissingCalls);
}
[Fact]
public void ResolveReference_SameArgsTwice_ResolvesOnceAndSharesInstances()
{