Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerParseOnceTests.cs
T
Joseph Doherty dde22a3dd1 perf(site-runtime): deserialize a deploy's flattened config once and hand the parsed object down (review 2026-10-21 01/SR-P5)
The deploy compile warm parses once off-thread; the parsed FlattenedConfiguration
rides DeployCompileWarmed to the gate (new Validate(FlattenedConfiguration)
overload), the redeploy buffer, CreateInstanceActor's DCL ensure and a new
InstanceActor constructor overload. Stored-row paths parse once inside
CreateInstanceActor; an unparseable stored row still fails init via S6.
2026-10-22 09:10:26 -04:00

99 lines
4.6 KiB
C#

using System.Text.Json;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.TestSupport;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
/// <summary>
/// Review 2026-10-21 01/SR-P5: a deploy's flattened config is deserialized ONCE (in the
/// off-thread compile warm) and the parsed object is handed to the gate, the DCL-connection
/// ensure and the Instance Actor. These pin the two contracts that refactor must keep: a config
/// that does not parse is still a readable gate rejection with nothing applied, and an Instance
/// Actor built from the parsed object behaves like one built from the JSON.
/// </summary>
public class DeploymentManagerParseOnceTests : TestKit, IDisposable
{
private readonly SiteStorageService _storage;
private readonly ScriptCompilationService _compilationService;
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly TestLocalDb _localDb;
/// <summary>Creates a temporary LocalDb-backed site store per test.</summary>
public DeploymentManagerParseOnceTests()
{
_localDb = TestLocalDb.CreateTemp("dm-parse-once");
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
_storage.InitializeAsync().GetAwaiter().GetResult();
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
_sharedScriptLibrary = new SharedScriptLibrary(
_compilationService, NullLogger<SharedScriptLibrary>.Instance);
}
/// <inheritdoc />
void IDisposable.Dispose()
{
Shutdown();
var path = _localDb.Path;
_localDb.Dispose();
TestLocalDb.DeleteFiles(path);
}
private IActorRef CreateBootedDeploymentManager()
{
var health = new DeploymentManagerStartupProbe();
var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
_storage, _compilationService, _sharedScriptLibrary, null,
new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 0 },
NullLogger<DeploymentManagerActor>.Instance,
dclManager: null, healthCollector: health, serviceProvider: null,
loggerFactory: null, configFetcher: null)));
AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(25));
return dm;
}
/// <summary>Malformed JSON is rejected by the gate with the deserialization error; nothing is persisted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Deploy_MalformedConfig_IsRejectedWithTheParseError()
{
var dm = CreateBootedDeploymentManager();
dm.Tell(new DeployInstanceCommand("dep-1", "Bad1", "r1", "{not json", "tester", DateTimeOffset.UtcNow));
var response = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(15));
Assert.Equal(DeploymentStatus.Failed, response.Status);
Assert.Contains("failed to deserialize", response.ErrorMessage);
Assert.Null(await _storage.GetDeployedConfigAsync("Bad1"));
}
/// <summary>A deploy built from the parsed object serves its configured attributes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Deploy_ParsedOncePath_CreatesAWorkingInstance()
{
var dm = CreateBootedDeploymentManager();
var json = JsonSerializer.Serialize(new FlattenedConfiguration
{
InstanceUniqueName = "Good1",
Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "7", DataType = "Int32" }]
});
dm.Tell(new DeployInstanceCommand("dep-2", "Good1", "r1", json, "tester", DateTimeOffset.UtcNow));
Assert.Equal(DeploymentStatus.Success, ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(15)).Status);
var instance = await Sys.ActorSelection(dm.Path / "Good1").ResolveOne(TimeSpan.FromSeconds(5));
instance.Tell(new GetAttributeRequest("g", "Good1", "A", DateTimeOffset.UtcNow));
Assert.Equal("7", ExpectMsg<GetAttributeResponse>().Value?.ToString());
}
}