Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/SymbolImport/TiaCsvImportIntegrationTests.cs
2026-04-26 06:32:18 -04:00

84 lines
3.9 KiB
C#

using System.IO;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
using ZB.MOM.WW.OtOpcUa.Driver.S7.SymbolImport;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.SymbolImport;
/// <summary>
/// PR-S7-D1 / #299 — golden-fixture integration test. Loads the canonical TIA Portal
/// CSV export shipped under <c>Fixtures/sample_tia_export.csv</c>, materialises a
/// driver-options object via <c>AddTiaCsvImport</c>, then exercises the runtime read
/// path against the python-snap7 simulator.
/// </summary>
/// <remarks>
/// <para>
/// The fixture's address layout (<c>%MW0</c>, <c>%DB1.DBW10</c>, <c>%DB1.DBD20</c>,
/// <c>%DB1.DBD30</c>, <c>%DB1.DBX50.3</c>) is deliberately aligned with the seed
/// offsets baked into the snap7 S7-1500 profile (<see cref="S7_1500Profile"/>) so
/// a successful round-trip proves the importer's address normalisation lands at
/// exactly the offsets the simulator seeds — a regression in <c>%</c>-stripping or
/// decimal-comma rewriting surfaces here as a read-mismatch, not a flaky timeout.
/// </para>
/// <para>
/// The test is build-only by default (the assertions run only when the simulator
/// fixture reports <c>SkipReason is null</c>) — local dev invokes it with snap7
/// running; CI relies on the fixture's auto-skip when no simulator is reachable.
/// </para>
/// </remarks>
[Collection(Snap7ServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "S7_1500")]
public sealed class TiaCsvImportIntegrationTests(Snap7ServerFixture sim)
{
private static string FixturePath(string name) =>
Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
[Fact]
public async Task Driver_imports_csv_then_reads_seeded_tags()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
// Start with an empty options object pinned at the simulator's endpoint, then
// layer the fixture CSV's tags on top via AddTiaCsvImport. The merge keeps the
// endpoint config (Host, Port, CpuType) untouched and adds the imported tags.
var baseOptions = new S7DriverOptions
{
Host = sim.Host,
Port = sim.Port,
CpuType = global::S7.Net.CpuType.S71500,
Timeout = TimeSpan.FromSeconds(5),
Probe = new S7ProbeOptions { Enabled = false },
Tags = [],
};
var options = baseOptions.AddTiaCsvImport(FixturePath("sample_tia_export.csv"), out var importResult);
// Fixture has 8 rows: 6 importable + 1 UDT placeholder + 1 HMI-hidden (skipped).
importResult.ParsedCount.ShouldBe(7);
importResult.SkippedCount.ShouldBe(1);
importResult.UdtPlaceholderCount.ShouldBe(1);
options.Tags.Count.ShouldBe(7);
await using var drv = new S7Driver(options, driverInstanceId: "s7-tia-import");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Read the seed-aligned tags by their browse-name references. ProbeWord at MW0
// doesn't have a snap7 seed (the simulator profile seeds DB1.DBW0 instead), so
// we focus on the DB-anchored tags whose offsets match the S7_1500Profile seeds.
var snapshots = await drv.ReadAsync(
["SmokeI16", "SmokeI32", "SmokeF32", "SmokeBool"],
TestContext.Current.CancellationToken);
snapshots.Count.ShouldBe(4);
foreach (var s in snapshots) s.StatusCode.ShouldBe(0u, "imported-then-read must succeed end-to-end");
Convert.ToInt32(snapshots[0].Value).ShouldBe((int)S7_1500Profile.SmokeI16SeedValue);
Convert.ToInt32(snapshots[1].Value).ShouldBe(S7_1500Profile.SmokeI32SeedValue);
Convert.ToSingle(snapshots[2].Value).ShouldBe(S7_1500Profile.SmokeF32SeedValue, tolerance: 0.0001f);
Convert.ToBoolean(snapshots[3].Value).ShouldBeTrue();
}
}