docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
This commit is contained in:
@@ -4,19 +4,29 @@ using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests;
|
||||
|
||||
/// <summary>Test fake implementation of <see cref="ITagUpstreamSource"/> for verifying subscription behavior.</summary>
|
||||
public sealed class FakeUpstream : ITagUpstreamSource
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DataValueSnapshot> _values = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, List<Action<string, DataValueSnapshot>>> _subs
|
||||
= new(StringComparer.Ordinal);
|
||||
/// <summary>Gets the current count of active subscriptions.</summary>
|
||||
public int ActiveSubscriptionCount { get; private set; }
|
||||
|
||||
/// <summary>Sets a tag value without notifying subscribers.</summary>
|
||||
/// <param name="path">The tag path to set.</param>
|
||||
/// <param name="value">The value to set for the tag.</param>
|
||||
/// <param name="statusCode">The OPC UA status code for the value.</param>
|
||||
public void Set(string path, object? value, uint statusCode = 0u)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
_values[path] = new DataValueSnapshot(value, statusCode, now, now);
|
||||
}
|
||||
|
||||
/// <summary>Sets a tag value and notifies all current subscribers.</summary>
|
||||
/// <param name="path">The tag path to set.</param>
|
||||
/// <param name="value">The value to set for the tag.</param>
|
||||
/// <param name="statusCode">The OPC UA status code for the value.</param>
|
||||
public void Push(string path, object? value, uint statusCode = 0u)
|
||||
{
|
||||
Set(path, value, statusCode);
|
||||
@@ -28,10 +38,15 @@ public sealed class FakeUpstream : ITagUpstreamSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the current value of a tag, or returns a bad-status snapshot if not set.</summary>
|
||||
/// <param name="path">The tag path to read.</param>
|
||||
public DataValueSnapshot ReadTag(string path)
|
||||
=> _values.TryGetValue(path, out var v) ? v
|
||||
: new DataValueSnapshot(null, 0x80340000u, null, DateTime.UtcNow);
|
||||
|
||||
/// <summary>Subscribes an observer to tag changes for the given path.</summary>
|
||||
/// <param name="path">The tag path to subscribe to.</param>
|
||||
/// <param name="observer">The observer callback to invoke on tag changes.</param>
|
||||
public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
|
||||
{
|
||||
var list = _subs.GetOrAdd(path, _ => []);
|
||||
@@ -40,13 +55,19 @@ public sealed class FakeUpstream : ITagUpstreamSource
|
||||
return new Unsub(this, path, observer);
|
||||
}
|
||||
|
||||
/// <summary>Disposable subscription handle that unsubscribes the observer when disposed.</summary>
|
||||
private sealed class Unsub : IDisposable
|
||||
{
|
||||
private readonly FakeUpstream _up;
|
||||
private readonly string _path;
|
||||
private readonly Action<string, DataValueSnapshot> _observer;
|
||||
/// <summary>Initializes the unsubscription handle with references needed to clean up the subscription.</summary>
|
||||
/// <param name="up">The upstream source containing the subscription list.</param>
|
||||
/// <param name="path">The tag path to unsubscribe from.</param>
|
||||
/// <param name="observer">The observer to remove from the subscription list.</param>
|
||||
public Unsub(FakeUpstream up, string path, Action<string, DataValueSnapshot> observer)
|
||||
{ _up = up; _path = path; _observer = observer; }
|
||||
/// <summary>Removes the observer from the subscription list.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_up._subs.TryGetValue(_path, out var list))
|
||||
|
||||
@@ -16,12 +16,14 @@ public sealed class MessageTemplateTests
|
||||
private static DataValueSnapshot? Resolver(Dictionary<string, DataValueSnapshot> map, string path)
|
||||
=> map.TryGetValue(path, out var v) ? v : null;
|
||||
|
||||
/// <summary>Verifies template with no tokens is returned unchanged.</summary>
|
||||
[Fact]
|
||||
public void No_tokens_returns_template_unchanged()
|
||||
{
|
||||
MessageTemplate.Resolve("No tokens here", _ => null).ShouldBe("No tokens here");
|
||||
}
|
||||
|
||||
/// <summary>Verifies single token in template is correctly substituted.</summary>
|
||||
[Fact]
|
||||
public void Single_token_substituted()
|
||||
{
|
||||
@@ -29,6 +31,7 @@ public sealed class MessageTemplateTests
|
||||
MessageTemplate.Resolve("Temp={Tank/Temp}C", p => Resolver(map, p)).ShouldBe("Temp=75.5C");
|
||||
}
|
||||
|
||||
/// <summary>Verifies multiple tokens in template are all substituted.</summary>
|
||||
[Fact]
|
||||
public void Multiple_tokens_substituted()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public sealed class MessageTemplateTests
|
||||
MessageTemplate.Resolve("{A}/{B}", p => Resolver(map, p)).ShouldBe("10/on");
|
||||
}
|
||||
|
||||
/// <summary>Verifies tokens with bad quality become question marks.</summary>
|
||||
[Fact]
|
||||
public void Bad_quality_token_becomes_question_mark()
|
||||
{
|
||||
@@ -47,12 +51,14 @@ public sealed class MessageTemplateTests
|
||||
MessageTemplate.Resolve("value={Bad}", p => Resolver(map, p)).ShouldBe("value={?}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies unknown token paths become question marks.</summary>
|
||||
[Fact]
|
||||
public void Unknown_path_becomes_question_mark()
|
||||
{
|
||||
MessageTemplate.Resolve("value={DoesNotExist}", _ => null).ShouldBe("value={?}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies null values with good quality become question marks.</summary>
|
||||
[Fact]
|
||||
public void Null_value_with_good_quality_becomes_question_mark()
|
||||
{
|
||||
@@ -60,6 +66,7 @@ public sealed class MessageTemplateTests
|
||||
MessageTemplate.Resolve("{X}", p => Resolver(map, p)).ShouldBe("{?}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies tokens containing slashes and dots are correctly resolved.</summary>
|
||||
[Fact]
|
||||
public void Tokens_with_slashes_and_dots_resolved()
|
||||
{
|
||||
@@ -71,18 +78,21 @@ public sealed class MessageTemplateTests
|
||||
.ShouldBe("rpm=1200");
|
||||
}
|
||||
|
||||
/// <summary>Verifies empty template returns an empty string.</summary>
|
||||
[Fact]
|
||||
public void Empty_template_returns_empty()
|
||||
{
|
||||
MessageTemplate.Resolve("", _ => null).ShouldBe("");
|
||||
}
|
||||
|
||||
/// <summary>Verifies null template returns an empty string without throwing.</summary>
|
||||
[Fact]
|
||||
public void Null_template_returns_empty_without_throwing()
|
||||
{
|
||||
MessageTemplate.Resolve(null!, _ => null).ShouldBe("");
|
||||
}
|
||||
|
||||
/// <summary>Verifies ExtractTokenPaths returns all token paths from a template.</summary>
|
||||
[Fact]
|
||||
public void ExtractTokenPaths_returns_every_distinct_token()
|
||||
{
|
||||
@@ -90,6 +100,7 @@ public sealed class MessageTemplateTests
|
||||
tokens.ShouldBe(new[] { "A", "B", "A", "C" });
|
||||
}
|
||||
|
||||
/// <summary>Verifies ExtractTokenPaths returns empty for templates without tokens.</summary>
|
||||
[Fact]
|
||||
public void ExtractTokenPaths_empty_for_tokenless_template()
|
||||
{
|
||||
@@ -98,6 +109,7 @@ public sealed class MessageTemplateTests
|
||||
MessageTemplate.ExtractTokenPaths(null).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies whitespace inside token is trimmed during resolution.</summary>
|
||||
[Fact]
|
||||
public void Whitespace_inside_token_is_trimmed()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class Part9StateMachineTests
|
||||
private static readonly DateTime T0 = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
private static AlarmConditionState Fresh() => AlarmConditionState.Fresh("alarm-1", T0);
|
||||
|
||||
/// <summary>Verifies that an inactive alarm becomes active and emits Activated when predicate becomes true.</summary>
|
||||
[Fact]
|
||||
public void Predicate_true_on_inactive_becomes_active_and_emits_Activated()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public sealed class Part9StateMachineTests
|
||||
r.State.LastActiveUtc.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an active alarm becomes inactive and emits Cleared when predicate becomes false.</summary>
|
||||
[Fact]
|
||||
public void Predicate_false_on_active_becomes_inactive_and_emits_Cleared()
|
||||
{
|
||||
@@ -36,6 +38,7 @@ public sealed class Part9StateMachineTests
|
||||
r.State.LastClearedUtc.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that predicate changes with unchanged state emit None.</summary>
|
||||
[Fact]
|
||||
public void Predicate_unchanged_state_emits_None()
|
||||
{
|
||||
@@ -43,6 +46,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a disabled alarm ignores predicate changes.</summary>
|
||||
[Fact]
|
||||
public void Disabled_alarm_ignores_predicate()
|
||||
{
|
||||
@@ -52,6 +56,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that acknowledging an unacknowledged alarm records the user and emits Acknowledged.</summary>
|
||||
[Fact]
|
||||
public void Acknowledge_from_unacked_records_user_and_emits()
|
||||
{
|
||||
@@ -64,6 +69,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.Acknowledged);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that acknowledging an already-acknowledged alarm is a no-op.</summary>
|
||||
[Fact]
|
||||
public void Acknowledge_when_already_acked_is_noop()
|
||||
{
|
||||
@@ -73,6 +79,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that acknowledging without a user throws ArgumentException.</summary>
|
||||
[Fact]
|
||||
public void Acknowledge_without_user_throws()
|
||||
{
|
||||
@@ -80,6 +87,7 @@ public sealed class Part9StateMachineTests
|
||||
Part9StateMachine.ApplyAcknowledge(Fresh(), "", null, T0));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that confirming after clear records the user and emits Confirmed.</summary>
|
||||
[Fact]
|
||||
public void Confirm_after_clear_records_user_and_emits()
|
||||
{
|
||||
@@ -95,6 +103,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.Confirmed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that one-shot shelve suppresses the next activation emission.</summary>
|
||||
[Fact]
|
||||
public void OneShotShelve_suppresses_next_activation_emission()
|
||||
{
|
||||
@@ -104,6 +113,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.Suppressed, "but subscribers don't see it");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that one-shot shelve expires when the alarm clears.</summary>
|
||||
[Fact]
|
||||
public void OneShotShelve_expires_on_clear()
|
||||
{
|
||||
@@ -114,6 +124,7 @@ public sealed class Part9StateMachineTests
|
||||
r.State.Shelving.Kind.ShouldBe(ShelvingKind.Unshelved, "OneShot expires on clear");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timed shelve requires a future unshelve time.</summary>
|
||||
[Fact]
|
||||
public void TimedShelve_requires_future_unshelve_time()
|
||||
{
|
||||
@@ -121,6 +132,7 @@ public sealed class Part9StateMachineTests
|
||||
Part9StateMachine.ApplyTimedShelve(Fresh(), "alice", T0, T0.AddSeconds(5)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timed shelve expires via shelving check at the specified time.</summary>
|
||||
[Fact]
|
||||
public void TimedShelve_expires_via_shelving_check()
|
||||
{
|
||||
@@ -140,6 +152,7 @@ public sealed class Part9StateMachineTests
|
||||
after.State.Comments.Any(c => c.Kind == "AutoUnshelve").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unshelving an unshelved alarm is a no-op.</summary>
|
||||
[Fact]
|
||||
public void Unshelve_from_unshelved_is_noop()
|
||||
{
|
||||
@@ -147,6 +160,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that explicit unshelve emits an Unshelved event.</summary>
|
||||
[Fact]
|
||||
public void Explicit_Unshelve_emits_event()
|
||||
{
|
||||
@@ -156,6 +170,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.Unshelved);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that adding a comment appends to the audit trail and emits CommentAdded.</summary>
|
||||
[Fact]
|
||||
public void AddComment_appends_to_audit_trail_with_event()
|
||||
{
|
||||
@@ -167,6 +182,7 @@ public sealed class Part9StateMachineTests
|
||||
r.Emission.ShouldBe(EmissionKind.CommentAdded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that comments are append-only and never rewritten.</summary>
|
||||
[Fact]
|
||||
public void Comments_are_append_only_never_rewritten()
|
||||
{
|
||||
@@ -179,6 +195,7 @@ public sealed class Part9StateMachineTests
|
||||
s.Comments[2].User.ShouldBe("carol");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a full alarm lifecycle walk produces all expected emissions.</summary>
|
||||
[Fact]
|
||||
public void Full_lifecycle_walk_produces_every_expected_emission()
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
MessageTemplate: msg,
|
||||
PredicateScriptSource: predicate);
|
||||
|
||||
/// <summary>Verifies that LoadAsync compiles the alarm predicate and subscribes to all referenced upstream tags.</summary>
|
||||
[Fact]
|
||||
public async Task Load_compiles_and_subscribes_to_referenced_upstreams()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
up.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that compile failures across multiple alarms are aggregated into a single error.</summary>
|
||||
[Fact]
|
||||
public async Task Compile_failures_aggregated_into_one_error()
|
||||
{
|
||||
@@ -60,6 +62,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
ex.Message.ShouldContain("2 alarm(s) did not compile");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an upstream tag change triggers predicate re-evaluation and emits an Activated event.</summary>
|
||||
[Fact]
|
||||
public async Task Upstream_change_re_evaluates_predicate_and_emits_Activated()
|
||||
{
|
||||
@@ -80,6 +83,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
eng.GetState("HighTemp")!.Active.ShouldBe(AlarmActiveState.Active);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing an upstream tag value emits a Cleared event and transitions the alarm to Inactive.</summary>
|
||||
[Fact]
|
||||
public async Task Clearing_upstream_emits_Cleared_event()
|
||||
{
|
||||
@@ -100,6 +104,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
eng.GetState("HighTemp")!.Active.ShouldBe(AlarmActiveState.Inactive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the message template resolves current tag values at the moment of alarm emission.</summary>
|
||||
[Fact]
|
||||
public async Task Message_template_resolves_tag_values_at_emission()
|
||||
{
|
||||
@@ -124,6 +129,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
events[0].Message.ShouldBe("Temp 150C exceeded limit 100C");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync records the operator user and persists the ack state to the store.</summary>
|
||||
[Fact]
|
||||
public async Task Ack_records_user_and_persists_to_store()
|
||||
{
|
||||
@@ -143,6 +149,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
persisted.Comments.Any(c => c.Kind == "Acknowledge" && c.User == "alice").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that startup recovery restores the persisted ack state but re-derives the active state from the live predicate.</summary>
|
||||
[Fact]
|
||||
public async Task Startup_recovery_preserves_ack_but_rederives_active_from_predicate()
|
||||
{
|
||||
@@ -190,6 +197,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
s.LastAckUser.ShouldBe("alice");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a shelved alarm transitions its internal state on activation but suppresses the Activated emission.</summary>
|
||||
[Fact]
|
||||
public async Task Shelved_active_transitions_state_but_suppresses_emission()
|
||||
{
|
||||
@@ -213,6 +221,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
"state still advances so startup recovery is consistent");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a runtime exception thrown by a predicate script leaves the alarm state unchanged and does not affect other alarms.</summary>
|
||||
[Fact]
|
||||
public async Task Predicate_runtime_exception_does_not_transition_state()
|
||||
{
|
||||
@@ -229,6 +238,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
eng.GetState("GoodScript")!.Active.ShouldBe(AlarmActiveState.Active);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a disabled alarm does not activate on predicate change and resumes normally after being re-enabled.</summary>
|
||||
[Fact]
|
||||
public async Task Disable_prevents_activation_until_re_enabled()
|
||||
{
|
||||
@@ -249,6 +259,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
await WaitForAsync(() => eng.GetState("HighTemp")!.Active == AlarmActiveState.Active);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AddCommentAsync appends to the audit trail without changing the alarm's active or ack state.</summary>
|
||||
[Fact]
|
||||
public async Task AddComment_appends_to_audit_without_state_change()
|
||||
{
|
||||
@@ -266,6 +277,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
s.Comments[0].Kind.ShouldBe("AddComment");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that predicate scripts are forbidden from calling SetVirtualTag, and that the exception is isolated without state change.</summary>
|
||||
[Fact]
|
||||
public async Task Predicate_scripts_cannot_SetVirtualTag()
|
||||
{
|
||||
@@ -289,6 +301,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
eng.GetState("Bad")!.Active.ShouldBe(AlarmActiveState.Inactive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the engine releases all upstream tag subscriptions.</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_releases_upstream_subscriptions()
|
||||
{
|
||||
@@ -303,6 +316,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
up.ActiveSubscriptionCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent reads of alarm state during dictionary mutations do not throw (regression for Core.ScriptedAlarms-001).</summary>
|
||||
[Fact]
|
||||
public async Task Concurrent_reads_during_mutation_do_not_throw(/* Core.ScriptedAlarms-001 */)
|
||||
{
|
||||
@@ -371,6 +385,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (1) Timed-shelve auto-expiry driven by the engine's shelving timer via an
|
||||
// injectable clock — the clock and scriptTimeout constructor parameters
|
||||
// exist for exactly this.
|
||||
/// <summary>Verifies that a timed shelve automatically expires when the engine's shelving check runs past the unshelve time.</summary>
|
||||
[Fact]
|
||||
public async Task TimedShelve_auto_expires_when_engine_shelving_check_runs(/* -012 (1) */)
|
||||
{
|
||||
@@ -407,6 +422,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
// (2a) ConfirmAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that ConfirmAsync records the confirming user and emits a Confirmed event persisted to the store.</summary>
|
||||
[Fact]
|
||||
public async Task ConfirmAsync_records_user_and_emits_Confirmed(/* -012 (2) */)
|
||||
{
|
||||
@@ -433,6 +449,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
// (2b) TimedShelveAsync / UnshelveAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that TimedShelveAsync shelves with a deadline and UnshelveAsync removes the shelve before the timer expires.</summary>
|
||||
[Fact]
|
||||
public async Task TimedShelveAsync_and_UnshelveAsync_round_trip(/* -012 (2) */)
|
||||
{
|
||||
@@ -460,6 +477,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
|
||||
// (2c) EnableAsync end-to-end through the engine.
|
||||
/// <summary>Verifies that EnableAsync transitions the alarm back to Enabled state and emits an Enabled event.</summary>
|
||||
[Fact]
|
||||
public async Task EnableAsync_re_enables_after_disable(/* -012 (2) */)
|
||||
{
|
||||
@@ -482,6 +500,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (3) OnEvent subscriber-throws isolation — a bad subscriber must not crash
|
||||
// the engine or prevent subsequent alarm state transitions. The engine logs
|
||||
// the exception and continues operating; any later alarm changes still work.
|
||||
/// <summary>Verifies that an exception thrown by an OnEvent subscriber is isolated and does not crash the engine or prevent further state transitions.</summary>
|
||||
[Fact]
|
||||
public async Task OnEvent_subscriber_exception_does_not_crash_engine(/* -012 (3) */)
|
||||
{
|
||||
@@ -514,6 +533,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (4) IAlarmStateStore.SaveAsync failure — in-memory state must remain at the
|
||||
// prior value after finding -007 fix (persist-before-update).
|
||||
/// <summary>Verifies that a store SaveAsync failure leaves the in-memory alarm state at its prior value (persist-before-update invariant, finding -007).</summary>
|
||||
[Fact]
|
||||
public async Task Store_save_failure_leaves_in_memory_state_unchanged(/* -012 (4) */)
|
||||
{
|
||||
@@ -544,6 +564,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (5) Re-entrant LoadAsync — the old timer must not keep firing after a second
|
||||
// call (regression for finding -002: _shelvingTimer?.Dispose() fix).
|
||||
/// <summary>Verifies that a second LoadAsync call disposes the prior shelving timer so it does not keep firing after reload (regression for finding -002).</summary>
|
||||
[Fact]
|
||||
public async Task Second_LoadAsync_does_not_leak_old_timer(/* -012 (5) */)
|
||||
{
|
||||
@@ -576,6 +597,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// (6) Cold-start AreInputsReady guard — null value, Bad status, and Uncertain
|
||||
// status inputs are all handled correctly.
|
||||
/// <summary>Verifies that AreInputsReady blocks predicate evaluation when inputs have null values or Bad status codes, while Uncertain quality is accepted.</summary>
|
||||
[Fact]
|
||||
public async Task AreInputsReady_blocks_evaluation_for_null_and_bad_inputs(/* -012 (6) */)
|
||||
{
|
||||
@@ -612,6 +634,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (2) A subscriber that re-enters the engine (e.g. AcknowledgeAsync) must
|
||||
// not deadlock against _evalGate. Both regressions are covered here.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that an OnEvent subscriber can call engine methods (e.g. AcknowledgeAsync) without deadlocking against the evaluation gate (regression for Core.ScriptedAlarms-003).</summary>
|
||||
[Fact]
|
||||
public async Task OnEvent_subscriber_can_call_back_into_engine_without_deadlock(/* -003 */)
|
||||
{
|
||||
@@ -663,6 +686,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that OnEvent emission occurs after the evaluation gate is released, so subscribers can re-enter the engine without deadlocking (regression for Core.ScriptedAlarms-003).</summary>
|
||||
[Fact]
|
||||
public void OnEvent_emission_happens_outside_evalGate(/* -003 */)
|
||||
{
|
||||
@@ -723,6 +747,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// or shelving check started just before Dispose can keep running and write
|
||||
// to a (possibly disposed) store after the engine has returned.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that Dispose blocks until in-flight background re-evaluation tasks complete, preventing the engine from outliving its store (regression for Core.ScriptedAlarms-006).</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_drains_in_flight_reevaluation_tasks(/* -006 */)
|
||||
{
|
||||
@@ -768,6 +793,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// resolution renders Uncertain as "{?}" so the operator sees the doubt
|
||||
// explicitly. The two policies are documented in docs/ScriptedAlarms.md.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that Uncertain-quality inputs are accepted by the predicate but rendered as "{?}" in the operator-facing message template (Core.ScriptedAlarms-010).</summary>
|
||||
[Fact]
|
||||
public async Task Uncertain_quality_drives_predicate_but_renders_question_mark_in_message(/* -010 */)
|
||||
{
|
||||
@@ -815,6 +841,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// (which still satisfies IReadOnlyList<AlarmComment> for existing
|
||||
// consumers).
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that the Comments collection is an ImmutableList, enabling O(log n) append and satisfying IReadOnlyList consumers (Core.ScriptedAlarms-008).</summary>
|
||||
[Fact]
|
||||
public async Task Comments_collection_uses_ImmutableList_for_efficient_append(/* -008 */)
|
||||
{
|
||||
@@ -836,6 +863,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
// propagated, not silently discarded. The class-level remarks promise a
|
||||
// diagnostic log line for no-op disabled-alarm evaluations.
|
||||
// -------------------------------------------------------------------------
|
||||
/// <summary>Verifies that TransitionResult.NoOp preserves the supplied diagnostic reason string for caller logging (Core.ScriptedAlarms-011).</summary>
|
||||
[Fact]
|
||||
public void TransitionResult_NoOp_propagates_reason(/* -011 */)
|
||||
{
|
||||
@@ -845,6 +873,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
"NoOp reason must be preserved on the TransitionResult so callers can log it");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TransitionResult.None carries a null reason, distinguishing it from the NoOp factory (Core.ScriptedAlarms-011).</summary>
|
||||
[Fact]
|
||||
public void TransitionResult_None_carries_no_reason(/* -011 */)
|
||||
{
|
||||
@@ -875,20 +904,32 @@ public sealed class ScriptedAlarmEngineTests
|
||||
private sealed class FailOnSaveAlarmStateStore : IAlarmStateStore
|
||||
{
|
||||
private readonly InMemoryAlarmStateStore _inner = new();
|
||||
/// <summary>Gets or sets a value indicating whether the next SaveAsync call should throw a simulated failure.</summary>
|
||||
public bool FailSave { get; set; }
|
||||
|
||||
/// <summary>Loads an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to load.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task<AlarmConditionState?> LoadAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.LoadAsync(alarmId, ct);
|
||||
|
||||
/// <summary>Loads all alarm condition states from the inner store.</summary>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task<IReadOnlyList<AlarmConditionState>> LoadAllAsync(CancellationToken ct)
|
||||
=> _inner.LoadAllAsync(ct);
|
||||
|
||||
/// <summary>Saves an alarm condition state, optionally throwing if FailSave is set.</summary>
|
||||
/// <param name="state">The alarm condition state to save.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task SaveAsync(AlarmConditionState state, CancellationToken ct)
|
||||
{
|
||||
if (FailSave) throw new InvalidOperationException("Simulated store failure");
|
||||
return _inner.SaveAsync(state, ct);
|
||||
}
|
||||
|
||||
/// <summary>Removes an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to remove.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task RemoveAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.RemoveAsync(alarmId, ct);
|
||||
}
|
||||
@@ -900,15 +941,25 @@ public sealed class ScriptedAlarmEngineTests
|
||||
private sealed class BlockingSaveAlarmStateStore : IAlarmStateStore
|
||||
{
|
||||
private readonly InMemoryAlarmStateStore _inner = new();
|
||||
/// <summary>Gets or sets a TaskCompletionSource that, when set, blocks the next SaveAsync until signalled.</summary>
|
||||
public TaskCompletionSource? BlockNextSave { get; set; }
|
||||
/// <summary>Gets a value indicating whether a SaveAsync call is currently blocked waiting on BlockNextSave.</summary>
|
||||
public bool SaveInProgress { get; private set; }
|
||||
|
||||
/// <summary>Loads an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to load.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task<AlarmConditionState?> LoadAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.LoadAsync(alarmId, ct);
|
||||
|
||||
/// <summary>Loads all alarm condition states from the inner store.</summary>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task<IReadOnlyList<AlarmConditionState>> LoadAllAsync(CancellationToken ct)
|
||||
=> _inner.LoadAllAsync(ct);
|
||||
|
||||
/// <summary>Saves an alarm condition state, optionally blocking on BlockNextSave gate.</summary>
|
||||
/// <param name="state">The alarm condition state to save.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public async Task SaveAsync(AlarmConditionState state, CancellationToken ct)
|
||||
{
|
||||
var gate = BlockNextSave;
|
||||
@@ -922,12 +973,16 @@ public sealed class ScriptedAlarmEngineTests
|
||||
await _inner.SaveAsync(state, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Removes an alarm condition state by ID from the inner store.</summary>
|
||||
/// <param name="alarmId">The ID of the alarm condition state to remove.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
public Task RemoveAsync(string alarmId, CancellationToken ct)
|
||||
=> _inner.RemoveAsync(alarmId, ct);
|
||||
}
|
||||
|
||||
// --- Core.ScriptedAlarms-009: per-alarm evaluation-scratch reuse ---
|
||||
|
||||
/// <summary>Verifies that re-evaluations reuse the same read cache dictionary instance instead of allocating a new one.</summary>
|
||||
[Fact]
|
||||
public async Task Reevaluation_reuses_the_same_read_cache_dictionary()
|
||||
{
|
||||
@@ -960,6 +1015,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
scratchAfterPush!["Temp"].Value.ShouldBe(150, "refill must update the existing dictionary in place");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that re-evaluations reuse the same predicate context instance across evaluations.</summary>
|
||||
[Fact]
|
||||
public async Task Reevaluation_reuses_the_same_predicate_context()
|
||||
{
|
||||
@@ -985,6 +1041,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
"the AlarmPredicateContext must be reused across evaluations (Core.ScriptedAlarms-009).");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadAsync clears prior evaluation scratch so new alarms use fresh scratch.</summary>
|
||||
[Fact]
|
||||
public async Task LoadAsync_drops_the_prior_generations_scratch()
|
||||
{
|
||||
@@ -1018,6 +1075,7 @@ public sealed class ScriptedAlarmEngineTests
|
||||
|
||||
// --- Core.Scripting-016: engine routes compiles through CompiledScriptCache ---
|
||||
|
||||
/// <summary>Verifies that Dispose unloads the compiled predicate assembly via the script cache.</summary>
|
||||
[Fact]
|
||||
public void Dispose_unloads_compiled_predicate_assembly()
|
||||
{
|
||||
|
||||
@@ -40,6 +40,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
return (engine, source, up);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribing with an empty filter receives every alarm emission.</summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_with_empty_filter_receives_every_alarm_emission()
|
||||
{
|
||||
@@ -64,6 +65,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribing with an equipment prefix filters alarms by that prefix.</summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_with_equipment_prefix_filters_by_that_prefix()
|
||||
{
|
||||
@@ -86,6 +88,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribing stops further alarm events.</summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_stops_further_events()
|
||||
{
|
||||
@@ -104,6 +107,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
events.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AcknowledgeAsync routes to the engine with a default user.</summary>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_routes_to_engine_with_default_user()
|
||||
{
|
||||
@@ -125,6 +129,7 @@ public sealed class ScriptedAlarmSourceTests
|
||||
state.LastAckComment.ShouldBe("ack via opcua");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that null arguments are rejected.</summary>
|
||||
[Fact]
|
||||
public async Task Null_arguments_rejected()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user