Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
5.0 KiB
C#
140 lines
5.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
|
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.Admin.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasDriverDetailServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task GetAsync_returns_null_for_unknown_instance()
|
|
{
|
|
using var ctx = NewContext();
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
(await svc.GetAsync("missing", CancellationToken.None)).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_returns_null_for_non_focas_driver_type()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.DriverInstances.Add(NewInstance("drv-modbus", "ModbusTcp", "{}"));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
(await svc.GetAsync("drv-modbus", CancellationToken.None)).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_parses_devices_tags_and_alarm_projection()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", """
|
|
{
|
|
"Devices": [
|
|
{ "HostAddress": "focas://10.20.30.40:8193", "Series": "ThirtyOne_i" }
|
|
],
|
|
"Tags": [
|
|
{ "Name": "Mode", "DeviceHostAddress": "focas://10.20.30.40:8193",
|
|
"Address": "PARAM:3402", "DataType": "Int32", "Writable": false }
|
|
],
|
|
"AlarmProjection": { "Enabled": true, "PollInterval": "00:00:05" },
|
|
"HandleRecycle": { "Enabled": true, "Interval": "01:00:00" }
|
|
}
|
|
"""));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
var detail = await svc.GetAsync("drv-focas", CancellationToken.None);
|
|
|
|
detail.ShouldNotBeNull();
|
|
detail.ParseError.ShouldBeNull();
|
|
detail.Config.ShouldNotBeNull();
|
|
detail.Config.Devices!.Single().HostAddress.ShouldBe("focas://10.20.30.40:8193");
|
|
detail.Config.Devices!.Single().Series.ShouldBe("ThirtyOne_i");
|
|
detail.Config.Tags!.Single().Name.ShouldBe("Mode");
|
|
detail.Config.AlarmProjection!.Enabled.ShouldBeTrue();
|
|
detail.Config.HandleRecycle!.Enabled.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_surfaces_parse_error_for_malformed_json()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.DriverInstances.Add(NewInstance("drv-bad", "Focas", "{ not-valid-json"));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
var detail = await svc.GetAsync("drv-bad", CancellationToken.None);
|
|
|
|
detail.ShouldNotBeNull();
|
|
detail.ParseError.ShouldNotBeNull();
|
|
detail.Config.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_joins_host_status_rows_for_the_instance()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", "{}"));
|
|
ctx.DriverHostStatuses.Add(new DriverHostStatus
|
|
{
|
|
NodeId = "node-A",
|
|
DriverInstanceId = "drv-focas",
|
|
HostName = "focas://10.0.0.1:8193",
|
|
State = DriverHostState.Running,
|
|
StateChangedUtc = DateTime.UtcNow.AddMinutes(-5),
|
|
LastSeenUtc = DateTime.UtcNow.AddSeconds(-3),
|
|
});
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
var detail = await svc.GetAsync("drv-focas", CancellationToken.None);
|
|
|
|
detail.ShouldNotBeNull();
|
|
detail.HostStatuses.Count.ShouldBe(1);
|
|
detail.HostStatuses[0].HostName.ShouldBe("focas://10.0.0.1:8193");
|
|
detail.HostStatuses[0].State.ShouldBe("Running");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_picks_latest_generation_when_multiple_rows_exist()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", "{\"Tags\":[]}", generationId: 1));
|
|
ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", """{"Tags":[{"Name":"later"}]}""", generationId: 2));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var svc = new FocasDriverDetailService(ctx);
|
|
var detail = await svc.GetAsync("drv-focas", CancellationToken.None);
|
|
|
|
detail.ShouldNotBeNull();
|
|
detail.Config!.Tags!.Single().Name.ShouldBe("later");
|
|
}
|
|
|
|
private static DriverInstance NewInstance(
|
|
string driverInstanceId, string driverType, string driverConfigJson, long generationId = 1) => new()
|
|
{
|
|
GenerationId = generationId,
|
|
DriverInstanceId = driverInstanceId,
|
|
ClusterId = "cluster-1",
|
|
NamespaceId = "ns-1",
|
|
Name = driverInstanceId,
|
|
DriverType = driverType,
|
|
DriverConfig = driverConfigJson,
|
|
};
|
|
|
|
private static OtOpcUaConfigDbContext NewContext()
|
|
{
|
|
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new OtOpcUaConfigDbContext(opts);
|
|
}
|
|
}
|