fix(abcip): Task 13 review r1 - bounded listing pager, exclusive large reads, template bound, safe lease release
This commit is contained in:
@@ -135,4 +135,78 @@ public sealed class NativeAbCipTagEnumeratorTests
|
||||
|
||||
bytes.ShouldBe(Concat(Entries(ControllerEntries)));
|
||||
}
|
||||
|
||||
private static FakeCipChannel RepeatingPartialPage(uint lastInstance)
|
||||
{
|
||||
var ch = new FakeCipChannel();
|
||||
// One entry (instance = lastInstance, name "A") replied with "more" every time.
|
||||
var entry = new byte[22 + 1];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(entry, lastInstance);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(entry.AsSpan(20), 1);
|
||||
entry[22] = (byte)'A';
|
||||
ch.RespondPerService = _ => FakeCipChannel.Reply(0x55, 0x06, entry);
|
||||
return ch;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_partial_page_that_never_advances_fails_fast_instead_of_looping()
|
||||
{
|
||||
// Instance 5 is repeated forever: page 2 starts at 6 but answers last = 5 (< start).
|
||||
var ch = RepeatingPartialPage(5);
|
||||
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(
|
||||
() => NativeListingReader.ReadAsync(sched, "@tags", cts.Token));
|
||||
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
ch.SentCount.ShouldBeLessThan(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_partial_page_ending_at_the_maximum_instance_is_rejected_rather_than_wrapping()
|
||||
{
|
||||
var ch = RepeatingPartialPage(uint.MaxValue);
|
||||
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(
|
||||
() => NativeListingReader.ReadAsync(sched, "@tags", cts.Token));
|
||||
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
ch.SentCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancelling_mid_enumeration_releases_the_lease()
|
||||
{
|
||||
var ch = Serve(new() { [""] = Entries(ControllerEntries) });
|
||||
ch.Gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
||||
var released = 0;
|
||||
using var enumerator = new NativeAbCipTagEnumerator(_ => (sched, () => { released++; return ValueTask.CompletedTask; }));
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
|
||||
|
||||
var run = Task.Run(async () =>
|
||||
{
|
||||
await foreach (var _ in enumerator.EnumerateAsync(Params, cts.Token)) { }
|
||||
}, TestContext.Current.CancellationToken);
|
||||
await ch.WaitForSendsAsync(1);
|
||||
cts.Cancel();
|
||||
|
||||
await Should.ThrowAsync<OperationCanceledException>(() => run);
|
||||
released.ShouldBe(1);
|
||||
ch.Gate.TrySetResult();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_throwing_release_delegate_never_escapes_ReleaseBounded()
|
||||
{
|
||||
NativeLease.ReleaseBounded(() => throw new InvalidOperationException("boom"),
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
|
||||
NativeLease.ReleaseBounded(() => ValueTask.FromException(new ObjectDisposedException("x")),
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,4 +128,50 @@ public sealed class NativeAbCipTemplateReaderTests
|
||||
reader.ReadAsync(Params, 0x0FCE, TestContext.Current.CancellationToken));
|
||||
released.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_template_read_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;
|
||||
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 read;
|
||||
|
||||
// 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.ShouldContain(r => r[0] == 0x4C); // the Read Template requests went out bare
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_template_size_over_the_bound_is_rejected()
|
||||
{
|
||||
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.StringTemplate);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(captured.AsSpan(2), 0x10000000);
|
||||
var ch = ServeTemplate(captured, 504);
|
||||
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
||||
using var reader = new NativeAbCipTemplateReader(_ => (sched, ch, () => ValueTask.CompletedTask));
|
||||
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(
|
||||
() => reader.ReadAsync(Params, 0x0FCE, TestContext.Current.CancellationToken));
|
||||
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
ch.Sent.ShouldNotContain(r => r[0] == 0x4C);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user