Merge pull request (#142) - ab_server per-family profiles
This commit was merged in pull request #142.
This commit is contained in:
@@ -189,6 +189,35 @@ Modbus has no native String, DateTime, or Int64 — those rows are skipped on th
|
||||
- **ab_server tag-type coverage is finite** (BOOL, DINT, REAL, arrays, basic strings). UDTs and `Program:` scoping are not fully implemented. Document an "ab_server-supported tag set" in the harness and exclude the rest from default CI; UDT coverage moves to the Studio 5000 Emulate golden-box tier.
|
||||
- CIP has no native subscriptions, so polling behavior matches real hardware.
|
||||
|
||||
### CI fixture (task #180)
|
||||
|
||||
The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` exposes two test-time contracts:
|
||||
|
||||
- **`AbServerFixture(AbServerProfile)`** — starts the simulator with the CLI args composed from the profile's `--plc` family + seed-tag set. One fixture instance per family, one simulator process per test case (smoke tier). For larger suites that can share a simulator across several reads/writes, use a `IClassFixture<AbServerFixture>` wrapper per family.
|
||||
- **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — the four per-family profiles. Drives the simulator's `--plc` mode + the preseed `--tag name:type[:size]` set. Micro800 + GuardLogix fall back to `controllogix` under the hood because ab_server has no dedicated mode for them — the driver-side family profile still enforces the narrower connection shape / safety classification separately.
|
||||
|
||||
**CI step (intended — fleet-ops to wire):**
|
||||
|
||||
```yaml
|
||||
# GitHub Actions step placed before `dotnet test`:
|
||||
- name: Fetch ab_server
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ver = '<pinned libplctag release tag>'
|
||||
$url = "https://github.com/libplctag/libplctag/releases/download/$ver/ab_server-windows-x64.zip"
|
||||
Invoke-WebRequest $url -OutFile $env:RUNNER_TEMP/ab_server.zip
|
||||
# SHA256 check against a pinned value recorded in this repo's CI lockfile — drift = fail closed
|
||||
$expected = '<pinned sha256>'
|
||||
$actual = (Get-FileHash -Algorithm SHA256 $env:RUNNER_TEMP/ab_server.zip).Hash
|
||||
if ($expected -ne $actual) { throw "ab_server hash mismatch" }
|
||||
Expand-Archive $env:RUNNER_TEMP/ab_server.zip -DestinationPath $env:RUNNER_TEMP/ab_server
|
||||
echo "$env:RUNNER_TEMP/ab_server" >> $env:GITHUB_PATH
|
||||
```
|
||||
|
||||
The fixture's `LocateBinary()` picks the binary up off PATH so the C# harness doesn't own the download — CI YAML is the right place for version pinning + hash verification. Developer workstations install the binary once from source (`cmake + make ab_server` under a libplctag clone) and the same fixture works identically.
|
||||
|
||||
Tests without ab_server on PATH are marked `Skip` via `AbServerFactAttribute` / `AbServerTheoryAttribute`, so fresh-clone runs without the simulator still pass all unit suites in this project.
|
||||
|
||||
---
|
||||
|
||||
## 3. Allen-Bradley Legacy (SLC 500 / MicroLogix, PCCC)
|
||||
|
||||
@@ -8,37 +8,43 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
/// <summary>
|
||||
/// End-to-end smoke tests that exercise the real libplctag stack against a running
|
||||
/// <c>ab_server</c>. Skipped when the binary isn't on PATH (<see cref="AbServerFactAttribute"/>).
|
||||
/// Parametrized over <see cref="KnownProfiles.All"/> so one test file covers every family
|
||||
/// (ControlLogix / CompactLogix / Micro800 / GuardLogix).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intentionally minimal — per-family + per-capability coverage ships in PRs 9–12 once the
|
||||
/// integration harness is CI-ready. This file exists at PR 3 time to prove the wire path
|
||||
/// works end-to-end on developer boxes that have <c>ab_server</c>.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Requires", "AbServer")]
|
||||
public sealed class AbCipReadSmokeTests : IAsyncLifetime
|
||||
public sealed class AbCipReadSmokeTests
|
||||
{
|
||||
private readonly AbServerFixture _fixture = new();
|
||||
public static IEnumerable<object[]> Profiles =>
|
||||
KnownProfiles.All.Select(p => new object[] { p });
|
||||
|
||||
public async ValueTask InitializeAsync() => await _fixture.InitializeAsync();
|
||||
public async ValueTask DisposeAsync() => await _fixture.DisposeAsync();
|
||||
|
||||
[AbServerFact]
|
||||
public async Task Driver_reads_DInt_from_ab_server()
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
public async Task Driver_reads_seeded_DInt_from_ab_server(AbServerProfile profile)
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
var fixture = new AbServerFixture(profile);
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions($"ab://127.0.0.1:{_fixture.Port}/1,0", AbCipPlcFamily.ControlLogix)],
|
||||
Tags = [new AbCipTagDefinition("Counter", $"ab://127.0.0.1:{_fixture.Port}/1,0", "TestDINT", AbCipDataType.DInt)],
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
}, "drv-smoke");
|
||||
var deviceUri = $"ab://127.0.0.1:{fixture.Port}/1,0";
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)],
|
||||
Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)],
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
}, $"drv-smoke-{profile.Family}");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,28 +6,44 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Shared fixture that starts libplctag's <c>ab_server</c> simulator in the background for
|
||||
/// the duration of an integration test collection. Binary is expected on PATH; the per-test
|
||||
/// JSON profile is passed via <c>--config</c>.
|
||||
/// the duration of an integration test collection. The fixture takes an
|
||||
/// <see cref="AbServerProfile"/> (see <see cref="KnownProfiles"/>) so each AB family — ControlLogix,
|
||||
/// CompactLogix, Micro800, GuardLogix — starts the simulator with the right <c>--plc</c>
|
||||
/// mode + preseed tag set. Binary is expected on PATH; CI resolves that via a job step
|
||||
/// that downloads the pinned Windows build from libplctag GitHub Releases before
|
||||
/// <c>dotnet test</c> — see <c>docs/v2/test-data-sources.md §2.CI</c> for the exact step.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><c>ab_server</c> is a C binary shipped in the same repo as libplctag (see
|
||||
/// <c>test-data-sources.md</c> §2 and plan decision #99). On a developer workstation it's
|
||||
/// built once from source and placed on PATH; in CI we intend to publish a prebuilt Windows
|
||||
/// x64 binary as a GitHub release asset in a follow-up PR so the fixture can download +
|
||||
/// extract it at setup time. Until then every test in this project is skipped when
|
||||
/// <c>ab_server</c> is not locatable.</para>
|
||||
/// <para><c>ab_server</c> is a C binary shipped in libplctag's repo (MIT). On developer
|
||||
/// workstations it's built once from source and placed on PATH; on CI the workflow file
|
||||
/// fetches a version-pinned prebuilt + stages it. Tests skip (via
|
||||
/// <see cref="AbServerFactAttribute"/>) when the binary is not on PATH so a fresh clone
|
||||
/// without the simulator still gets a green unit-test run.</para>
|
||||
///
|
||||
/// <para>Per-family JSON profiles (ControlLogix / CompactLogix / Micro800 / GuardLogix)
|
||||
/// ship under <c>Profiles/</c> and drive the simulator's tag shape — this is where the
|
||||
/// UDT + Program-scope coverage gap will be filled by the hand-rolled stub in PR 6.</para>
|
||||
/// <para>Per-family profiles live in <see cref="KnownProfiles"/>. When a test wants a
|
||||
/// specific family, instantiate the fixture with that profile — either via a
|
||||
/// <see cref="IClassFixture{TFixture}"/> derived type or by constructing directly in a
|
||||
/// parametric test (the latter is used below for the smoke suite).</para>
|
||||
/// </remarks>
|
||||
public sealed class AbServerFixture : IAsyncLifetime
|
||||
{
|
||||
private Process? _proc;
|
||||
public int Port { get; } = 44818;
|
||||
|
||||
/// <summary>The profile the simulator was started with. Same instance the driver-side options should use.</summary>
|
||||
public AbServerProfile Profile { get; }
|
||||
public int Port { get; }
|
||||
public bool IsAvailable { get; private set; }
|
||||
|
||||
public AbServerFixture() : this(KnownProfiles.ControlLogix, AbServerProfile.DefaultPort) { }
|
||||
|
||||
public AbServerFixture(AbServerProfile profile) : this(profile, AbServerProfile.DefaultPort) { }
|
||||
|
||||
public AbServerFixture(AbServerProfile profile, int port)
|
||||
{
|
||||
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
Port = port;
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => InitializeAsync(default);
|
||||
public ValueTask DisposeAsync() => DisposeAsync(default);
|
||||
|
||||
@@ -45,7 +61,7 @@ public sealed class AbServerFixture : IAsyncLifetime
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = binary,
|
||||
Arguments = $"--port {Port} --plc controllogix",
|
||||
Arguments = Profile.BuildCliArgs(Port),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
@@ -75,7 +91,7 @@ public sealed class AbServerFixture : IAsyncLifetime
|
||||
|
||||
/// <summary>
|
||||
/// Locate <c>ab_server</c> on PATH. Returns <c>null</c> when missing — tests that
|
||||
/// depend on it should use <see cref="AbServerFact"/> so CI runs without the binary
|
||||
/// depend on it should use <see cref="AbServerFactAttribute"/> so CI runs without the binary
|
||||
/// simply skip rather than fail.
|
||||
/// </summary>
|
||||
public static string? LocateBinary()
|
||||
@@ -107,3 +123,17 @@ public sealed class AbServerFactAttribute : FactAttribute
|
||||
Skip = "ab_server not on PATH; install libplctag test binaries to run.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>[Theory]</c>-equivalent that skips when <c>ab_server</c> is not on PATH. Pair with
|
||||
/// <c>[MemberData(nameof(KnownProfiles.All))]</c>-style providers to run one theory row per
|
||||
/// profile so a single test covers all four families.
|
||||
/// </summary>
|
||||
public sealed class AbServerTheoryAttribute : TheoryAttribute
|
||||
{
|
||||
public AbServerTheoryAttribute()
|
||||
{
|
||||
if (AbServerFixture.LocateBinary() is null)
|
||||
Skip = "ab_server not on PATH; install libplctag test binaries to run.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
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 <arg></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 <name>:<type>[:<size>]</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><name>:<type>[:<array_size>]</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 9–12.</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.");
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-unit tests for the profile → CLI arg composition. Runs without <c>ab_server</c>
|
||||
/// on PATH so CI without the binary still exercises these contracts + catches any
|
||||
/// profile-definition drift (e.g. a typo in <c>--plc</c> mapping would silently make the
|
||||
/// simulator boot with the wrong family).
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbServerProfileTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildCliArgs_Emits_Port_And_Plc_And_TagFlags()
|
||||
{
|
||||
var profile = new AbServerProfile(
|
||||
Family: AbCipPlcFamily.ControlLogix,
|
||||
AbServerPlcArg: "controllogix",
|
||||
SeedTags: new AbServerSeedTag[]
|
||||
{
|
||||
new("A", "DINT"),
|
||||
new("B", "REAL"),
|
||||
},
|
||||
Notes: "test");
|
||||
|
||||
profile.BuildCliArgs(44818).ShouldBe("--port 44818 --plc controllogix --tag A:DINT --tag B:REAL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCliArgs_NoSeedTags_Emits_Just_Port_And_Plc()
|
||||
{
|
||||
var profile = new AbServerProfile(
|
||||
AbCipPlcFamily.ControlLogix, "controllogix", [], "empty");
|
||||
|
||||
profile.BuildCliArgs(5000).ShouldBe("--port 5000 --plc controllogix");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbServerSeedTag_ArraySize_FormatsAsThirdSegment()
|
||||
{
|
||||
new AbServerSeedTag("TestArray", "DINT", ArraySize: 16)
|
||||
.ToCliSpec().ShouldBe("TestArray:DINT:16");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbServerSeedTag_NoArraySize_TwoSegments()
|
||||
{
|
||||
new AbServerSeedTag("TestScalar", "REAL")
|
||||
.ToCliSpec().ShouldBe("TestScalar:REAL");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AbCipPlcFamily.ControlLogix, "controllogix")]
|
||||
[InlineData(AbCipPlcFamily.CompactLogix, "compactlogix")]
|
||||
[InlineData(AbCipPlcFamily.Micro800, "controllogix")] // falls back — ab_server lacks dedicated mode
|
||||
[InlineData(AbCipPlcFamily.GuardLogix, "controllogix")] // falls back — ab_server lacks safety subsystem
|
||||
public void KnownProfiles_ForFamily_Returns_Expected_AbServerPlcArg(AbCipPlcFamily family, string expected)
|
||||
{
|
||||
KnownProfiles.ForFamily(family).AbServerPlcArg.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownProfiles_All_Covers_Every_Family()
|
||||
{
|
||||
var covered = KnownProfiles.All.Select(p => p.Family).ToHashSet();
|
||||
foreach (var family in Enum.GetValues<AbCipPlcFamily>())
|
||||
covered.ShouldContain(family, $"Family {family} is missing a KnownProfiles entry.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownProfiles_ControlLogix_Includes_AllAtomicTypes()
|
||||
{
|
||||
var tags = KnownProfiles.ControlLogix.SeedTags.Select(t => t.AbServerType).ToHashSet();
|
||||
tags.ShouldContain("DINT");
|
||||
tags.ShouldContain("REAL");
|
||||
tags.ShouldContain("BOOL");
|
||||
tags.ShouldContain("SINT");
|
||||
tags.ShouldContain("STRING");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownProfiles_GuardLogix_SeedsSafetySuffixedTag()
|
||||
{
|
||||
KnownProfiles.GuardLogix.SeedTags
|
||||
.ShouldContain(t => t.Name.EndsWith("_S"), "GuardLogix profile must seed at least one _S-suffixed tag for safety-classification coverage.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user