Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs
Joseph Doherty 32dff7f1d6 ab_server integration fixture — per-family profiles + documented CI-fetch contract. Closes task #180 (AB CIP follow-up — ab_server CI fixture). Replaces the prior hardcoded single-family fixture with a parametric AbServerProfile abstraction covering ControlLogix / CompactLogix / Micro800 / GuardLogix. Prebuilt-Windows-binary fetch is documented as a CI YAML step rather than fabricated C#-side, because SHA-pinned binary distribution is a CI workflow concern (libplctag owns releases, we pin a version + verify hash) not a test-framework concern. New AbServerProfile record + KnownProfiles static class at tests/.../AbServerProfile.cs. Four profiles: ControlLogix (widest coverage — DINT/REAL/BOOL/SINT/STRING atomic + DINT[16] array so the driver's @tags Symbol-Object decoder + array-bound path both get end-to-end coverage), CompactLogix (atomic subset — driver-side ConnectionSize quirk from PR 10 still applies since ab_server doesn't enforce the narrower limit), Micro800 (ab_server has no dedicated --plc micro800 mode — falls back to controllogix while driver-side path enforces empty routing + unconnected-only per PR 11; real Micro800 coverage requires a 2080 lab rig), GuardLogix (ab_server has no safety subsystem — profile emulates the _S-suffixed naming contract the driver's safety-ViewOnly classification reads in PR 12; real safety-lock behavior requires a 1756-L8xS physical rig). Each profile composes --plc + --tag args via BuildCliArgs(port) — pure string formatter so the composition logic is unit-testable without launching the simulator. AbServerFixture gains a ctor overload taking AbServerProfile + port (defaults back to ControlLogix on parameterless ctor so existing test suites keep compiling). Fixture's InitializeAsync hands the profile's CLI args to ProcessStartInfo.Arguments. New AbServerTheoryAttribute mirrors AbServerFactAttribute but extends TheoryAttribute so a single test can MemberData over KnownProfiles.All + cover all four families. AbCipReadSmokeTests converted from single-fact to theory parametrized over KnownProfiles.All — one row per family reads TestDINT + asserts Good status + Healthy driver state. Fixture lifecycle is explicit try/finally rather than await using because IAsyncLifetime.DisposeAsync returns ValueTask + xUnit's concrete IAsyncDisposable shim depends on xunit version; explicit beats implicit here. Eight new unit tests in AbServerProfileTests.cs (runs without the simulator so CI green even when the binary is absent): BuildCliArgs composes port + plc + tag flags in the documented order; empty seed-tag list still emits port + plc; SeedTag.ToCliSpec handles both 2-segment scalar + 3-segment array; KnownProfiles.ForFamily returns expected --plc arg for every family (verifies Micro800 + GuardLogix both fall back to controllogix); KnownProfiles.All covers every AbCipPlcFamily enum value (regression guard — adding a new family without a profile fails this test); ControlLogix seeds every atomic type the driver supports; GuardLogix seeds at least one _S-suffixed safety tag. Integration tests still skip cleanly when ab_server isn't on PATH. 11/11 unit tests passing in this project (8 new + 3 prior). Full Admin solution builds 0 errors. docs/v2/test-data-sources.md gets a new "CI fixture" subsection under §2.Gotchas with the exact GitHub Actions YAML step — fetch the pinned libplctag release, SHA256-verify against a pinned hash recorded in the repo's CI lockfile (drift = fail closed), extract, append to PATH. The C# harness stays PATH-driven so dev-box installs (cmake + make from source) work identically to CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:57:24 -04:00

135 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
/// <summary>
/// Per-family provisioning profile for the <c>ab_server</c> simulator. Instead of hard-coding
/// one fixture shape + one set of CLI args, each integration test picks a profile matching the
/// family it wants to exercise — ControlLogix / CompactLogix / Micro800 / GuardLogix. The
/// profile composes the CLI arg list passed to <c>ab_server</c> + the tag-definition set the
/// driver uses to address the simulator's pre-provisioned tags.
/// </summary>
/// <param name="Family">OtOpcUa driver family this profile targets. Drives
/// <see cref="AbCipDeviceOptions.PlcFamily"/> + driver-side connection-parameter profile
/// (ConnectionSize, unconnected-only, etc.) per decision #9.</param>
/// <param name="AbServerPlcArg">The value passed to <c>ab_server --plc &lt;arg&gt;</c>. Some families
/// map 1:1 (ControlLogix → "controllogix"); Micro800/GuardLogix fall back to the family whose
/// CIP behavior ab_server emulates most faithfully (see per-profile Notes).</param>
/// <param name="SeedTags">Tags to preseed on the simulator via <c>--tag &lt;name&gt;:&lt;type&gt;[:&lt;size&gt;]</c>
/// flags. Each entry becomes one CLI arg; the driver-side <see cref="AbCipTagDefinition"/>
/// list references the same names so tests can read/write without walking the @tags surface
/// first.</param>
/// <param name="Notes">Operator-facing description of what the profile covers + any quirks.</param>
public sealed record AbServerProfile(
AbCipPlcFamily Family,
string AbServerPlcArg,
IReadOnlyList<AbServerSeedTag> SeedTags,
string Notes)
{
/// <summary>Default port — every profile uses the same so parallel-runs-of-different-families
/// would conflict (deliberately — one simulator per test collection is the model).</summary>
public const int DefaultPort = 44818;
/// <summary>Compose the full <c>ab_server</c> CLI arg string for
/// <see cref="System.Diagnostics.ProcessStartInfo.Arguments"/>.</summary>
public string BuildCliArgs(int port)
{
var parts = new List<string>
{
"--port", port.ToString(),
"--plc", AbServerPlcArg,
};
foreach (var tag in SeedTags)
{
parts.Add("--tag");
parts.Add(tag.ToCliSpec());
}
return string.Join(' ', parts);
}
}
/// <summary>One tag the simulator pre-creates. ab_server spec format:
/// <c>&lt;name&gt;:&lt;type&gt;[:&lt;array_size&gt;]</c>.</summary>
public sealed record AbServerSeedTag(string Name, string AbServerType, int? ArraySize = null)
{
public string ToCliSpec() => ArraySize is { } n ? $"{Name}:{AbServerType}:{n}" : $"{Name}:{AbServerType}";
}
/// <summary>Canonical profiles covering every AB CIP family shipped in PRs 912.</summary>
public static class KnownProfiles
{
/// <summary>
/// ControlLogix — the widest-coverage family: full CIP capabilities, generous connection
/// size, @tags controller-walk supported. Tag shape covers atomic types + a Program-scoped
/// tag so the Symbol-Object decoder's scope-split path is exercised.
/// </summary>
public static readonly AbServerProfile ControlLogix = new(
Family: AbCipPlcFamily.ControlLogix,
AbServerPlcArg: "controllogix",
SeedTags: new AbServerSeedTag[]
{
new("TestDINT", "DINT"),
new("TestREAL", "REAL"),
new("TestBOOL", "BOOL"),
new("TestSINT", "SINT"),
new("TestString","STRING"),
new("TestArray", "DINT", ArraySize: 16),
},
Notes: "Widest-coverage profile — PR 9 baseline. UDTs live in PR 6-shipped Template Object tests; ab_server lacks full UDT emulation.");
/// <summary>
/// CompactLogix — narrower ConnectionSize quirk exercised here. ab_server doesn't
/// enforce the narrower limit itself; the driver-side profile caps it + this simulator
/// honors whatever the client asks for. Tag set is a subset of ControlLogix.
/// </summary>
public static readonly AbServerProfile CompactLogix = new(
Family: AbCipPlcFamily.CompactLogix,
AbServerPlcArg: "compactlogix",
SeedTags: new AbServerSeedTag[]
{
new("TestDINT", "DINT"),
new("TestREAL", "REAL"),
new("TestBOOL", "BOOL"),
},
Notes: "Narrower ConnectionSize than ControlLogix — driver-side profile caps it per PR 10. Tag set mirrors the CompactLogix atomic subset.");
/// <summary>
/// Micro800 — unconnected-only family. ab_server has no explicit micro800 plc mode so
/// we fall back to the nearest CIP-compatible emulation (controllogix) + document the
/// discrepancy. Driver-side path enforcement (empty routing path, unconnected-only
/// sessions) is exercised in the unit suite; this integration profile smoke-tests that
/// reads work end-to-end against the unconnected path.
/// </summary>
public static readonly AbServerProfile Micro800 = new(
Family: AbCipPlcFamily.Micro800,
AbServerPlcArg: "controllogix", // ab_server lacks dedicated micro800 mode — see Notes
SeedTags: new AbServerSeedTag[]
{
new("TestDINT", "DINT"),
new("TestREAL", "REAL"),
},
Notes: "ab_server has no --plc micro800 — falls back to controllogix emulation. Driver side still enforces empty path + unconnected-only per PR 11. Real Micro800 coverage requires a 2080 on a lab rig.");
/// <summary>
/// GuardLogix — safety-capable ControlLogix variant with ViewOnly safety tags. ab_server
/// doesn't emulate the safety subsystem; we preseed a safety-suffixed name (<c>_S</c>) so
/// the driver's read-only classification path is exercised against a real tag.
/// </summary>
public static readonly AbServerProfile GuardLogix = new(
Family: AbCipPlcFamily.GuardLogix,
AbServerPlcArg: "controllogix",
SeedTags: new AbServerSeedTag[]
{
new("TestDINT", "DINT"),
new("SafetyDINT_S", "DINT"), // _S-suffixed → driver classifies as safety-ViewOnly per PR 12
},
Notes: "ab_server has no safety subsystem — this profile emulates the tag-naming contract. Real safety-lock behavior requires a physical GuardLogix 1756-L8xS rig.");
public static IReadOnlyList<AbServerProfile> All { get; } =
new[] { ControlLogix, CompactLogix, Micro800, GuardLogix };
public static AbServerProfile ForFamily(AbCipPlcFamily family) =>
All.FirstOrDefault(p => p.Family == family)
?? throw new ArgumentOutOfRangeException(nameof(family), family, "No integration profile for this family.");
}