test(scripts): pin that repeat compiles resolve the assembly closure zero times
This commit is contained in:
+62
@@ -89,4 +89,66 @@ public class CachingScriptMetadataResolverTests
|
|||||||
var sut = new CachingScriptMetadataResolver(new CountingResolver());
|
var sut = new CachingScriptMetadataResolver(new CountingResolver());
|
||||||
Assert.True(sut.ResolveMissingAssemblies);
|
Assert.True(sut.ResolveMissingAssemblies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The decisive regression: with options shaped like the real compile
|
||||||
|
/// surfaces (small explicit set, large transitive closure), the FIRST
|
||||||
|
/// compile resolves the closure through the resolver and every subsequent
|
||||||
|
/// compile — same or different code — resolves NOTHING. Before the fix,
|
||||||
|
/// every compile re-resolved the full closure (measured: 74 fresh
|
||||||
|
/// PortableExecutableReferences per compile on Roslyn 5.0.0).
|
||||||
|
/// A probe wraps the DEFAULT resolver so the test also proves resolution
|
||||||
|
/// actually flows through this path at all (first > 0) — guarding against
|
||||||
|
/// the whole mechanism silently changing in a Roslyn upgrade.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Compile_RepeatAndDistinctScripts_ResolveClosureOnlyOnce()
|
||||||
|
{
|
||||||
|
var probe = new ProbeResolver(ScriptOptions.Default.MetadataResolver);
|
||||||
|
var caching = new CachingScriptMetadataResolver(probe);
|
||||||
|
|
||||||
|
var options = ScriptOptions.Default
|
||||||
|
.WithReferences(
|
||||||
|
typeof(object).Assembly,
|
||||||
|
typeof(Enumerable).Assembly,
|
||||||
|
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly)
|
||||||
|
.WithImports("System", "System.Linq")
|
||||||
|
.WithMetadataResolver(caching);
|
||||||
|
|
||||||
|
long CompileAndCount(string code)
|
||||||
|
{
|
||||||
|
long before = probe.MissingCalls;
|
||||||
|
var script = Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.Create<object?>(code, options);
|
||||||
|
var errors = script.Compile().Count(d => d.Severity == DiagnosticSeverity.Error);
|
||||||
|
Assert.Equal(0, errors);
|
||||||
|
return probe.MissingCalls - before;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = CompileAndCount("return Enumerable.Range(1, 3).Sum();");
|
||||||
|
var repeat = CompileAndCount("return Enumerable.Range(1, 3).Sum();");
|
||||||
|
var distinct = CompileAndCount("return string.Join(\",\", Enumerable.Range(1, 2)).Length;");
|
||||||
|
|
||||||
|
Assert.True(first > 0, "expected the first compile to resolve the transitive closure through the resolver");
|
||||||
|
Assert.Equal(0, repeat); // pre-fix: == first (~74) — one fresh native metadata copy per assembly per compile
|
||||||
|
Assert.Equal(0, distinct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ProbeResolver : MetadataReferenceResolver
|
||||||
|
{
|
||||||
|
private readonly MetadataReferenceResolver _inner;
|
||||||
|
public long MissingCalls;
|
||||||
|
public ProbeResolver(MetadataReferenceResolver inner) => _inner = inner;
|
||||||
|
public override bool ResolveMissingAssemblies => _inner.ResolveMissingAssemblies;
|
||||||
|
public override PortableExecutableReference? ResolveMissingAssembly(
|
||||||
|
MetadataReference definition, AssemblyIdentity referenceIdentity)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref MissingCalls);
|
||||||
|
return _inner.ResolveMissingAssembly(definition, referenceIdentity);
|
||||||
|
}
|
||||||
|
public override ImmutableArray<PortableExecutableReference> ResolveReference(
|
||||||
|
string reference, string? baseFilePath, MetadataReferenceProperties properties)
|
||||||
|
=> _inner.ResolveReference(reference, baseFilePath, properties);
|
||||||
|
public override bool Equals(object? other) => ReferenceEquals(this, other);
|
||||||
|
public override int GetHashCode() => 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user