using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Scripting; namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests; /// /// Pins the per-compile native-metadata fix (2026-08-12 follow-up to 5a781c70): /// Roslyn's default ScriptMetadataResolver re-resolves the transitive assembly /// closure through MetadataReference.CreateFromFile on EVERY script compile — /// each call minting a fresh AssemblyMetadata → PEReader → NativeHeapMemoryBlock. /// The decorator memoizes so each distinct assembly is resolved once per process. /// public class CachingScriptMetadataResolverTests { private sealed class CountingResolver : MetadataReferenceResolver { public int MissingCalls; public int ReferenceCalls; public override bool ResolveMissingAssemblies => true; public override PortableExecutableReference? ResolveMissingAssembly( MetadataReference definition, AssemblyIdentity referenceIdentity) { Interlocked.Increment(ref MissingCalls); return MetadataReference.CreateFromFile(typeof(object).Assembly.Location); } public override ImmutableArray ResolveReference( string reference, string? baseFilePath, MetadataReferenceProperties properties) { Interlocked.Increment(ref ReferenceCalls); return [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]; } 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); [Fact] public void ResolveMissingAssembly_SameIdentityTwice_ResolvesOnceAndSharesInstance() { var inner = new CountingResolver(); var sut = new CachingScriptMetadataResolver(inner); var first = sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity); var second = sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity); Assert.Equal(1, inner.MissingCalls); Assert.Same(first, second); // one AssemblyMetadata/PEReader/native block, not two } [Fact] public void ResolveMissingAssembly_DistinctIdentities_ResolveIndependently() { var inner = new CountingResolver(); var sut = new CachingScriptMetadataResolver(inner); sut.ResolveMissingAssembly(SomeDefinition, SomeIdentity); sut.ResolveMissingAssembly(SomeDefinition, new AssemblyIdentity("System.Other", new Version(1, 0, 0, 0))); Assert.Equal(2, inner.MissingCalls); } [Fact] public void ResolveReference_SameArgsTwice_ResolvesOnceAndSharesInstances() { var inner = new CountingResolver(); var sut = new CachingScriptMetadataResolver(inner); var first = sut.ResolveReference("System.Xml", baseFilePath: null, MetadataReferenceProperties.Assembly); var second = sut.ResolveReference("System.Xml", baseFilePath: null, MetadataReferenceProperties.Assembly); Assert.Equal(1, inner.ReferenceCalls); Assert.Same(first[0], second[0]); } [Fact] public void Instance_IsProcessWideSingleton() => Assert.Same(CachingScriptMetadataResolver.Instance, CachingScriptMetadataResolver.Instance); [Fact] public void ResolveMissingAssemblies_DelegatesToInner() { var sut = new CachingScriptMetadataResolver(new CountingResolver()); Assert.True(sut.ResolveMissingAssemblies); } }