b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask, SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass TestContext.Current.CancellationToken. Test counts unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
158 lines
7.8 KiB
C#
158 lines
7.8 KiB
C#
using System.Text;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Shouldly;
|
||
using Xunit;
|
||
using ZB.MOM.WW.OtOpcUa.AdminUI.Hosts;
|
||
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Hosts;
|
||
|
||
/// <summary>
|
||
/// Review D-b r1, I1: <c>/hosts</c> keys eviction on authored ∪ DEPLOYED drivers, because a running driver
|
||
/// whose health is unchanged never re-publishes (fingerprint dedup) — staleness is not a liveness proof.
|
||
/// </summary>
|
||
[Trait("Category", "Unit")]
|
||
public sealed class DeployedDriverSetTests
|
||
{
|
||
private static readonly DateTime Now = new(2026, 10, 22, 12, 0, 0, DateTimeKind.Utc);
|
||
|
||
// Test artifacts are a plain comma list; the real parser is DeploymentArtifact.ParseDriverInstances.
|
||
private static IEnumerable<string> Parse(byte[] blob) =>
|
||
Encoding.UTF8.GetString(blob).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||
|
||
private static byte[] Artifact(params string[] driverIds) => Encoding.UTF8.GetBytes(string.Join(",", driverIds));
|
||
|
||
private sealed class Factory(string name) : IDbContextFactory<OtOpcUaConfigDbContext>
|
||
{
|
||
public int Created;
|
||
public OtOpcUaConfigDbContext CreateDbContext()
|
||
{
|
||
Created++;
|
||
return new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>().UseInMemoryDatabase(name).Options);
|
||
}
|
||
|
||
public Task<OtOpcUaConfigDbContext> CreateDbContextAsync(CancellationToken ct = default) => Task.FromResult(CreateDbContext());
|
||
}
|
||
|
||
private static Deployment Dep(Guid id, DeploymentStatus status, DateTime created, byte[] blob) =>
|
||
new() { DeploymentId = id, RevisionHash = id.ToString("N"), CreatedBy = "t", Status = status, CreatedAtUtc = created, ArtifactBlob = blob };
|
||
|
||
private static NodeDeploymentState Node(string node, Guid dep, NodeDeploymentStatus status, DateTime started) =>
|
||
new() { NodeId = node, DeploymentId = dep, Status = status, StartedAtUtc = started };
|
||
|
||
[Fact]
|
||
public async Task Quiet_running_driver_deleted_in_raw_survives_the_sweep()
|
||
{
|
||
// drv-quiet is running (in the deployed artifact), deleted in /raw (not authored), and published
|
||
// once 5 minutes ago — a Faulted driver with a steady error never re-publishes.
|
||
var factory = new Factory(Guid.NewGuid().ToString("N"));
|
||
var d1 = Guid.NewGuid();
|
||
await using (var db = factory.CreateDbContext())
|
||
{
|
||
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now.AddHours(-1), Artifact("drv-quiet", "drv-kept")));
|
||
db.NodeDeploymentStates.Add(Node("n1", d1, NodeDeploymentStatus.Applied, Now.AddHours(-1)));
|
||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||
}
|
||
|
||
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken);
|
||
deployed.ShouldNotBeNull();
|
||
|
||
var store = new InMemoryDriverStatusSnapshotStore();
|
||
store.Upsert(new DriverHealthChanged("MAIN", "drv-quiet", "Faulted", null, "refused", 3, Now.AddMinutes(-5)));
|
||
store.Upsert(new DriverHealthChanged("MAIN", "drv-gone", "Faulted", null, "refused", 3, Now.AddMinutes(-5)));
|
||
|
||
var configured = new HashSet<string>(["drv-kept"], StringComparer.OrdinalIgnoreCase); // authored
|
||
configured.UnionWith(deployed!);
|
||
var removed = store.EvictUnconfiguredStale(configured, Now.AddSeconds(-60));
|
||
|
||
store.TryGet("drv-quiet", out _).ShouldBeTrue();
|
||
// Positive control: an undeployed, unconfigured, stale driver is still evicted.
|
||
removed.ShouldBe(1);
|
||
store.TryGet("drv-gone", out _).ShouldBeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Union_covers_each_nodes_latest_and_latest_applied_and_the_latest_sealed()
|
||
{
|
||
var factory = new Factory(Guid.NewGuid().ToString("N"));
|
||
Guid old = Guid.NewGuid(), failed = Guid.NewGuid(), sealedOther = Guid.NewGuid(), stale = Guid.NewGuid();
|
||
await using (var db = factory.CreateDbContext())
|
||
{
|
||
db.Deployments.Add(Dep(stale, DeploymentStatus.Sealed, Now.AddDays(-9), Artifact("drv-ancient")));
|
||
db.Deployments.Add(Dep(old, DeploymentStatus.Sealed, Now.AddHours(-3), Artifact("drv-old")));
|
||
db.Deployments.Add(Dep(failed, DeploymentStatus.PartiallyFailed, Now.AddHours(-1), Artifact("drv-new")));
|
||
db.Deployments.Add(Dep(sealedOther, DeploymentStatus.Sealed, Now.AddHours(-2), Artifact("drv-sealed")));
|
||
db.NodeDeploymentStates.Add(Node("n1", stale, NodeDeploymentStatus.Applied, Now.AddDays(-9)));
|
||
db.NodeDeploymentStates.Add(Node("n1", old, NodeDeploymentStatus.Applied, Now.AddHours(-3)));
|
||
db.NodeDeploymentStates.Add(Node("n1", failed, NodeDeploymentStatus.Failed, Now.AddHours(-1)));
|
||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||
}
|
||
|
||
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken);
|
||
|
||
// n1's latest (failed apply, may be partly running), its latest Applied (what it ran before), and the
|
||
// latest Sealed deployment — but not a superseded one nobody runs.
|
||
deployed!.ShouldBe(["drv-new", "drv-old", "drv-sealed"], ignoreOrder: true);
|
||
deployed!.ShouldNotContain("drv-ancient");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Artifacts_are_parsed_once_per_deployment_and_pruned()
|
||
{
|
||
var factory = new Factory(Guid.NewGuid().ToString("N"));
|
||
var d1 = Guid.NewGuid();
|
||
await using (var db = factory.CreateDbContext())
|
||
{
|
||
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Artifact("drv-a")));
|
||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||
}
|
||
|
||
var parses = 0;
|
||
var set = new DeployedDriverSet(factory, parseDriverIds: b => { parses++; return Parse(b); });
|
||
await set.LoadAsync(TestContext.Current.CancellationToken);
|
||
await set.LoadAsync(TestContext.Current.CancellationToken);
|
||
parses.ShouldBe(1);
|
||
set.CachedDeployments.ShouldBe(1);
|
||
|
||
var d2 = Guid.NewGuid();
|
||
await using (var db = factory.CreateDbContext())
|
||
{
|
||
db.Deployments.Add(Dep(d2, DeploymentStatus.Sealed, Now.AddMinutes(1), Artifact("drv-b")));
|
||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||
}
|
||
|
||
(await set.LoadAsync(TestContext.Current.CancellationToken))!.ShouldBe(["drv-b"]);
|
||
set.CachedDeployments.ShouldBe(1); // d1 is no longer live, so it is pruned
|
||
parses.ShouldBe(2);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task An_unreadable_artifact_means_unknown_so_nothing_is_evicted()
|
||
{
|
||
var factory = new Factory(Guid.NewGuid().ToString("N"));
|
||
var d1 = Guid.NewGuid();
|
||
await using (var db = factory.CreateDbContext())
|
||
{
|
||
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Array.Empty<byte>())); // retention-emptied
|
||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||
}
|
||
|
||
(await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken)).ShouldBeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public void Hosts_skips_eviction_when_the_deployed_set_is_unknown()
|
||
{
|
||
var src = AdminUiSource.Read("Components", "Pages", "Hosts.razor");
|
||
src.ShouldContain("var deployed = await Deployed.LoadAsync(_cts.Token);");
|
||
src.ShouldContain("configured.UnionWith(deployed);");
|
||
src.IndexOf("if (deployed is not null)", StringComparison.Ordinal)
|
||
.ShouldBeLessThan(src.IndexOf("DriverStore.EvictUnconfiguredStale(", StringComparison.Ordinal));
|
||
src.ShouldNotContain("≈ 12 health publishes");
|
||
}
|
||
}
|