fix(client/dotnet): restore warnings-as-errors floor; license metadata; LazyBrowseNode publication (Client.Dotnet-022..025)
This commit is contained in:
@@ -135,25 +135,35 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
/// <summary>Queue of exceptions to throw from BrowseChildren; dequeued in FIFO order.</summary>
|
||||
public Queue<Exception> BrowseChildrenExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional hook awaited inside BrowseChildren before the reply is produced. Lets a
|
||||
/// test hold an RPC mid-flight to exercise concurrent reads of the in-progress node.
|
||||
/// </summary>
|
||||
public Func<Task>? BrowseChildrenGate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The BrowseChildrenRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<BrowseChildrenReply> BrowseChildrenAsync(
|
||||
public async Task<BrowseChildrenReply> BrowseChildrenAsync(
|
||||
BrowseChildrenRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
BrowseChildrenCalls.Add((request, callOptions));
|
||||
if (BrowseChildrenExceptions.TryDequeue(out Exception? exception))
|
||||
{
|
||||
return Task.FromException<BrowseChildrenReply>(exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return Task.FromResult(
|
||||
BrowseChildrenReplies.TryDequeue(out BrowseChildrenReply? reply)
|
||||
? reply
|
||||
: BrowseChildrenReply);
|
||||
if (BrowseChildrenGate is { } gate)
|
||||
{
|
||||
await gate().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return BrowseChildrenReplies.TryDequeue(out BrowseChildrenReply? reply)
|
||||
? reply
|
||||
: BrowseChildrenReply;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -175,6 +175,96 @@ public sealed class LazyBrowseNodeTests
|
||||
Assert.Equal(2, transport.BrowseChildrenCalls.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that reading Children/IsExpanded concurrently with an in-flight ExpandAsync
|
||||
/// never throws (no torn enumeration of a mid-append list) and, once IsExpanded flips to
|
||||
/// true, the published Children snapshot is fully populated. Pins the safe-publication
|
||||
/// contract on the lock-free readers (Client.Dotnet-025).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Expand_ConcurrentReadOfChildren_NeverTearsAndPublishesAtomically()
|
||||
{
|
||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
||||
children: [BuildObject(1, "Plant", isArea: true)],
|
||||
childHasChildren: [true],
|
||||
cacheSequence: 1));
|
||||
|
||||
// Multi-page child set so the expand loop spends meaningful time appending,
|
||||
// widening the window for a concurrent reader to observe a torn list.
|
||||
BrowseChildrenReply childPage1 = BuildReply(
|
||||
children: [BuildObject(10, "A"), BuildObject(11, "B"), BuildObject(12, "C")],
|
||||
childHasChildren: [false, false, false],
|
||||
cacheSequence: 1);
|
||||
childPage1.NextPageToken = "1:p:3";
|
||||
transport.BrowseChildrenReplies.Enqueue(childPage1);
|
||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
||||
children: [BuildObject(13, "D"), BuildObject(14, "E")],
|
||||
childHasChildren: [false, false],
|
||||
cacheSequence: 1));
|
||||
|
||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
||||
LazyBrowseNode node = roots[0];
|
||||
|
||||
// Gate the child-page RPCs so the expand stays mid-flight while the reader spins.
|
||||
using SemaphoreSlim release = new(0, 1);
|
||||
bool firstChildCall = true;
|
||||
transport.BrowseChildrenGate = async () =>
|
||||
{
|
||||
if (firstChildCall)
|
||||
{
|
||||
firstChildCall = false;
|
||||
await release.WaitAsync().ConfigureAwait(false);
|
||||
}
|
||||
};
|
||||
|
||||
using CancellationTokenSource readerStop = new();
|
||||
Exception? readerFailure = null;
|
||||
Task reader = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!readerStop.IsCancellationRequested)
|
||||
{
|
||||
bool expanded = node.IsExpanded;
|
||||
|
||||
// Enumerate the snapshot; a torn/mid-append list would throw here.
|
||||
int count = 0;
|
||||
foreach (LazyBrowseNode _ in node.Children)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
// If the node reports expanded, the published snapshot must be complete.
|
||||
if (expanded)
|
||||
{
|
||||
Assert.Equal(5, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
readerFailure = ex;
|
||||
}
|
||||
});
|
||||
|
||||
Task expand = node.ExpandAsync();
|
||||
// Let the reader spin against the empty pre-publication snapshot for a moment.
|
||||
await Task.Delay(50);
|
||||
release.Release();
|
||||
await expand;
|
||||
|
||||
// Let the reader observe the post-publication state, then stop it.
|
||||
await Task.Delay(50);
|
||||
readerStop.Cancel();
|
||||
await reader;
|
||||
|
||||
Assert.Null(readerFailure);
|
||||
Assert.True(node.IsExpanded);
|
||||
Assert.Equal(5, node.Children.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user