db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
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
125 lines
4.7 KiB
C#
125 lines
4.7 KiB
C#
// 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;
|
|
}
|
|
}
|