diff --git a/tests/DotMaybe.PropertyTests/AllocationProperties.cs b/tests/DotMaybe.PropertyTests/AllocationProperties.cs index e3eb400..8d356ff 100644 --- a/tests/DotMaybe.PropertyTests/AllocationProperties.cs +++ b/tests/DotMaybe.PropertyTests/AllocationProperties.cs @@ -10,7 +10,10 @@ namespace DotMaybe.PropertyTests; /// /// Skipped in Debug builds (see ); CI runs them in Release. /// Not covered on purpose: ToString (a string), sequences (Choose, Sequence, FirstOrNone -/// over IEnumerable<T> allocate like LINQ), JSON, and async steps that really wait. +/// over IEnumerable<T> allocate like LINQ), JSON, and async steps that really wait (the runtime +/// allocates a state machine and a task for every await that does not complete synchronously, so 0 bytes cannot be +/// promised there). Steps with Task-returning delegates are measured with tasks completed in advance +/// ( and friends), so any allocation comes from the library, not from the delegate. /// public sealed class AllocationProperties { @@ -132,8 +135,100 @@ public void Async_steps_on_completed_sources_with_synchronous_steps_allocate_not Allocations.AssertNone( "async query with two from (Task)", () => _ = (from value in fromTask from half in Half(value) select value + half).IsCompleted); + + Allocations.AssertNone( + "MatchAsync (ValueTask)", + () => _ = fromValueTask.MatchAsync(static value => value, static () => 0).IsCompleted); + Allocations.AssertNone( + "MatchAsync (Task)", + () => _ = fromTask.MatchAsync(static value => value, static () => 0).IsCompleted); + Allocations.AssertNone( + "OrDefaultAsync(factory) (ValueTask)", + () => _ = fromValueTask.OrDefaultAsync(static () => 0).IsCompleted); + Allocations.AssertNone( + "OrDefaultAsync(factory) (Task)", + () => _ = fromTask.OrDefaultAsync(static () => 0).IsCompleted); + Allocations.AssertNone("BindAsync (Task)", () => _ = fromTask.BindAsync(static value => Half(value)).IsCompleted); + Allocations.AssertNone("FilterAsync (Task)", () => _ = fromTask.FilterAsync(static value => value > 0).IsCompleted); + Allocations.AssertNone( + "async query (Task)", + () => _ = (from value in fromTask where value > 0 select value + 1).IsCompleted); + } + + [Property(Skip = Allocations.SkipInDebug)] + public void Async_steps_with_completed_tasks_allocate_nothing(Maybe maybe) + { + Allocations.AssertNone("MapAsync", () => _ = maybe.MapAsync(static _ => CompletedOne).IsCompleted); + Allocations.AssertNone("BindAsync", () => _ = maybe.BindAsync(static _ => CompletedSomeOne).IsCompleted); + Allocations.AssertNone("FilterAsync", () => _ = maybe.FilterAsync(static _ => CompletedTrue).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, async)", + () => _ = maybe.MatchAsync(static _ => CompletedOne, static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, sync)", + () => _ = maybe.MatchAsync(static _ => CompletedOne, static () => 0).IsCompleted); + Allocations.AssertNone( + "OrDefaultAsync(async factory)", + () => _ = maybe.OrDefaultAsync(static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "async query from a maybe", + () => _ = (from value in maybe from one in CompletedSomeOne select value + one).IsCompleted); + } + + [Property(Skip = Allocations.SkipInDebug)] + public void Async_steps_on_completed_sources_with_completed_tasks_allocate_nothing(Maybe maybe) + { + var fromValueTask = ValueTask.FromResult(maybe); + var fromTask = Task.FromResult(maybe); + + Allocations.AssertNone( + "MapAsync (ValueTask)", + () => _ = fromValueTask.MapAsync(static _ => CompletedOne).IsCompleted); + Allocations.AssertNone( + "BindAsync (ValueTask)", + () => _ = fromValueTask.BindAsync(static _ => CompletedSomeOne).IsCompleted); + Allocations.AssertNone( + "FilterAsync (ValueTask)", + () => _ = fromValueTask.FilterAsync(static _ => CompletedTrue).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, async) (ValueTask)", + () => _ = fromValueTask.MatchAsync(static _ => CompletedOne, static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, sync) (ValueTask)", + () => _ = fromValueTask.MatchAsync(static _ => CompletedOne, static () => 0).IsCompleted); + Allocations.AssertNone( + "OrDefaultAsync(async factory) (ValueTask)", + () => _ = fromValueTask.OrDefaultAsync(static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "async query with an async second from (ValueTask)", + () => _ = (from value in fromValueTask from one in CompletedSomeOne select value + one).IsCompleted); + + Allocations.AssertNone("MapAsync (Task)", () => _ = fromTask.MapAsync(static _ => CompletedOne).IsCompleted); + Allocations.AssertNone( + "BindAsync (Task)", + () => _ = fromTask.BindAsync(static _ => CompletedSomeOne).IsCompleted); + Allocations.AssertNone( + "FilterAsync (Task)", + () => _ = fromTask.FilterAsync(static _ => CompletedTrue).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, async) (Task)", + () => _ = fromTask.MatchAsync(static _ => CompletedOne, static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "MatchAsync(async, sync) (Task)", + () => _ = fromTask.MatchAsync(static _ => CompletedOne, static () => 0).IsCompleted); + Allocations.AssertNone( + "OrDefaultAsync(async factory) (Task)", + () => _ = fromTask.OrDefaultAsync(static () => CompletedOne).IsCompleted); + Allocations.AssertNone( + "async query with an async second from (Task)", + () => _ = (from value in fromTask from one in CompletedSomeOne select value + one).IsCompleted); } + // Completed in advance: a Task-returning step hands these back, so the measured code creates no task of its own. + private static readonly Task CompletedOne = Task.FromResult(1); + private static readonly Task CompletedTrue = Task.FromResult(true); + private static readonly Task> CompletedSomeOne = Task.FromResult(Maybes.Some(1)); + private static Maybe Half(int value) => value % 2 == 0 ? value / 2 : none; // Pattern matching through a type parameter, as consumers write it in generic code since the .NET 11 GA compiler.