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>
159 lines
5.4 KiB
C#
159 lines
5.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class IdentificationFolderBuilderTests
|
|
{
|
|
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
|
{
|
|
public List<(string BrowseName, string DisplayName)> Folders { get; } = [];
|
|
public List<(string BrowseName, DriverDataType DataType, object? Value)> Properties { get; } = [];
|
|
|
|
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
|
{
|
|
Folders.Add((browseName, displayName));
|
|
return this; // flat recording — identification fields land in the same bucket
|
|
}
|
|
|
|
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
|
=> throw new NotSupportedException("Identification fields use AddProperty, not Variable");
|
|
|
|
public void AddProperty(string browseName, DriverDataType dataType, object? value)
|
|
=> Properties.Add((browseName, dataType, value));
|
|
}
|
|
|
|
private static Equipment EmptyEquipment() => new()
|
|
{
|
|
EquipmentId = "EQ-000000000001",
|
|
DriverInstanceId = "drv-1",
|
|
UnsLineId = "line-1",
|
|
Name = "eq-1",
|
|
MachineCode = "machine_001",
|
|
};
|
|
|
|
private static Equipment FullyPopulatedEquipment() => new()
|
|
{
|
|
EquipmentId = "EQ-000000000001",
|
|
DriverInstanceId = "drv-1",
|
|
UnsLineId = "line-1",
|
|
Name = "eq-1",
|
|
MachineCode = "machine_001",
|
|
Manufacturer = "Siemens",
|
|
Model = "S7-1500",
|
|
SerialNumber = "SN-12345",
|
|
HardwareRevision = "Rev-A",
|
|
SoftwareRevision = "Fw-2.3.1",
|
|
YearOfConstruction = 2023,
|
|
AssetLocation = "Warsaw-West/Bldg-3",
|
|
ManufacturerUri = "https://siemens.example",
|
|
DeviceManualUri = "https://siemens.example/manual",
|
|
};
|
|
|
|
[Fact]
|
|
public void HasAnyFields_AllNull_ReturnsFalse()
|
|
{
|
|
IdentificationFolderBuilder.HasAnyFields(EmptyEquipment()).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void HasAnyFields_OneNonNull_ReturnsTrue()
|
|
{
|
|
var eq = EmptyEquipment();
|
|
eq.SerialNumber = "SN-1";
|
|
IdentificationFolderBuilder.HasAnyFields(eq).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_AllNull_ReturnsNull_AndDoesNotEmit_Folder()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
|
|
var result = IdentificationFolderBuilder.Build(builder, EmptyEquipment());
|
|
|
|
result.ShouldBeNull();
|
|
builder.Folders.ShouldBeEmpty("no Identification folder when every field is null");
|
|
builder.Properties.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_FullyPopulated_EmitsAllNineFields()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
|
|
var result = IdentificationFolderBuilder.Build(builder, FullyPopulatedEquipment());
|
|
|
|
result.ShouldNotBeNull();
|
|
builder.Folders.ShouldContain(f => f.BrowseName == "Identification");
|
|
builder.Properties.Count.ShouldBe(9);
|
|
builder.Properties.Select(p => p.BrowseName).ShouldBe(
|
|
["Manufacturer", "Model", "SerialNumber",
|
|
"HardwareRevision", "SoftwareRevision",
|
|
"YearOfConstruction", "AssetLocation",
|
|
"ManufacturerUri", "DeviceManualUri"],
|
|
"property order matches decision #139 exactly");
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_OnlyNonNull_Are_Emitted()
|
|
{
|
|
var eq = EmptyEquipment();
|
|
eq.Manufacturer = "Siemens";
|
|
eq.SerialNumber = "SN-1";
|
|
eq.YearOfConstruction = 2024;
|
|
var builder = new RecordingBuilder();
|
|
|
|
IdentificationFolderBuilder.Build(builder, eq);
|
|
|
|
builder.Properties.Count.ShouldBe(3, "only the 3 non-null fields are exposed");
|
|
builder.Properties.Select(p => p.BrowseName).ShouldBe(
|
|
["Manufacturer", "SerialNumber", "YearOfConstruction"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void YearOfConstruction_Maps_Short_To_Int32_DriverDataType()
|
|
{
|
|
var eq = EmptyEquipment();
|
|
eq.YearOfConstruction = 2023;
|
|
var builder = new RecordingBuilder();
|
|
|
|
IdentificationFolderBuilder.Build(builder, eq);
|
|
|
|
var prop = builder.Properties.Single(p => p.BrowseName == "YearOfConstruction");
|
|
prop.DataType.ShouldBe(DriverDataType.Int32);
|
|
prop.Value.ShouldBe(2023, "short is widened to int for OPC UA Int32 representation");
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_StringValues_RoundTrip()
|
|
{
|
|
var eq = FullyPopulatedEquipment();
|
|
var builder = new RecordingBuilder();
|
|
|
|
IdentificationFolderBuilder.Build(builder, eq);
|
|
|
|
builder.Properties.Single(p => p.BrowseName == "Manufacturer").Value.ShouldBe("Siemens");
|
|
builder.Properties.Single(p => p.BrowseName == "DeviceManualUri").Value.ShouldBe("https://siemens.example/manual");
|
|
}
|
|
|
|
[Fact]
|
|
public void FieldNames_Match_Decision139_Exactly()
|
|
{
|
|
IdentificationFolderBuilder.FieldNames.ShouldBe(
|
|
["Manufacturer", "Model", "SerialNumber",
|
|
"HardwareRevision", "SoftwareRevision",
|
|
"YearOfConstruction", "AssetLocation",
|
|
"ManufacturerUri", "DeviceManualUri"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void FolderName_Is_Identification()
|
|
{
|
|
IdentificationFolderBuilder.FolderName.ShouldBe("Identification");
|
|
}
|
|
}
|