081fa693af
I-1: a PUT/GET-disabled CPU (every read denied) stayed Faulted with LastSuccessfulRead frozen, so the comms-loss watchdog reconnected and re-subscribed it every 90 s forever. Both access-denied arms now also record an answer: Faulted is kept (worst state wins), the stamp advances, and the sticky write fault still clears only on a real success. Pinned end to end in Runtime.Tests: the real S7Driver under the real DriverInstanceActor watchdog is not reconnected across 2x the threshold; the control (every read timing out) is. Minors: - m-1: injectable pass clock (UtcNow); "LastSuccessfulRead advanced" is now asserted strictly, including the write-refusal case. - m-2: the editor uses S7TagShapeRules.IsByteAnchored instead of its own list. - m-3: a release that races an init publish also cancels the probe loop the publish started after the release took its snapshot. - m-4: a retained poll failing during a lifecycle operation writes no health. - m-5: a multi-var chunk that fell back memoises its members refused alone. - m-7: access-denied needs S7.Net's exact wording and no transport fault in the chain. - m-8: sync teardown clears the write-side Degraded owner; a sync Dispose racing a running shutdown is tested. Refs #742 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
86 lines
4.4 KiB
C#
86 lines
4.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
|
|
|
|
/// <summary>
|
|
/// Live gate for R07:H4 — a real CPU refusal over real S7comm. The snap7 server has no DB99, so it
|
|
/// answers a read of <c>DB99.DBW0</c> with an S7 item error. That is a per-tag Bad with no health
|
|
/// impact (and LastSuccessfulRead advances), not a Degraded that would trip the comms-loss watchdog
|
|
/// against a reachable PLC.
|
|
/// </summary>
|
|
[Collection(Snap7ServerCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Device", "S7_1500")]
|
|
public sealed class S7_1500RefusalTests(Snap7ServerFixture sim)
|
|
{
|
|
/// <summary>A refused DB read is per-tag Bad, keeps the driver Healthy, and its neighbour reads Good.</summary>
|
|
[Fact]
|
|
public async Task A_read_of_a_missing_DB_is_refused_per_tag_without_degrading()
|
|
{
|
|
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
|
var ct = TestContext.Current.CancellationToken;
|
|
|
|
var baseOptions = S7_1500Profile.BuildOptions(sim.Host, sim.Port);
|
|
var options = new S7DriverOptions
|
|
{
|
|
Host = baseOptions.Host, Port = baseOptions.Port, CpuType = baseOptions.CpuType,
|
|
Rack = baseOptions.Rack, Slot = baseOptions.Slot, Timeout = baseOptions.Timeout,
|
|
Probe = baseOptions.Probe,
|
|
RawTags = [.. baseOptions.RawTags, .. S7RawTags.Entries(
|
|
new S7TagDefinition("Missing/Db99", "DB99.DBW0", S7DataType.UInt16, Writable: false))],
|
|
};
|
|
await using var drv = new S7Driver(options, driverInstanceId: "s7-refusal-live");
|
|
await drv.InitializeAsync("{}", ct);
|
|
var before = drv.GetHealth().LastSuccessfulRead!.Value;
|
|
// Let the wall clock move past the init stamp so "advanced" is asserted strictly (review m-1).
|
|
while (DateTime.UtcNow <= before) await Task.Yield();
|
|
|
|
// Read the refused tag ALONE first: an all-refused pass is exactly the case that used to
|
|
// freeze LastSuccessfulRead and latch Degraded.
|
|
var refused = await drv.ReadAsync(["Missing/Db99"], ct);
|
|
refused[0].StatusCode.ShouldNotBe(0u);
|
|
refused[0].Value.ShouldBeNull();
|
|
var health = drv.GetHealth();
|
|
health.State.ShouldBe(DriverState.Healthy, "a CPU refusal is not a comms fault");
|
|
health.LastSuccessfulRead!.Value.ShouldBeGreaterThan(before, "the refusal is an answer: the stamp advances");
|
|
|
|
// Positive control: the same connection still serves the seeded neighbour.
|
|
var good = await drv.ReadAsync([S7_1500Profile.ProbeTag], ct);
|
|
good[0].StatusCode.ShouldBe(0u);
|
|
Convert.ToInt32(good[0].Value).ShouldBe(S7_1500Profile.ProbeSeedValue);
|
|
}
|
|
|
|
/// <summary>A refused write is per-tag Bad and keeps the driver Healthy; a good write still lands.</summary>
|
|
[Fact]
|
|
public async Task A_write_to_a_missing_DB_is_refused_per_tag_without_degrading()
|
|
{
|
|
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
|
var ct = TestContext.Current.CancellationToken;
|
|
|
|
var baseOptions = S7_1500Profile.BuildOptions(sim.Host, sim.Port);
|
|
var options = new S7DriverOptions
|
|
{
|
|
Host = baseOptions.Host, Port = baseOptions.Port, CpuType = baseOptions.CpuType,
|
|
Rack = baseOptions.Rack, Slot = baseOptions.Slot, Timeout = baseOptions.Timeout,
|
|
Probe = baseOptions.Probe,
|
|
RawTags = [.. baseOptions.RawTags, .. S7RawTags.Entries(
|
|
new S7TagDefinition("Missing/Db99W", "DB99.DBW0", S7DataType.UInt16, Writable: true))],
|
|
};
|
|
await using var drv = new S7Driver(options, driverInstanceId: "s7-refusal-write-live");
|
|
await drv.InitializeAsync("{}", ct);
|
|
|
|
var refused = await drv.WriteAsync([new WriteRequest("Missing/Db99W", (ushort)5)], ct);
|
|
refused[0].StatusCode.ShouldNotBe(0u);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy, "a CPU write refusal is not a comms fault");
|
|
|
|
// Positive control: a write the CPU accepts still lands (and reads back).
|
|
var ok = await drv.WriteAsync([new WriteRequest(S7_1500Profile.WriteScratchTag, (ushort)4711)], ct);
|
|
ok[0].StatusCode.ShouldBe(0u);
|
|
var back = await drv.ReadAsync([S7_1500Profile.WriteScratchTag], ct);
|
|
Convert.ToInt32(back[0].Value).ShouldBe(4711);
|
|
await drv.WriteAsync([new WriteRequest(S7_1500Profile.WriteScratchTag, (ushort)0)], ct);
|
|
}
|
|
}
|