chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
public sealed class InputWaiterTests : IDisposable
|
||||
{
|
||||
private readonly InputWaiter _waiter = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._waiter.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_CompletesAfterSignalAsync()
|
||||
{
|
||||
this._waiter.SignalInput();
|
||||
|
||||
// WaitForInputAsync should complete immediately since input was already signaled
|
||||
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
|
||||
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
|
||||
|
||||
completed.Should().BeSameAs(waitTask, "the wait task should complete before the timeout");
|
||||
await waitTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_BlocksUntilSignaledAsync()
|
||||
{
|
||||
// Use the no-timeout overload so that the wait can only be released by SignalInput.
|
||||
// A finite timeout would make this test's logic racy: the component correctly
|
||||
// honors the timeout, but if the test thread is starved of CPU time (CI load,
|
||||
// GC pause) long enough for the timeout to fire, waitTask completes before
|
||||
// SignalInput is called and the "should not complete before signaled" assertion
|
||||
// flakes. Timeout behavior is covered separately below.
|
||||
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
|
||||
|
||||
Task completedBeforeSignal = await Task.WhenAny(waitTask, Task.Delay(100));
|
||||
completedBeforeSignal.Should().NotBeSameAs(
|
||||
waitTask,
|
||||
"the waiter should not complete before input is signaled");
|
||||
|
||||
this._waiter.SignalInput();
|
||||
|
||||
Task completedAfterSignal = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
|
||||
completedAfterSignal.Should().BeSameAs(
|
||||
waitTask,
|
||||
"the wait task should complete after being signaled");
|
||||
|
||||
await waitTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputWaiter_SignalInput_DoubleSignalDoesNotThrow()
|
||||
{
|
||||
// Binary semaphore behavior: double signal should be idempotent
|
||||
FluentActions.Invoking(() =>
|
||||
{
|
||||
this._waiter.SignalInput();
|
||||
this._waiter.SignalInput();
|
||||
}).Should().NotThrow("double signaling should be handled gracefully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_RespectsCancellationAsync()
|
||||
{
|
||||
using CancellationTokenSource cts = new();
|
||||
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
Func<Task> act = () => waitTask;
|
||||
await act.Should().ThrowAsync<OperationCanceledException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_DoesNotCompleteWhenNotSignaledAsync()
|
||||
{
|
||||
using CancellationTokenSource cts = new();
|
||||
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
|
||||
Task completed = await Task.WhenAny(waitTask, Task.Delay(100));
|
||||
|
||||
completed.Should().NotBeSameAs(waitTask, "the wait task should not complete when input is not signaled");
|
||||
|
||||
// Cancel and observe the pending task to avoid an unobserved exception on Dispose
|
||||
cts.Cancel();
|
||||
try { await waitTask; }
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_CanBeSignaledMultipleTimesSequentiallyAsync()
|
||||
{
|
||||
// First signal/wait cycle
|
||||
this._waiter.SignalInput();
|
||||
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
|
||||
|
||||
// Second signal/wait cycle
|
||||
this._waiter.SignalInput();
|
||||
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsync()
|
||||
{
|
||||
// Verify that a finite timeout releases the block even without a signal.
|
||||
// We only assert that it *does* complete (within a generous outer bound);
|
||||
// we intentionally do not assert that it stays blocked until the timeout,
|
||||
// because that would re-introduce the same wall-clock flakiness
|
||||
// described in BlocksUntilSignaledAsync (see comment on that test).
|
||||
Task waitTask = this._waiter.WaitForInputAsync(TimeSpan.FromMilliseconds(300));
|
||||
|
||||
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(5)));
|
||||
completed.Should().BeSameAs(waitTask, "the wait task should complete once the timeout expires");
|
||||
await waitTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user