test(abcip): Task 13 review r2 - non-vacuous exclusive-packing tests
Scheduler tests (exclusive mid-queue and at head) and a template-reader test that genuinely queues a Read Template behind a small read; bounded QueuedCount wait replaces the fixed delay. Falsified: removing exclusive from the reader turns the reader test red; removing the Exclusive handling in RequestScheduler turns all three red.
This commit is contained in:
+47
@@ -390,4 +390,51 @@ public sealed class RequestSchedulerTests
|
||||
ch.SentCount.ShouldBe(1);
|
||||
Should.Throw<ObjectDisposedException>(() => s.SendAsync(Req(8), CancellationToken.None));
|
||||
}
|
||||
|
||||
private static bool IsMsp(byte[] r) => r[0] == 0x0A;
|
||||
|
||||
[Fact]
|
||||
public async Task An_exclusive_request_goes_out_bare_and_splits_the_packing_around_it_in_fifo_order()
|
||||
{
|
||||
var ch = new FakeCipChannel();
|
||||
await using var s = new RequestScheduler(ch, allowPacking: true);
|
||||
var a = await HoldFirstAsync(s, ch, Req(8, 1));
|
||||
|
||||
var b1 = s.SendAsync(Req(8, 2), Ct);
|
||||
var b2 = s.SendAsync(Req(8, 3), Ct);
|
||||
var c = s.SendAsync(Req(8, 4), Ct, exclusive: true);
|
||||
var d1 = s.SendAsync(Req(8, 5), Ct);
|
||||
var d2 = s.SendAsync(Req(8, 6), Ct);
|
||||
s.QueuedCount.ShouldBe(5);
|
||||
ch.Gate!.SetResult();
|
||||
await Task.WhenAll(a, b1, b2, c, d1, d2).WaitAsync(Timeout, Ct);
|
||||
|
||||
// A, then [B1 B2] packed, C bare, then [D1 D2] packed: the exclusive request is a batch boundary.
|
||||
ch.Sent.Count.ShouldBe(4);
|
||||
IsMsp(ch.Sent[1]).ShouldBeTrue();
|
||||
FakeCipChannel.SplitMsp(ch.Sent[1]).Select(e => e[^1]).ShouldBe(new byte[] { 2, 3 });
|
||||
IsMsp(ch.Sent[2]).ShouldBeFalse();
|
||||
ch.Sent[2][^1].ShouldBe((byte)4);
|
||||
IsMsp(ch.Sent[3]).ShouldBeTrue();
|
||||
FakeCipChannel.SplitMsp(ch.Sent[3]).Select(e => e[^1]).ShouldBe(new byte[] { 5, 6 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_exclusive_request_at_the_head_of_the_queue_does_not_take_followers_with_it()
|
||||
{
|
||||
var ch = new FakeCipChannel();
|
||||
await using var s = new RequestScheduler(ch, allowPacking: true);
|
||||
var a = await HoldFirstAsync(s, ch, Req(8, 1));
|
||||
|
||||
var c = s.SendAsync(Req(8, 2), Ct, exclusive: true);
|
||||
var d1 = s.SendAsync(Req(8, 3), Ct);
|
||||
var d2 = s.SendAsync(Req(8, 4), Ct);
|
||||
ch.Gate!.SetResult();
|
||||
await Task.WhenAll(a, c, d1, d2).WaitAsync(Timeout, Ct);
|
||||
|
||||
ch.Sent.Count.ShouldBe(3);
|
||||
IsMsp(ch.Sent[1]).ShouldBeFalse();
|
||||
ch.Sent[1][^1].ShouldBe((byte)2);
|
||||
FakeCipChannel.SplitMsp(ch.Sent[2]).Select(e => e[^1]).ShouldBe(new byte[] { 3, 4 });
|
||||
}
|
||||
}
|
||||
|
||||
+40
-20
@@ -21,6 +21,13 @@ public sealed class NativeAbCipTemplateReaderTests
|
||||
|
||||
/// <summary>Builds a channel serving <paramref name="captured"/> (header + raw) the way a controller would.</summary>
|
||||
private static FakeCipChannel ServeTemplate(byte[] captured, int maxRequestBytes, int? statusOverrideAtFirst = null)
|
||||
{
|
||||
var ch = new FakeCipChannel(maxRequestBytes);
|
||||
ch.RespondPerService = ServeTemplatePerService(captured, maxRequestBytes);
|
||||
return ch;
|
||||
}
|
||||
|
||||
private static Func<byte[], byte[]> ServeTemplatePerService(byte[] captured, int maxRequestBytes)
|
||||
{
|
||||
var id = BinaryPrimitives.ReadUInt16LittleEndian(captured);
|
||||
var defWords = BinaryPrimitives.ReadUInt32LittleEndian(captured.AsSpan(2));
|
||||
@@ -29,9 +36,7 @@ public sealed class NativeAbCipTemplateReaderTests
|
||||
var handle = BinaryPrimitives.ReadUInt16LittleEndian(captured.AsSpan(12));
|
||||
var raw = captured.AsSpan(14).ToArray();
|
||||
var total = (int)(defWords * 4 - 23);
|
||||
|
||||
var ch = new FakeCipChannel(maxRequestBytes);
|
||||
ch.RespondPerService = req =>
|
||||
return req =>
|
||||
{
|
||||
if (req[0] == 0x03)
|
||||
{
|
||||
@@ -56,7 +61,6 @@ public sealed class NativeAbCipTemplateReaderTests
|
||||
var done = offset + n >= total;
|
||||
return FakeCipChannel.Reply(0x4C, done ? (byte)0x00 : (byte)0x06, raw[offset..(offset + n)]);
|
||||
};
|
||||
return ch;
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ReadNativeAsync(FakeCipChannel ch, ushort id)
|
||||
@@ -130,32 +134,48 @@ public sealed class NativeAbCipTemplateReaderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_template_read_queued_behind_a_small_read_on_a_packing_scheduler_goes_out_alone()
|
||||
public async Task A_read_template_queued_behind_a_small_read_on_a_packing_scheduler_goes_out_alone()
|
||||
{
|
||||
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.LookupMemberTemplate);
|
||||
var ch = ServeTemplate(captured, 504);
|
||||
ch.Gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var served = ch.RespondPerService;
|
||||
ch.RespondPerService = r => r[0] == 0x0E ? FakeCipChannel.OkReply(r) : served(r);
|
||||
await using var sched = new RequestScheduler(ch, allowPacking: true);
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var small = new byte[] { 0x0E, 0x02, 0x20, 0x01, 0x24, 0x01 }; // Get Attribute Single
|
||||
var hold = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var inner = ch.RespondPerService;
|
||||
var firstSmall = true;
|
||||
Task<CipReply>? s1 = null;
|
||||
ch.Respond = async req =>
|
||||
{
|
||||
// The Get Attributes reply is delivered with a small read already queued behind it, so the
|
||||
// pump takes that read next (S1) and holds it in flight while the reader submits its first
|
||||
// Read Template: that template is genuinely queued behind another request.
|
||||
if (req[0] == 0x03)
|
||||
{
|
||||
s1 = sched.SendAsync(small, ct);
|
||||
return await ch.DefaultRespond(req);
|
||||
}
|
||||
if (req[0] == 0x0E && firstSmall)
|
||||
{
|
||||
firstSmall = false;
|
||||
await hold.Task;
|
||||
}
|
||||
return await ch.DefaultRespond(req);
|
||||
};
|
||||
ch.RespondPerService = r => r[0] == 0x0E ? FakeCipChannel.OkReply(r) : inner(r);
|
||||
using var reader = new NativeAbCipTemplateReader(_ => (sched, ch, () => ValueTask.CompletedTask));
|
||||
|
||||
// A is in flight (gated); B queues behind it; the template read then queues behind B.
|
||||
var small = new byte[] { 0x0E, 0x02, 0x20, 0x01, 0x24, 0x01 }; // Get Attribute Single
|
||||
var a = sched.SendAsync(small, ct);
|
||||
await ch.WaitForSendsAsync(1);
|
||||
var b = sched.SendAsync(small, ct);
|
||||
var read = reader.ReadAsync(Params, 3386, ct);
|
||||
await Task.Delay(200, ct);
|
||||
ch.Gate.SetResult();
|
||||
await Task.WhenAll(a, b);
|
||||
await ch.WaitForSendsAsync(2); // Get Attributes, then S1 held in flight
|
||||
var s2 = sched.SendAsync(small, ct);
|
||||
using var bound = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
bound.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
while (sched.QueuedCount < 2) await Task.Delay(1, bound.Token); // S2 + the first Read Template
|
||||
hold.SetResult();
|
||||
await read;
|
||||
await Task.WhenAll(s1!, s2);
|
||||
|
||||
// Packing was live (the attribute query shared an MSP with B), yet no Read Template rode in one.
|
||||
ch.Sent.ShouldContain(r => r[0] == 0x0A);
|
||||
foreach (var msp in ch.Sent.Where(r => r[0] == 0x0A))
|
||||
FakeCipChannel.SplitMsp(msp).ShouldNotContain(e => e[0] == 0x4C);
|
||||
ch.Sent.Where(r => r[0] == 0x0A).ShouldAllBe(msp => FakeCipChannel.SplitMsp(msp).All(e => e[0] != 0x4C));
|
||||
ch.Sent.ShouldContain(r => r[0] == 0x4C); // the Read Template requests went out bare
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user