review(Core.Abstractions): document ReadEventsAsync continuation contract (OpcUaServer-002 root)
Re-review at 7286d320. Core.Abstractions-009: ReadEventsAsync maxEvents<=0 sentinel now
documents the implementer's continuation-point obligation when a backend cap truncates
(the root of OpcUaServer-002). -010: PollGroupEngineTests pass CancellationToken. Plus
EquipmentTagRefResolver.TryResolve [MaybeNullWhen(false)] NRT cleanup + test.
This commit is contained in:
@@ -51,4 +51,23 @@ public class EquipmentTagRefResolverTests
|
||||
r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
|
||||
calls.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-009: <see cref="EquipmentTagRefResolver{TDef}.TryResolve"/> carries
|
||||
/// <c>[MaybeNullWhen(false)]</c> on its <c>out</c> parameter so NRT-enabled callers do
|
||||
/// not need a null-forgiving operator when the return value is <see langword="false"/>.
|
||||
/// This test pins the false-path behaviour: <c>def</c> must not be used when the method
|
||||
/// returns <see langword="false"/> — the contract is that it is undefined (currently null).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryResolve_false_path_sets_def_to_default()
|
||||
{
|
||||
var r = Make(new(), _ => null);
|
||||
var returned = r.TryResolve("no-such-ref", out var def);
|
||||
returned.ShouldBeFalse();
|
||||
// The [MaybeNullWhen(false)] attribute means callers must treat def as potentially null
|
||||
// on the false path — the value here is the implementation-defined default (null for
|
||||
// a reference type), which satisfies that contract.
|
||||
def.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -169,6 +169,33 @@ public sealed class IHistorianDataSourceContractTests
|
||||
parameter!.ParameterType.ShouldBe(expectedType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-009 (OpcUaServer-002): when <c>maxEvents <= 0</c> the contract
|
||||
/// requires implementations to set <see cref="HistoricalEventsResult.ContinuationPoint"/>
|
||||
/// non-null when more results exist. Pins the <c>maxEvents</c> parameter type and the
|
||||
/// <see cref="HistoricalEventsResult"/> continuation-point member so accidental removal
|
||||
/// breaks this test — the downstream paging contract depends on both.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ReadEventsAsync_sentinel_and_continuation_contract_types_pinned()
|
||||
{
|
||||
var method = typeof(IHistorianDataSource).GetMethod("ReadEventsAsync");
|
||||
method.ShouldNotBeNull();
|
||||
|
||||
var maxEventsParam = method!.GetParameters().FirstOrDefault(p => p.Name == "maxEvents");
|
||||
maxEventsParam.ShouldNotBeNull("maxEvents parameter must exist for the sentinel contract");
|
||||
maxEventsParam!.ParameterType.ShouldBe(typeof(int),
|
||||
"maxEvents is int (not uint) so callers can pass <=0 as a 'use backend default cap' sentinel");
|
||||
|
||||
// HistoricalEventsResult must carry a nullable ContinuationPoint so implementations
|
||||
// can signal 'more results exist' (non-null) vs 'all results returned' (null).
|
||||
var resultType = typeof(HistoricalEventsResult);
|
||||
var continuationProp = resultType.GetProperty("ContinuationPoint");
|
||||
continuationProp.ShouldNotBeNull("HistoricalEventsResult.ContinuationPoint must exist for paging");
|
||||
continuationProp!.PropertyType.ShouldBe(typeof(byte[]),
|
||||
"ContinuationPoint is byte[]? — null means no more pages, non-null means caller must page");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-008: <see cref="IHistoryProvider.ReadAtTimeAsync"/> and
|
||||
/// <see cref="IHistoryProvider.ReadEventsAsync"/> are C# default interface methods
|
||||
|
||||
@@ -65,7 +65,7 @@ public sealed class PollGroupEngineTests
|
||||
(h, r, s) => events.Enqueue((h, r, s)));
|
||||
|
||||
var handle = engine.Subscribe(["X"], TimeSpan.FromMilliseconds(100));
|
||||
await Task.Delay(500);
|
||||
await Task.Delay(500, TestContext.Current.CancellationToken);
|
||||
engine.Unsubscribe(handle);
|
||||
|
||||
events.Count.ShouldBe(1);
|
||||
@@ -108,7 +108,7 @@ public sealed class PollGroupEngineTests
|
||||
var afterUnsub = events.Count;
|
||||
|
||||
src.Values["X"] = 999;
|
||||
await Task.Delay(400);
|
||||
await Task.Delay(400, TestContext.Current.CancellationToken);
|
||||
events.Count.ShouldBe(afterUnsub);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public sealed class PollGroupEngineTests
|
||||
minInterval: TimeSpan.FromMilliseconds(200));
|
||||
|
||||
var handle = engine.Subscribe(["X"], TimeSpan.FromMilliseconds(5));
|
||||
await Task.Delay(300);
|
||||
await Task.Delay(300, TestContext.Current.CancellationToken);
|
||||
engine.Unsubscribe(handle);
|
||||
|
||||
// 300 ms window, 200 ms floor, stable value → initial push + at most 1 extra poll.
|
||||
@@ -243,7 +243,7 @@ public sealed class PollGroupEngineTests
|
||||
engine.ActiveSubscriptionCount.ShouldBe(0);
|
||||
|
||||
var afterDispose = events.Count;
|
||||
await Task.Delay(300);
|
||||
await Task.Delay(300, TestContext.Current.CancellationToken);
|
||||
// After dispose no more events — everything is cancelled.
|
||||
events.Count.ShouldBe(afterDispose);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ public sealed class PollGroupEngineTests
|
||||
|
||||
var handle = engine.Subscribe(["A"], TimeSpan.FromMilliseconds(50));
|
||||
// Allow several poll cycles so a broken implementation would accumulate extra events.
|
||||
await Task.Delay(400);
|
||||
await Task.Delay(400, TestContext.Current.CancellationToken);
|
||||
engine.Unsubscribe(handle);
|
||||
|
||||
// Only the initial-data push should have fired; subsequent polls with identical
|
||||
|
||||
Reference in New Issue
Block a user