fix(s7): a read pass that observes nothing reports nothing — empty pass no longer clears Faulted
This commit is contained in:
@@ -672,10 +672,17 @@ public sealed class S7Driver
|
|||||||
// arms abandon the batch mid-pass, and a partially-observed pass must not stamp a
|
// arms abandon the batch mid-pass, and a partially-observed pass must not stamp a
|
||||||
// verdict. LastSuccessfulRead advances whenever ANYTHING read, even into a
|
// verdict. LastSuccessfulRead advances whenever ANYTHING read, even into a
|
||||||
// Degraded/Faulted state — see ReadPassOutcome for why that is load-bearing.
|
// Degraded/Faulted state — see ReadPassOutcome for why that is load-bearing.
|
||||||
_health = new DriverHealth(
|
//
|
||||||
outcome.WorstFailure,
|
// A pass that observed NOTHING — an empty batch, or one whose every reference was
|
||||||
outcome.AnySuccess ? now : _health.LastSuccessfulRead,
|
// unresolvable — reports nothing: it writes no health at all, preserving a prior
|
||||||
outcome.WorstFailure == DriverState.Healthy ? null : outcome.FirstError);
|
// Faulted (e.g. the access-denied config alert) that a Healthy write would have erased.
|
||||||
|
if (outcome.AnySuccess || outcome.WorstFailure != DriverState.Healthy)
|
||||||
|
{
|
||||||
|
_health = new DriverHealth(
|
||||||
|
outcome.WorstFailure,
|
||||||
|
outcome.AnySuccess ? now : _health.LastSuccessfulRead,
|
||||||
|
outcome.WorstFailure == DriverState.Healthy ? null : outcome.FirstError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally { _gate.Release(); }
|
finally { _gate.Release(); }
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -555,6 +555,44 @@ public sealed class S7SpanReadParityTests
|
|||||||
before!.Value, "one tag read fine, so the watchdog's evidence must still advance");
|
before!.Value, "one tag read fine, so the watchdog's evidence must still advance");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A pass that observes NOTHING — every reference unresolvable, so not one tag was read and
|
||||||
|
/// not one failed — reports nothing: it must leave the driver's prior <c>Faulted</c> config
|
||||||
|
/// alert (and its <c>LastSuccessfulRead</c>) exactly as it found them.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Deciding health once per pass means the pass has to answer "what did I observe?", and a
|
||||||
|
/// degenerate pass observed nothing at all — writing <c>Healthy</c> for it would invent
|
||||||
|
/// evidence and silently clear a PUT/GET config alert an operator still has to act on. The
|
||||||
|
/// per-item health writes this replaced had that property for free (no item, no write); the
|
||||||
|
/// end-of-pass guard is what preserves it.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task A_pass_that_observes_nothing_leaves_a_prior_Faulted_untouched()
|
||||||
|
{
|
||||||
|
var ct = TestContext.Current.CancellationToken;
|
||||||
|
var factory = new ImageS7PlcFactory(Images());
|
||||||
|
factory.PerTagFaults["DB1.DBW0"] = new PlcException(ErrorCode.ReadData, "Accessing object not allowed");
|
||||||
|
using var driver = new S7Driver(Options(NarrowTags(), maxGap: -1), "s7-observes-nothing", factory);
|
||||||
|
await driver.InitializeAsync("{}", ct);
|
||||||
|
|
||||||
|
// Drive the driver into the access-denied config alert first.
|
||||||
|
_ = await driver.ReadAsync(["A"], ct);
|
||||||
|
var faulted = driver.GetHealth();
|
||||||
|
faulted.State.ShouldBe(DriverState.Faulted);
|
||||||
|
|
||||||
|
// Now a pass whose every reference is unresolvable: they are stamped BadNodeIdUnknown
|
||||||
|
// before the planner ever sees them, so no span, no multi-var item and no per-tag read
|
||||||
|
// happens — the pass observes neither a success nor a failure.
|
||||||
|
var results = await driver.ReadAsync(["NoSuchTag", "AlsoMissing"], ct);
|
||||||
|
results.ShouldAllBe(s => s.StatusCode == StatusBadNodeIdUnknown);
|
||||||
|
|
||||||
|
var after = driver.GetHealth();
|
||||||
|
after.State.ShouldBe(DriverState.Faulted, "an observe-nothing pass must not clear the config alert");
|
||||||
|
after.LastError.ShouldBe(faulted.LastError);
|
||||||
|
after.LastSuccessfulRead.ShouldBe(faulted.LastSuccessfulRead, "nothing was read, so nothing advances");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An answer of the wrong SHAPE — one item short — cannot be mapped back onto the request's
|
/// An answer of the wrong SHAPE — one item short — cannot be mapped back onto the request's
|
||||||
/// indexes, so the driver refuses to guess which tag got which value and falls the whole
|
/// indexes, so the driver refuses to guess which tag got which value and falls the whole
|
||||||
|
|||||||
Reference in New Issue
Block a user