refactor(link): bound every teardown and handler wait behind one helper [CI]
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

This commit is contained in:
justme committed 2026-10-19 10:07:36 +08:00
1 parent c05842fd91
commit d95acc6575
10 files changed
+130 -31

No files matched your search

@@ -0,0 +1,28 @@
using Fleet.Core;
namespace Fleet.Tests;
public sealed class BoundedTests
{
[Fact]
public async Task WaitAsync_ReturnsFalse_WhenTheWorkNeverFinishes()
{
var never = new TaskCompletionSource();
Assert.False(await Bounded.WaitAsync(never.Task, TimeSpan.FromMilliseconds(100), TimeProvider.System));
}
[Fact]
public async Task WaitAsync_ReturnsTrue_WhenTheWorkFinishes_OrFailsWithSessionNoise()
{
Assert.True(await Bounded.WaitAsync(Task.CompletedTask, TimeSpan.FromSeconds(5), TimeProvider.System));
Assert.True(await Bounded.WaitAsync(Task.FromException(new IOException("closed")), TimeSpan.FromSeconds(5), TimeProvider.System));
Assert.True(await Bounded.WaitAsync(Task.FromException(new ObjectDisposedException("x")), TimeSpan.FromSeconds(5), TimeProvider.System));
Assert.True(await Bounded.WaitAsync(Task.FromCanceled(new CancellationToken(true)), TimeSpan.FromSeconds(5), TimeProvider.System));
}
[Fact]
public async Task WaitAsync_DoesNotHideRealFailures()
{
await Assert.ThrowsAsync<FormatException>(() => Bounded.WaitAsync(Task.FromException(new FormatException()), TimeSpan.FromSeconds(5), TimeProvider.System));
}
}
@@ -275,6 +275,27 @@ public sealed class LinkTests
await run.WaitAsync(TimeSpan.FromSeconds(5));
}
[Fact]
public async Task Session_RunEnds_EvenWhenAnInboundHandlerIsStuck()
{
(TlsFrameTransport left, TlsFrameTransport right) = await PairAsync();
MethodTable table = TestTables.Of(TestTables.Req("right.stuck"));
var stuck = new TaskCompletionSource<Result<Number>>(TaskCreationOptions.RunContinuationsAsynchronously); // 不理会取消令牌:模拟卡在原生层的处理器
var rightRouter = new RpcRouter<string>(RpcSide.Listener, table).On("right.stuck", LinkJson.Default.Number, LinkJson.Default.Number, (_, _, _) => new ValueTask<Result<Number>>(stuck.Task));
await using var leftSession = new RpcSession<string>(left, new RpcRouter<string>(RpcSide.Dialer, table), "left", TimeProvider.System);
await using var rightSession = new RpcSession<string>(right, rightRouter, "right", TimeProvider.System);
Task leftRun = leftSession.RunAsync(default);
Task rightRun = rightSession.RunAsync(default);
_ = leftSession.RequestAsync("right.stuck", new Number { Value = 1 }, LinkJson.Default.Number, LinkJson.Default.Number, default, TimeSpan.FromSeconds(30)).AsTask();
await Task.Delay(300); // 请求到达并进入处理器
await leftSession.CloseAsync("bye", default);
await rightRun.WaitAsync(TimeSpan.FromSeconds(10)); // 以前这里会一直等到处理器返回
stuck.TrySetResult(Result<Number>.Ok(new Number { Value = 0 }));
await leftRun.WaitAsync(TimeSpan.FromSeconds(5));
}
// ───────────────────────── 点对点握手 ─────────────────────────
private sealed class PeerFixture : IAsyncDisposable