1b37b2f2b0
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
75 lines
3.5 KiB
C#
75 lines
3.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
|
|
|
|
/// <summary>
|
|
/// Covers the <see cref="ScriptSandbox.Build"/> argument-validation guards. The
|
|
/// guards are the only call-site protection against a typo / mis-wired context
|
|
/// type silently producing a sandbox missing the concrete context's assembly
|
|
/// reference; without coverage, the guards could be deleted by a refactor without
|
|
/// any test failing. (Core.Scripting-011.)
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScriptSandboxBuildTests
|
|
{
|
|
/// <summary>Verifies that a null context type throws ArgumentNullException.</summary>
|
|
[Fact]
|
|
public void Null_context_type_throws_ArgumentNullException()
|
|
{
|
|
Should.Throw<ArgumentNullException>(() => ScriptSandbox.Build(null!));
|
|
}
|
|
|
|
/// <summary>Verifies that a non-ScriptContext type throws ArgumentException.</summary>
|
|
[Fact]
|
|
public void Non_ScriptContext_type_throws_ArgumentException()
|
|
{
|
|
// ScriptSandbox must reject types that don't derive from ScriptContext —
|
|
// ScriptGlobals<TContext> is constrained where TContext : ScriptContext, so
|
|
// sneaking a non-derived type past Build would later blow up inside Roslyn.
|
|
Should.Throw<ArgumentException>(() => ScriptSandbox.Build(typeof(string)))
|
|
.ParamName.ShouldBe("contextType");
|
|
}
|
|
|
|
/// <summary>Verifies that the abstract ScriptContext base type is accepted by the sandbox builder.</summary>
|
|
[Fact]
|
|
public void Abstract_ScriptContext_base_type_is_accepted()
|
|
{
|
|
// The base ScriptContext type satisfies the IsAssignableFrom check, so the
|
|
// factory must not reject it even though it cannot be instantiated directly.
|
|
// Callers wiring a base-typed sandbox up for diagnostics rely on this.
|
|
var options = ScriptSandbox.Build(typeof(ScriptContext));
|
|
options.ShouldNotBeNull();
|
|
}
|
|
|
|
/// <summary>Verifies that a concrete subclass is accepted and its assembly is referenced in the sandbox.</summary>
|
|
[Fact]
|
|
public void Concrete_subclass_is_accepted_and_its_assembly_referenced()
|
|
{
|
|
// The concrete context type's assembly must end up in the allow-listed
|
|
// references — otherwise Roslyn cannot resolve ScriptGlobals<TContext> at
|
|
// compile. We can't easily inspect the ScriptOptions metadata references
|
|
// by-assembly cross-version, so we exercise the end-to-end path instead: a
|
|
// script compiled against FakeScriptContext must successfully reach a
|
|
// FakeScriptContext member.
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, double>.Compile(
|
|
"""return (double)ctx.GetTag("X").Value;""");
|
|
evaluator.ShouldNotBeNull();
|
|
}
|
|
|
|
/// <summary>#603: the BCL reference set is built once per process, so two Build calls share the same
|
|
/// <c>MetadataReference</c> instances rather than re-reading ~170 DLLs per compile.</summary>
|
|
[Fact]
|
|
public void Build_reuses_metadata_reference_instances_across_calls()
|
|
{
|
|
var first = ScriptSandbox.Build(typeof(ScriptContext));
|
|
var second = ScriptSandbox.Build(typeof(ScriptContext));
|
|
|
|
first.References.Count.ShouldBeGreaterThan(50);
|
|
first.References.Count.ShouldBe(second.References.Count);
|
|
for (var i = 0; i < first.References.Count; i++)
|
|
ReferenceEquals(first.References[i], second.References[i]).ShouldBeTrue($"reference {i} was rebuilt");
|
|
}
|
|
}
|