Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05a0596fb1 | |||
| 219d10a22d |
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using SerilogLogger = Serilog.ILogger;
|
||||
using SerilogLog = Serilog.Log;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
|
||||
/// <summary>
|
||||
/// F9b — production <see cref="IScriptedAlarmEvaluator"/> binding. Compiles each unique
|
||||
/// predicate once via <see cref="ScriptEvaluator{TContext, TResult}"/> against
|
||||
/// <see cref="AlarmPredicateContext"/> and caches the resulting evaluator. Predicates are
|
||||
/// pure functions returning <c>bool</c>: <see cref="AlarmPredicateContext.SetVirtualTag"/>
|
||||
/// throws so a misbehaving script can't smuggle a side effect into alarm evaluation.
|
||||
///
|
||||
/// Failure modes (compile error, sandbox violation, runtime exception, timeout) all surface
|
||||
/// as <see cref="ScriptedAlarmEvalResult.Failure"/>; <see cref="ScriptedAlarmActor"/>
|
||||
/// preserves the prior state on failure (does not flip Active/Inactive).
|
||||
/// </summary>
|
||||
public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDisposable
|
||||
{
|
||||
private static readonly SerilogLogger ScriptLogger = SerilogLog.ForContext<RoslynScriptedAlarmEvaluator>();
|
||||
|
||||
private readonly ConcurrentDictionary<string, ScriptEvaluator<AlarmPredicateContext, bool>> _cache
|
||||
= new(StringComparer.Ordinal);
|
||||
private readonly ILogger<RoslynScriptedAlarmEvaluator> _logger;
|
||||
private readonly TimeSpan _runTimeout;
|
||||
private bool _disposed;
|
||||
|
||||
public RoslynScriptedAlarmEvaluator(ILogger<RoslynScriptedAlarmEvaluator> logger, TimeSpan? runTimeout = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
public ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary<string, object?> dependencies)
|
||||
{
|
||||
if (_disposed) return ScriptedAlarmEvalResult.Failure("evaluator disposed");
|
||||
if (string.IsNullOrWhiteSpace(predicate)) return ScriptedAlarmEvalResult.Failure("empty predicate");
|
||||
|
||||
ScriptEvaluator<AlarmPredicateContext, bool> evaluator;
|
||||
try
|
||||
{
|
||||
evaluator = _cache.GetOrAdd(predicate, ScriptEvaluator<AlarmPredicateContext, bool>.Compile);
|
||||
}
|
||||
catch (CompilationErrorException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Alarm {Id}: predicate compile failed", alarmId);
|
||||
return ScriptedAlarmEvalResult.Failure($"compile error: {ex.Message}");
|
||||
}
|
||||
catch (ScriptSandboxViolationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Alarm {Id}: predicate sandbox violation", alarmId);
|
||||
return ScriptedAlarmEvalResult.Failure($"sandbox violation: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Alarm {Id}: predicate compile threw", alarmId);
|
||||
return ScriptedAlarmEvalResult.Failure($"compile failure: {ex.Message}");
|
||||
}
|
||||
|
||||
var readCache = BuildReadCache(dependencies);
|
||||
var context = new AlarmPredicateContext(readCache, ScriptLogger);
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(_runTimeout);
|
||||
var active = evaluator.RunAsync(context, cts.Token).GetAwaiter().GetResult();
|
||||
return ScriptedAlarmEvalResult.Ok(active);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return ScriptedAlarmEvalResult.Failure($"predicate timed out after {_runTimeout.TotalSeconds:F1}s");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Alarm {Id}: predicate execution threw", alarmId);
|
||||
return ScriptedAlarmEvalResult.Failure($"predicate threw: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(
|
||||
IReadOnlyDictionary<string, object?> deps)
|
||||
{
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
var cache = new Dictionary<string, DataValueSnapshot>(StringComparer.Ordinal);
|
||||
foreach (var kv in deps)
|
||||
{
|
||||
cache[kv.Key] = new DataValueSnapshot(kv.Value, StatusCode: 0u, SourceTimestampUtc: nowUtc, ServerTimestampUtc: nowUtc);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
foreach (var ev in _cache.Values)
|
||||
{
|
||||
try { ev.Dispose(); } catch { /* best-effort */ }
|
||||
}
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
|
||||
using SerilogLogger = Serilog.ILogger;
|
||||
using SerilogLog = Serilog.Log;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
|
||||
/// <summary>
|
||||
/// F8b — production <see cref="IVirtualTagEvaluator"/> binding. Compiles each unique
|
||||
/// expression once via <see cref="ScriptEvaluator{TContext, TResult}"/> (Roslyn-backed
|
||||
/// sandbox) and caches the resulting evaluator keyed by source. Subsequent evaluations are
|
||||
/// in-process method invocations on the dependency dictionary — fast enough to run inline
|
||||
/// inside the actor's message handler.
|
||||
///
|
||||
/// Single-tag mode: cross-tag <c>ctx.SetVirtualTag</c> writes are dropped (logged) because
|
||||
/// fan-out between actors is owned by <c>DependencyMuxActor</c>, not by the eval engine.
|
||||
/// Cycle detection + cascade ordering live in <see cref="VirtualTagEngine"/>; this adapter
|
||||
/// stays single-tag scoped to keep <see cref="VirtualTagActor"/>'s message loop simple.
|
||||
/// </summary>
|
||||
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IDisposable
|
||||
{
|
||||
private static readonly SerilogLogger ScriptLogger = SerilogLog.ForContext<RoslynVirtualTagEvaluator>();
|
||||
|
||||
private readonly ConcurrentDictionary<string, ScriptEvaluator<VirtualTagContext, object?>> _cache
|
||||
= new(StringComparer.Ordinal);
|
||||
private readonly ILogger<RoslynVirtualTagEvaluator> _logger;
|
||||
private readonly TimeSpan _runTimeout;
|
||||
private bool _disposed;
|
||||
|
||||
public RoslynVirtualTagEvaluator(ILogger<RoslynVirtualTagEvaluator> logger, TimeSpan? runTimeout = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
|
||||
{
|
||||
if (_disposed) return VirtualTagEvalResult.Failure("evaluator disposed");
|
||||
if (string.IsNullOrWhiteSpace(expression)) return VirtualTagEvalResult.Failure("empty expression");
|
||||
|
||||
ScriptEvaluator<VirtualTagContext, object?> evaluator;
|
||||
try
|
||||
{
|
||||
evaluator = _cache.GetOrAdd(expression, ScriptEvaluator<VirtualTagContext, object?>.Compile);
|
||||
}
|
||||
catch (CompilationErrorException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "VirtualTag {Id}: Roslyn compile failed", virtualTagId);
|
||||
return VirtualTagEvalResult.Failure($"compile error: {ex.Message}");
|
||||
}
|
||||
catch (ScriptSandboxViolationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "VirtualTag {Id}: sandbox violation", virtualTagId);
|
||||
return VirtualTagEvalResult.Failure($"sandbox violation: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "VirtualTag {Id}: compile threw", virtualTagId);
|
||||
return VirtualTagEvalResult.Failure($"compile failure: {ex.Message}");
|
||||
}
|
||||
|
||||
var readCache = BuildReadCache(dependencies);
|
||||
var context = new VirtualTagContext(
|
||||
readCache,
|
||||
setVirtualTag: (path, _) =>
|
||||
_logger.LogDebug("VirtualTag {Id}: cross-tag write to {Path} dropped (single-tag adapter)",
|
||||
virtualTagId, path),
|
||||
logger: ScriptLogger);
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(_runTimeout);
|
||||
var raw = evaluator.RunAsync(context, cts.Token).GetAwaiter().GetResult();
|
||||
return VirtualTagEvalResult.Ok(raw);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return VirtualTagEvalResult.Failure($"script timed out after {_runTimeout.TotalSeconds:F1}s");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "VirtualTag {Id}: script execution threw", virtualTagId);
|
||||
return VirtualTagEvalResult.Failure($"script threw: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(
|
||||
IReadOnlyDictionary<string, object?> deps)
|
||||
{
|
||||
// VirtualTagContext.GetTag returns a DataValueSnapshot — we wrap each raw dep value
|
||||
// as Good-quality so the script's `(int)ctx.GetTag("a").Value` pattern works. Null
|
||||
// values stay null; the script can null-check via GetTag(path).Value.
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
var cache = new Dictionary<string, DataValueSnapshot>(StringComparer.Ordinal);
|
||||
foreach (var kv in deps)
|
||||
{
|
||||
cache[kv.Key] = new DataValueSnapshot(kv.Value, StatusCode: 0u, SourceTimestampUtc: nowUtc, ServerTimestampUtc: nowUtc);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
foreach (var ev in _cache.Values)
|
||||
{
|
||||
try { ev.Dispose(); } catch { /* best-effort */ }
|
||||
}
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ using ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Host;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Observability;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
||||
@@ -70,6 +72,19 @@ if (hasDriver)
|
||||
// ILdapAuthService is registered by AddOtOpcUaAuth on admin nodes; on driver-only nodes
|
||||
// it isn't, so we register the LDAP options + service unconditionally for driver hosts
|
||||
// to keep parity. The LdapAdapter falls back to Deny on any backend error.
|
||||
// F8b — production virtual-tag evaluator (Roslyn-compiled scripts cached per expression).
|
||||
// Replaces the F8-default NullVirtualTagEvaluator so VirtualTagActor evaluates real user
|
||||
// scripts at runtime.
|
||||
builder.Services.AddSingleton<RoslynVirtualTagEvaluator>(sp =>
|
||||
new RoslynVirtualTagEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynVirtualTagEvaluator>()));
|
||||
builder.Services.AddSingleton<IVirtualTagEvaluator>(sp => sp.GetRequiredService<RoslynVirtualTagEvaluator>());
|
||||
|
||||
// F9b — same pattern for scripted-alarm predicates. The actor preserves prior state on
|
||||
// any Failure result, so a misbehaving script can't flip Active/Inactive spuriously.
|
||||
builder.Services.AddSingleton<RoslynScriptedAlarmEvaluator>(sp =>
|
||||
new RoslynScriptedAlarmEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>()));
|
||||
builder.Services.AddSingleton<IScriptedAlarmEvaluator>(sp => sp.GetRequiredService<RoslynScriptedAlarmEvaluator>());
|
||||
|
||||
builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap"));
|
||||
builder.Services.AddSingleton<ILdapAuthService, LdapAuthService>();
|
||||
builder.Services.AddSingleton<IOpcUaUserAuthenticator, LdapOpcUaUserAuthenticator>();
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
<AssemblyName>OtOpcUa.Host</AssemblyName>
|
||||
<UserSecretsId>zb-mom-ww-otopcua-host</UserSecretsId>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Microsoft.CodeAnalysis.CSharp.Scripting (4.12.0, pulled in via Core.Scripting for F8b
|
||||
user-script compilation) requires CodeAnalysis.Common 4.12.0 exactly, but ASP.NET
|
||||
Core's transitive Microsoft.CodeAnalysis.CSharp 5.0.0 wins resolution. Suppress
|
||||
NU1608 — the surface we use from Scripting (ScriptEvaluator + RoslynScriptHost) is
|
||||
stable across the version drift; verified by Core.Scripting.Tests. -->
|
||||
<NoWarn>$(NoWarn);NU1608</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -24,6 +30,9 @@
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Cluster\ZB.MOM.WW.OtOpcUa.Cluster.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Configuration\ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Scripting\ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.VirtualTags\ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Security\ZB.MOM.WW.OtOpcUa.Security.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.ControlPlane\ZB.MOM.WW.OtOpcUa.ControlPlane.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Runtime\ZB.MOM.WW.OtOpcUa.Runtime.csproj"/>
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// F9b — verifies <see cref="RoslynScriptedAlarmEvaluator"/> compiles alarm predicates,
|
||||
/// returns the bool result on success, surfaces compile/runtime errors as Failure (so the
|
||||
/// actor preserves prior state), and rejects predicates that try to ctx.SetVirtualTag (the
|
||||
/// AlarmPredicateContext throws on writes — predicates must stay pure).
|
||||
/// </summary>
|
||||
public sealed class RoslynScriptedAlarmEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Evaluate_predicate_returning_true_reports_Active()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate(
|
||||
alarmId: "alarm-hi",
|
||||
predicate: "return (int)ctx.GetTag(\"temp\").Value > 100;",
|
||||
dependencies: new Dictionary<string, object?> { ["temp"] = 150 });
|
||||
|
||||
result.Success.ShouldBeTrue(result.Reason);
|
||||
result.Active.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_predicate_returning_false_reports_Inactive()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate(
|
||||
alarmId: "alarm-hi",
|
||||
predicate: "return (int)ctx.GetTag(\"temp\").Value > 100;",
|
||||
dependencies: new Dictionary<string, object?> { ["temp"] = 50 });
|
||||
|
||||
result.Success.ShouldBeTrue(result.Reason);
|
||||
result.Active.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_caches_compiled_predicate_across_calls()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
const string predicate = "return (bool)ctx.GetTag(\"door_open\").Value;";
|
||||
|
||||
var first = sut.Evaluate("alarm-door", predicate, new Dictionary<string, object?> { ["door_open"] = true });
|
||||
var second = sut.Evaluate("alarm-door", predicate, new Dictionary<string, object?> { ["door_open"] = false });
|
||||
|
||||
first.Active.ShouldBeTrue();
|
||||
second.Active.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_compile_error_returns_Failure()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate("alarm-bad", "this isn't C#;", new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason!.ShouldContain("compile");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_predicate_writing_virtual_tag_returns_Failure()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
|
||||
// AlarmPredicateContext.SetVirtualTag throws — wrapper catches + reports as Failure.
|
||||
var result = sut.Evaluate(
|
||||
alarmId: "alarm-bad-write",
|
||||
predicate: "ctx.SetVirtualTag(\"x\", 1); return true;",
|
||||
dependencies: new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason!.ShouldContain("threw");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_empty_predicate_returns_Failure()
|
||||
{
|
||||
using var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
|
||||
sut.Evaluate("alarm-empty", "", new Dictionary<string, object?>()).Success.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_after_dispose_returns_Failure()
|
||||
{
|
||||
var sut = new RoslynScriptedAlarmEvaluator(NullLogger<RoslynScriptedAlarmEvaluator>.Instance);
|
||||
sut.Dispose();
|
||||
|
||||
var result = sut.Evaluate("alarm", "return true;", new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason!.ShouldContain("disposed");
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// F8b — verifies <see cref="RoslynVirtualTagEvaluator"/> compiles user expressions through
|
||||
/// the Core.Scripting sandbox, runs them against the dependency dictionary, caches the
|
||||
/// compiled assembly per source, and surfaces failures (compile error, sandbox violation,
|
||||
/// runtime throw) as <c>VirtualTagEvalResult.Failure</c> instead of propagating exceptions.
|
||||
/// </summary>
|
||||
public sealed class RoslynVirtualTagEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Evaluate_simple_addition_returns_summed_value()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate(
|
||||
virtualTagId: "vt-sum",
|
||||
expression: "return (int)ctx.GetTag(\"a\").Value + (int)ctx.GetTag(\"b\").Value;",
|
||||
dependencies: new Dictionary<string, object?> { ["a"] = 10, ["b"] = 32 });
|
||||
|
||||
result.Success.ShouldBeTrue(result.Reason);
|
||||
result.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_caches_compiled_expression_across_calls()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
const string expr = "return (int)ctx.GetTag(\"x\").Value * 2;";
|
||||
|
||||
var first = sut.Evaluate("vt-cache", expr, new Dictionary<string, object?> { ["x"] = 5 });
|
||||
var second = sut.Evaluate("vt-cache", expr, new Dictionary<string, object?> { ["x"] = 7 });
|
||||
|
||||
first.Success.ShouldBeTrue(first.Reason);
|
||||
first.Value.ShouldBe(10);
|
||||
second.Success.ShouldBeTrue(second.Reason);
|
||||
second.Value.ShouldBe(14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_compile_error_returns_Failure_with_reason()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate("vt-bad", "this is not valid C#;", new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason.ShouldNotBeNull();
|
||||
result.Reason.ShouldContain("compile");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_runtime_exception_returns_Failure_with_reason()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
|
||||
var result = sut.Evaluate(
|
||||
virtualTagId: "vt-div0",
|
||||
expression: "int a = 0; return 1 / a;",
|
||||
dependencies: new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason.ShouldNotBeNull();
|
||||
result.Reason.ShouldContain("threw");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_empty_expression_returns_Failure()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
|
||||
sut.Evaluate("vt-empty", "", new Dictionary<string, object?>()).Success.ShouldBeFalse();
|
||||
sut.Evaluate("vt-empty", " ", new Dictionary<string, object?>()).Success.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_after_dispose_returns_Failure()
|
||||
{
|
||||
var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
sut.Dispose();
|
||||
|
||||
var result = sut.Evaluate("vt", "return 1;", new Dictionary<string, object?>());
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Reason!.ShouldContain("disposed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user