refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// WP-15: Script Actor and Script Execution Actor tests.
|
||||
/// WP-20: Recursion limit tests.
|
||||
/// WP-22: Tell vs Ask convention tests.
|
||||
/// WP-32: Script error handling tests.
|
||||
/// </summary>
|
||||
public class ScriptActorTests : TestKit, IDisposable
|
||||
{
|
||||
private readonly SharedScriptLibrary _sharedLibrary;
|
||||
private readonly SiteRuntimeOptions _options;
|
||||
private readonly ScriptCompilationService _compilationService;
|
||||
|
||||
public ScriptActorTests()
|
||||
{
|
||||
_compilationService = new ScriptCompilationService(
|
||||
NullLogger<ScriptCompilationService>.Instance);
|
||||
_sharedLibrary = new SharedScriptLibrary(
|
||||
_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
||||
_options = new SiteRuntimeOptions
|
||||
{
|
||||
MaxScriptCallDepth = 10,
|
||||
ScriptExecutionTimeoutSeconds = 30
|
||||
};
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
private Script<object?> CompileScript(string code)
|
||||
{
|
||||
var scriptOptions = ScriptOptions.Default
|
||||
.WithReferences(typeof(object).Assembly, typeof(Enumerable).Assembly)
|
||||
.WithImports("System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks");
|
||||
|
||||
var script = CSharpScript.Create<object?>(code, scriptOptions, typeof(ScriptGlobals));
|
||||
script.Compile();
|
||||
return script;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_CallScript_ReturnsResult()
|
||||
{
|
||||
var compiled = CompileScript("42");
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "GetAnswer",
|
||||
Code = "42"
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"GetAnswer",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
// Ask pattern (WP-22) for CallScript
|
||||
scriptActor.Tell(new ScriptCallRequest("GetAnswer", null, 0, "corr-1"));
|
||||
|
||||
var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(result.Success, $"Script call failed: {result.ErrorMessage}");
|
||||
Assert.Equal(42, result.ReturnValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_CallScript_WithParameters_Works()
|
||||
{
|
||||
var compiled = CompileScript("(int)Parameters[\"x\"] + (int)Parameters[\"y\"]");
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "Add",
|
||||
Code = "(int)Parameters[\"x\"] + (int)Parameters[\"y\"]"
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"Add",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
var parameters = new Dictionary<string, object?> { ["x"] = 3, ["y"] = 4 };
|
||||
scriptActor.Tell(new ScriptCallRequest("Add", parameters, 0, "corr-2"));
|
||||
|
||||
var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(7, result.ReturnValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_NullCompiledScript_ReturnsError()
|
||||
{
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "Broken",
|
||||
Code = ""
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"Broken",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
null, // no compiled script
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
scriptActor.Tell(new ScriptCallRequest("Broken", null, 0, "corr-3"));
|
||||
|
||||
var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(5));
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("not compiled", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ValueChangeTrigger_SpawnsExecution()
|
||||
{
|
||||
var compiled = CompileScript("\"triggered\"");
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "OnChange",
|
||||
Code = "\"triggered\"",
|
||||
TriggerType = "ValueChange",
|
||||
TriggerConfiguration = "{\"attributeName\":\"Temperature\"}"
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"OnChange",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
// Send an attribute change that matches the trigger
|
||||
scriptActor.Tell(new AttributeValueChanged(
|
||||
"TestInstance", "Temperature", "Temperature", "100.0", "Good", DateTimeOffset.UtcNow));
|
||||
|
||||
// The script should execute (we can't easily verify the output since it's fire-and-forget)
|
||||
// But we can verify the actor doesn't crash
|
||||
ExpectNoMsg(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_MinTimeBetweenRuns_SkipsIfTooSoon()
|
||||
{
|
||||
var compiled = CompileScript("\"ok\"");
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "Throttled",
|
||||
Code = "\"ok\"",
|
||||
TriggerType = "ValueChange",
|
||||
TriggerConfiguration = "{\"attributeName\":\"Temp\"}",
|
||||
MinTimeBetweenRuns = TimeSpan.FromMinutes(10) // long minimum
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"Throttled",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
// First trigger -- should execute
|
||||
scriptActor.Tell(new AttributeValueChanged(
|
||||
"TestInstance", "Temp", "Temp", "1", "Good", DateTimeOffset.UtcNow));
|
||||
|
||||
// Second trigger immediately -- should be skipped due to min time
|
||||
scriptActor.Tell(new AttributeValueChanged(
|
||||
"TestInstance", "Temp", "Temp", "2", "Good", DateTimeOffset.UtcNow));
|
||||
|
||||
// No crash expected
|
||||
ExpectNoMsg(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_WP32_ScriptFailure_DoesNotDisable()
|
||||
{
|
||||
// Script that throws an exception
|
||||
var compiled = CompileScript("throw new System.Exception(\"boom\")");
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = "Failing",
|
||||
Code = "throw new System.Exception(\"boom\")"
|
||||
};
|
||||
|
||||
var instanceActor = CreateTestProbe();
|
||||
var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
"Failing",
|
||||
"TestInstance",
|
||||
instanceActor.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance)));
|
||||
|
||||
// First call -- fails
|
||||
scriptActor.Tell(new ScriptCallRequest("Failing", null, 0, "corr-fail-1"));
|
||||
var result1 = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
|
||||
Assert.False(result1.Success);
|
||||
|
||||
// Second call -- should still work (script not disabled after failure)
|
||||
scriptActor.Tell(new ScriptCallRequest("Failing", null, 0, "corr-fail-2"));
|
||||
var result2 = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
|
||||
Assert.False(result2.Success); // Still fails, but the actor is still alive
|
||||
}
|
||||
|
||||
// ── WhileTrue trigger mode (Conditional + Expression) ──────────────────
|
||||
//
|
||||
// A fired script runs `Instance.SetAttribute("Fired", "1")`, which the
|
||||
// Instance Actor receives as a SetStaticAttributeCommand. The probe stands
|
||||
// in for the Instance Actor: an auto-pilot replies so each execution
|
||||
// completes promptly (freeing the script-execution scheduler), while every
|
||||
// command remains observable via ExpectMsg — one command per script firing.
|
||||
|
||||
private const string FiringScriptCode = "await Instance.SetAttribute(\"Fired\", \"1\")";
|
||||
|
||||
/// <summary>Builds a ScriptActor whose script fires one observable command per run.</summary>
|
||||
private (IActorRef Actor, TestProbe Instance) CreateTriggeredActor(
|
||||
string name,
|
||||
string triggerType,
|
||||
string triggerConfig,
|
||||
TimeSpan? minTimeBetweenRuns,
|
||||
Script<object?>? triggerExpression = null)
|
||||
{
|
||||
var compiled = CompileScript(FiringScriptCode);
|
||||
var scriptConfig = new ResolvedScript
|
||||
{
|
||||
CanonicalName = name,
|
||||
Code = FiringScriptCode,
|
||||
TriggerType = triggerType,
|
||||
TriggerConfiguration = triggerConfig,
|
||||
MinTimeBetweenRuns = minTimeBetweenRuns
|
||||
};
|
||||
|
||||
var instance = CreateTestProbe();
|
||||
instance.SetAutoPilot(new DelegateAutoPilot((sender, message) =>
|
||||
{
|
||||
if (message is SetStaticAttributeCommand cmd)
|
||||
{
|
||||
sender.Tell(new SetStaticAttributeResponse(
|
||||
cmd.CorrelationId, cmd.InstanceUniqueName, cmd.AttributeName,
|
||||
true, null, DateTimeOffset.UtcNow));
|
||||
}
|
||||
return AutoPilot.KeepRunning;
|
||||
}));
|
||||
|
||||
var actor = ActorOf(Props.Create(() => new ScriptActor(
|
||||
name,
|
||||
"TestInstance",
|
||||
instance.Ref,
|
||||
compiled,
|
||||
scriptConfig,
|
||||
_sharedLibrary,
|
||||
_options,
|
||||
NullLogger<ScriptActor>.Instance,
|
||||
triggerExpression,
|
||||
null,
|
||||
null,
|
||||
null)));
|
||||
|
||||
return (actor, instance);
|
||||
}
|
||||
|
||||
private AttributeValueChanged Change(string attribute, object? value) =>
|
||||
new("TestInstance", attribute, attribute, value, "Good", DateTimeOffset.UtcNow);
|
||||
|
||||
private Script<object?> CompileTriggerExpression(string expression) =>
|
||||
_compilationService.CompileTriggerExpression("trigger-expr", expression).CompiledScript!;
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ConditionalWhileTrue_FiresOnEdgeThenReFiresWhileConditionHolds()
|
||||
{
|
||||
// WhileTrue re-fire cadence is the script's MinTimeBetweenRuns.
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"CondWhile",
|
||||
"Conditional",
|
||||
"{\"attributeName\":\"Temp\",\"operator\":\">\",\"threshold\":50,\"mode\":\"WhileTrue\"}",
|
||||
TimeSpan.FromMilliseconds(300));
|
||||
|
||||
// Temp 100 > 50 -> false->true edge: fire immediately.
|
||||
actor.Tell(Change("Temp", "100"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // edge fire
|
||||
|
||||
// Then the timer re-fires while the condition still holds.
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // tick 1
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // tick 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ConditionalWhileTrue_StopsReFiringWhenConditionGoesFalse()
|
||||
{
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"CondStop",
|
||||
"Conditional",
|
||||
"{\"attributeName\":\"Temp\",\"operator\":\">\",\"threshold\":50,\"mode\":\"WhileTrue\"}",
|
||||
TimeSpan.FromMilliseconds(300));
|
||||
|
||||
actor.Tell(Change("Temp", "100"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // edge fire
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // at least one tick
|
||||
|
||||
// Temp 10 -> condition false: the re-fire timer stops.
|
||||
actor.Tell(Change("Temp", "10"));
|
||||
instance.ReceiveWhile(o => o, TimeSpan.FromSeconds(1)); // drain any in-flight straggler tick
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(700)); // re-firing has stopped
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ConditionalWhileTrue_ReArmsAfterConditionFalseThenTrueAgain()
|
||||
{
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"CondReArm",
|
||||
"Conditional",
|
||||
"{\"attributeName\":\"Temp\",\"operator\":\">\",\"threshold\":50,\"mode\":\"WhileTrue\"}",
|
||||
TimeSpan.FromMilliseconds(300));
|
||||
|
||||
actor.Tell(Change("Temp", "100")); // true edge -> fire
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2));
|
||||
actor.Tell(Change("Temp", "10")); // false -> stop
|
||||
instance.ReceiveWhile(o => o, TimeSpan.FromSeconds(1)); // drain any in-flight straggler tick
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
|
||||
actor.Tell(Change("Temp", "100")); // false->true again: re-arm + fire
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ConditionalWhileTrue_WithoutMinTimeBetweenRuns_FiresOnceOnly()
|
||||
{
|
||||
// No MinTimeBetweenRuns -> no re-fire interval: degrades to a single edge fire.
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"CondNoInterval",
|
||||
"Conditional",
|
||||
"{\"attributeName\":\"Temp\",\"operator\":\">\",\"threshold\":50,\"mode\":\"WhileTrue\"}",
|
||||
minTimeBetweenRuns: null);
|
||||
|
||||
actor.Tell(Change("Temp", "100"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // edge fire
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(900)); // no repeats
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ConditionalOnTrue_FiresOnEachChangeWhileTrue_NoTimer()
|
||||
{
|
||||
// Regression: OnTrue (the existing behavior) fires per matching change
|
||||
// and never re-fires on a timer of its own.
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"CondOnTrue",
|
||||
"Conditional",
|
||||
"{\"attributeName\":\"Temp\",\"operator\":\">\",\"threshold\":50,\"mode\":\"OnTrue\"}",
|
||||
minTimeBetweenRuns: null);
|
||||
|
||||
actor.Tell(Change("Temp", "100"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2));
|
||||
actor.Tell(Change("Temp", "101"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2));
|
||||
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(600)); // no self-driven re-fire
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ExpressionWhileTrue_ReFiresWhileExpressionHolds()
|
||||
{
|
||||
var triggerExpr = CompileTriggerExpression("Attributes[\"Active\"]?.ToString() == \"yes\"");
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"ExprWhile",
|
||||
"Expression",
|
||||
"{\"expression\":\"Attributes[\\\"Active\\\"]?.ToString() == \\\"yes\\\"\",\"mode\":\"WhileTrue\"}",
|
||||
TimeSpan.FromMilliseconds(300),
|
||||
triggerExpr);
|
||||
|
||||
actor.Tell(Change("Active", "yes"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // edge fire
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // tick 1
|
||||
|
||||
actor.Tell(Change("Active", "no"));
|
||||
instance.ReceiveWhile(o => o, TimeSpan.FromSeconds(1)); // drain any in-flight straggler tick
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptActor_ExpressionOnTrue_FiresOncePerFalseToTrueEdge()
|
||||
{
|
||||
// Regression: OnTrue expression triggers stay edge-triggered.
|
||||
var triggerExpr = CompileTriggerExpression("Attributes[\"Active\"]?.ToString() == \"yes\"");
|
||||
var (actor, instance) = CreateTriggeredActor(
|
||||
"ExprOnTrue",
|
||||
"Expression",
|
||||
"{\"expression\":\"Attributes[\\\"Active\\\"]?.ToString() == \\\"yes\\\"\",\"mode\":\"OnTrue\"}",
|
||||
minTimeBetweenRuns: null,
|
||||
triggerExpr);
|
||||
|
||||
actor.Tell(Change("Active", "yes"));
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2)); // edge fire
|
||||
actor.Tell(Change("Active", "yes")); // still true, no edge
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
|
||||
actor.Tell(Change("Active", "no")); // -> false
|
||||
actor.Tell(Change("Active", "yes")); // false->true edge again
|
||||
instance.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user