Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ public async Task IndependentChildren_DrainBeforeOwnedCleanup(bool explicitLifet
}

[Test]
public async Task SubmissionFailure_IndependentCompletionStillDrains()
public async Task ReturnedSubmissionFailure_IndependentCompletionStillDrains()
{
var events = new ConcurrentQueue<string>();
await using var lifetime = new TaskSchedulerMiddlewareLifetime();
var outer = new IndependentCompletion(events);
var inner = new GatedCompletion(events);
var scheduler = new FailingTerminal().UseMiddleware(new Factory(outer), lifetime)
var scheduler = new FaultedTerminal().UseMiddleware(new Factory(outer), lifetime)
.UseMiddleware(new Factory(inner));
var operation = scheduler.Enqueue(() => 42);
try
Expand Down Expand Up @@ -250,10 +250,10 @@ public async Task DisposalRacingChildCompletion_CleansUpOnceAfterChild()
}
}

private sealed class FailingTerminal : ITaskScheduler
private sealed class FaultedTerminal : ITaskScheduler
{
public Task<T> Enqueue<T>(Func<object?, CancellationToken, ValueTask<T>> taskFunc, object? state, CancellationToken cancellationToken)
=> throw new FormatException("submission");
=> Task.FromException<T>(new FormatException("submission"));
}

private sealed class Factory(ITaskSchedulerMiddleware middleware) : ITaskSchedulerMiddlewareFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ public async Task AnnotationOnlySnapshot_DrainsAcceptedWorkAndRejectsClosedEntry
var shutdown = flow.DisposeAsync().AsTask();
Assert.That(shutdown.IsCompleted, Is.False);
Task<int> rejected = null!;
Assert.That(() => { rejected = scheduler.Enqueue(() => 0); }, Throws.Nothing);
await Assert.ThatAsync(async () => await rejected, Throws.TypeOf<ObjectDisposedException>());
Assert.That(() => { rejected = scheduler.Enqueue(() => 0); }, Throws.TypeOf<ObjectDisposedException>());
Assert.That(rejected, Is.Null, "Closed entry must reject the enqueue call before returning a task.");
release.SetResult(true);
Assert.That(await pending, Is.EqualTo(42));
await shutdown;
Expand Down Expand Up @@ -223,7 +223,8 @@ public async Task InlineTerminals_PreserveContinuationResultsAndCancellation(boo
{
await Assert.ThatAsync(async () => await task, Throws.Exception.SameAs(failure));
Assert.That(task.IsCanceled, Is.EqualTo(failure is OperationCanceledException));
Assert.That(observer.Status, Is.EqualTo(failure is OperationCanceledException ? TaskStatus.Canceled : TaskStatus.Faulted));
// A synchronous next rejection returns no continuation task for this observer to inspect.
Assert.That(observer.Status, Is.EqualTo(!faultedTask ? TaskStatus.Created : failure is OperationCanceledException ? TaskStatus.Canceled : TaskStatus.Faulted));
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions TaskFlow.Tests/TaskFlowSchedulerAdapterFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace TaskFlow.Tests;

using NUnit.Framework;
using System.Threading.Tasks.Flow;

[TestFixture]
internal sealed class TaskFlowSchedulerAdapterFixture
{
[TestCase(false)]
[TestCase(true)]
public async Task Enqueue_QueueTaskFailure_ShouldPreserveStartNewException(bool disposed)
{
Exception failure = disposed ? new ObjectDisposedException("scheduler") : new InvalidOperationException("rejected");
var scheduler = new TaskFlowSchedulerAdapter(new RejectingScheduler(failure));
Task<int>? pending = null;

Assert.That(() => { pending = scheduler.Enqueue((_, _) => new ValueTask<int>(42), null, CancellationToken.None); }, Throws.Nothing);
await Assert.ThatAsync(async () => await pending!,
Throws.TypeOf<TaskSchedulerException>().With.Property(nameof(Exception.InnerException)).SameAs(failure));
}

[Test]
public async Task Enqueue_NullDelegate_ShouldRetainTaskBasedValidation()
{
var scheduler = new TaskFlowSchedulerAdapter(TaskScheduler.Default);
Task<int>? pending = null;
Assert.That(() => { pending = scheduler.Enqueue<int>(null!, null, CancellationToken.None); }, Throws.Nothing);
await Assert.ThatAsync(async () => await pending!, Throws.TypeOf<ArgumentNullException>());
}

private sealed class RejectingScheduler(Exception failure) : TaskScheduler
{
protected override void QueueTask(Task task) => throw failure;
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
protected override IEnumerable<Task> GetScheduledTasks() => [];
}
}
153 changes: 153 additions & 0 deletions TaskFlow.Tests/TaskSchedulerDisposalConformance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
namespace TaskFlow.Tests;

using NUnit.Framework;
using System.Threading.Tasks.Flow;

internal static class TaskSchedulerDisposalConformance
{
public static readonly string[] Overloads =
[
"DirectGeneric", "Action", "ActionWithToken", "TokenAction", "TokenActionWithToken",
"Task", "TokenTask", "TokenTaskWithToken", "StateTask",
"ValueTask", "TokenValueTask", "TokenValueTaskWithToken", "StateValueTask",
"TaskResult", "TokenTaskResult", "TokenTaskResultWithToken", "StateTaskResult",
"ValueTaskResult", "TokenValueTaskResult", "TokenValueTaskResultWithToken", "StateValueTaskResult",
"Func", "TokenFunc",
];

public static void AssertRejected(ITaskScheduler scheduler, string overload, bool keyed)
{
var invoked = false;
Task? returnedTask = null;
Assert.That(() => { returnedTask = Enqueue(scheduler, overload, () => invoked = true, keyed); }, Throws.TypeOf<ObjectDisposedException>());
Assert.That(returnedTask, Is.Null, "A disposed scheduler must reject the call before returning a task.");
Assert.That(invoked, Is.False, "A rejected delegate must never execute.");
}

public static Task Enqueue(ITaskScheduler scheduler, string overload, Action action, bool keyed)
{
Task TaskOperation()
{ action(); return Task.CompletedTask; }
ValueTask ValueTaskOperation()
{ action(); return ValueTask.CompletedTask; }
if (keyed)
{
return overload switch
{
"DirectGeneric" => scheduler.Enqueue<string, int, int>("key", (key, state, token) => { action(); return new ValueTask<int>(42); }, 42, CancellationToken.None),
"Action" => scheduler.Enqueue<string>("key", action),
"ActionWithToken" => scheduler.Enqueue<string>("key", action, CancellationToken.None),
"TokenAction" => scheduler.Enqueue<string>("key", (string key) => { action(); }),
"TokenActionWithToken" => scheduler.Enqueue<string>("key", (string key) => { action(); }, CancellationToken.None),
"Task" => scheduler.Enqueue<string>("key", TaskOperation),
"TokenTask" => scheduler.Enqueue<string>("key", (CancellationToken token) => TaskOperation()),
"TokenTaskWithToken" => scheduler.Enqueue<string>("key", (CancellationToken token) => TaskOperation(), CancellationToken.None),
"StateTask" => scheduler.Enqueue<string, int, int>("key", (key, state, token) => { action(); return Task.FromResult(state); }, 42, CancellationToken.None),
"ValueTask" => scheduler.Enqueue<string>("key", ValueTaskOperation),
"TokenValueTask" => scheduler.Enqueue<string>("key", (CancellationToken token) => ValueTaskOperation()),
"TokenValueTaskWithToken" => scheduler.Enqueue<string>("key", (CancellationToken token) => ValueTaskOperation(), CancellationToken.None),
"StateValueTask" => scheduler.Enqueue<string, int, int>("key", (key, state, token) => { action(); return new ValueTask<int>(state); }, 42, CancellationToken.None),
"TaskResult" => scheduler.Enqueue<string, int>("key", () => { action(); return Task.FromResult(42); }),
"TokenTaskResult" => scheduler.Enqueue<string, int>("key", (CancellationToken token) => { action(); return Task.FromResult(42); }),
"TokenTaskResultWithToken" => scheduler.Enqueue<string, int>("key", (CancellationToken token) => { action(); return Task.FromResult(42); }, CancellationToken.None),
"StateTaskResult" => scheduler.Enqueue<string, int, int>("key", (key, state, token) => { action(); return Task.FromResult(state); }, 42, CancellationToken.None),
"ValueTaskResult" => scheduler.Enqueue<string, int>("key", () => { action(); return new ValueTask<int>(42); }),
"TokenValueTaskResult" => scheduler.Enqueue<string, int>("key", (CancellationToken token) => { action(); return new ValueTask<int>(42); }),
"TokenValueTaskResultWithToken" => scheduler.Enqueue<string, int>("key", (CancellationToken token) => { action(); return new ValueTask<int>(42); }, CancellationToken.None),
"StateValueTaskResult" => scheduler.Enqueue<string, int, int>("key", (key, state, token) => { action(); return new ValueTask<int>(state); }, 42, CancellationToken.None),
"Func" => scheduler.Enqueue<string, int>("key", () => { action(); return 42; }),
"TokenFunc" => scheduler.Enqueue<string, int>("key", (string key) => { action(); return 42; }, CancellationToken.None),
_ => throw new ArgumentOutOfRangeException(nameof(overload)),
};
}

return overload switch
{
"DirectGeneric" => scheduler.Enqueue((state, token) => { action(); return new ValueTask<int>(42); }, null, CancellationToken.None),
"Action" => scheduler.Enqueue(action),
"ActionWithToken" => scheduler.Enqueue(action, CancellationToken.None),
"TokenAction" => scheduler.Enqueue(token => { action(); }),
"TokenActionWithToken" => scheduler.Enqueue(token => { action(); }, CancellationToken.None),
"Task" => scheduler.Enqueue(TaskOperation),
"TokenTask" => scheduler.Enqueue(token => TaskOperation()),
"TokenTaskWithToken" => scheduler.Enqueue(token => TaskOperation(), CancellationToken.None),
"StateTask" => scheduler.Enqueue((int state, CancellationToken token) => TaskOperation(), 42, CancellationToken.None),
"ValueTask" => scheduler.Enqueue(ValueTaskOperation, null),
"TokenValueTask" => scheduler.Enqueue(token => ValueTaskOperation(), null),
"TokenValueTaskWithToken" => scheduler.Enqueue(token => ValueTaskOperation(), CancellationToken.None, null),
"StateValueTask" => scheduler.Enqueue((int state, CancellationToken token) => ValueTaskOperation(), 42, CancellationToken.None),
"TaskResult" => scheduler.Enqueue(() => { action(); return Task.FromResult(42); }),
"TokenTaskResult" => scheduler.Enqueue(token => { action(); return Task.FromResult(42); }),
"TokenTaskResultWithToken" => scheduler.Enqueue(token => { action(); return Task.FromResult(42); }, CancellationToken.None),
"StateTaskResult" => scheduler.Enqueue((int state, CancellationToken token) => { action(); return Task.FromResult(state); }, 42, CancellationToken.None),
"ValueTaskResult" => scheduler.Enqueue(() => { action(); return new ValueTask<int>(42); }, null),
"TokenValueTaskResult" => scheduler.Enqueue(token => { action(); return new ValueTask<int>(42); }, null),
"TokenValueTaskResultWithToken" => scheduler.Enqueue(token => { action(); return new ValueTask<int>(42); }, CancellationToken.None),
"StateValueTaskResult" => scheduler.Enqueue((int state, CancellationToken token) => { action(); return new ValueTask<int>(state); }, 42, CancellationToken.None),
"Func" => scheduler.Enqueue(() => { action(); return 42; }),
"TokenFunc" => scheduler.Enqueue(token => { action(); return 42; }, CancellationToken.None),
_ => throw new ArgumentOutOfRangeException(nameof(overload)),
};
}

public static Task EnqueueNullDelegate(ITaskScheduler scheduler, string overload, bool keyed)
{
if (keyed)
{
return overload switch
{
"DirectGeneric" => scheduler.Enqueue<string, int, int>("key", (Func<string, int, CancellationToken, ValueTask<int>>)null!, 42, CancellationToken.None),
"Action" => scheduler.Enqueue<string>("key", (Action)null!),
"ActionWithToken" => scheduler.Enqueue<string>("key", (Action)null!, CancellationToken.None),
"TokenAction" => scheduler.Enqueue<string>("key", (Action<string>)null!),
"TokenActionWithToken" => scheduler.Enqueue<string>("key", (Action<string>)null!, CancellationToken.None),
"Task" => scheduler.Enqueue<string>("key", (Func<Task>)null!),
"TokenTask" => scheduler.Enqueue<string>("key", (Func<CancellationToken, Task>)null!),
"TokenTaskWithToken" => scheduler.Enqueue<string>("key", (Func<CancellationToken, Task>)null!, CancellationToken.None),
"StateTask" => scheduler.Enqueue<string, int, int>("key", (Func<string, int, CancellationToken, Task<int>>)null!, 42, CancellationToken.None),
"ValueTask" => scheduler.Enqueue<string>("key", (Func<ValueTask>)null!),
"TokenValueTask" => scheduler.Enqueue<string>("key", (Func<CancellationToken, ValueTask>)null!),
"TokenValueTaskWithToken" => scheduler.Enqueue<string>("key", (Func<CancellationToken, ValueTask>)null!, CancellationToken.None),
"StateValueTask" => scheduler.Enqueue<string, int, int>("key", (Func<string, int, CancellationToken, ValueTask<int>>)null!, 42, CancellationToken.None),
"TaskResult" => scheduler.Enqueue<string, int>("key", (Func<Task<int>>)null!),
"TokenTaskResult" => scheduler.Enqueue<string, int>("key", (Func<CancellationToken, Task<int>>)null!),
"TokenTaskResultWithToken" => scheduler.Enqueue<string, int>("key", (Func<CancellationToken, Task<int>>)null!, CancellationToken.None),
"StateTaskResult" => scheduler.Enqueue<string, int, int>("key", (Func<string, int, CancellationToken, Task<int>>)null!, 42, CancellationToken.None),
"ValueTaskResult" => scheduler.Enqueue<string, int>("key", (Func<ValueTask<int>>)null!),
"TokenValueTaskResult" => scheduler.Enqueue<string, int>("key", (Func<CancellationToken, ValueTask<int>>)null!),
"TokenValueTaskResultWithToken" => scheduler.Enqueue<string, int>("key", (Func<CancellationToken, ValueTask<int>>)null!, CancellationToken.None),
"StateValueTaskResult" => scheduler.Enqueue<string, int, int>("key", (Func<string, int, CancellationToken, ValueTask<int>>)null!, 42, CancellationToken.None),
"Func" => scheduler.Enqueue<string, int>("key", (Func<int>)null!),
"TokenFunc" => scheduler.Enqueue<string, int>("key", (Func<string, int>)null!, CancellationToken.None),
_ => throw new ArgumentOutOfRangeException(nameof(overload)),
};
}
return overload switch
{
"DirectGeneric" => scheduler.Enqueue<int>(null!, null, CancellationToken.None),
"Action" => scheduler.Enqueue((Action)null!),
"ActionWithToken" => scheduler.Enqueue((Action)null!, CancellationToken.None),
"TokenAction" => scheduler.Enqueue((Action<CancellationToken>)null!),
"TokenActionWithToken" => scheduler.Enqueue((Action<CancellationToken>)null!, CancellationToken.None),
"Task" => scheduler.Enqueue((Func<Task>)null!),
"TokenTask" => scheduler.Enqueue((Func<CancellationToken, Task>)null!),
"TokenTaskWithToken" => scheduler.Enqueue((Func<CancellationToken, Task>)null!, CancellationToken.None),
"StateTask" => scheduler.Enqueue((Func<int, CancellationToken, Task>)null!, 42, CancellationToken.None),
"ValueTask" => scheduler.Enqueue((Func<ValueTask>)null!, _: null),
"TokenValueTask" => scheduler.Enqueue((Func<CancellationToken, ValueTask>)null!, _: null),
"TokenValueTaskWithToken" => scheduler.Enqueue((Func<CancellationToken, ValueTask>)null!, CancellationToken.None, null),
"StateValueTask" => scheduler.Enqueue((Func<int, CancellationToken, ValueTask>)null!, 42, CancellationToken.None),
"TaskResult" => scheduler.Enqueue((Func<Task<int>>)null!),
"TokenTaskResult" => scheduler.Enqueue((Func<CancellationToken, Task<int>>)null!),
"TokenTaskResultWithToken" => scheduler.Enqueue((Func<CancellationToken, Task<int>>)null!, CancellationToken.None),
"StateTaskResult" => scheduler.Enqueue((Func<int, CancellationToken, Task<int>>)null!, 42, CancellationToken.None),
"ValueTaskResult" => scheduler.Enqueue((Func<ValueTask<int>>)null!, _: null),
"TokenValueTaskResult" => scheduler.Enqueue((Func<CancellationToken, ValueTask<int>>)null!, _: null),
"TokenValueTaskResultWithToken" => scheduler.Enqueue((Func<CancellationToken, ValueTask<int>>)null!, CancellationToken.None),
"StateValueTaskResult" => scheduler.Enqueue((Func<int, CancellationToken, ValueTask<int>>)null!, 42, CancellationToken.None),
"Func" => scheduler.Enqueue((Func<int>)null!),
"TokenFunc" => scheduler.Enqueue((Func<CancellationToken, int>)null!, CancellationToken.None),
_ => throw new ArgumentOutOfRangeException(nameof(overload)),
};
}
}
Loading
Loading