From b2a95b2f4ee62978dd13a1902c93243a995cf386 Mon Sep 17 00:00:00 2001 From: nth-commit Date: Sun, 17 Jul 2022 10:15:01 +1200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=8D=20Add=20exhaustion=20to=20`CheckAs?= =?UTF-8?q?ync`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AssertTests/AboutExhaustion.cs | 31 +++++++- .../RunnerTests/CheckTests/AboutExhaustion.cs | 75 ++++++++++++++++--- .../RunnerTests/PrintTests/AboutExhaustion.cs | 38 ++++++++-- src/GalaxyCheck.Tests/Timeouts.cs | 43 +++++++++++ .../Internal/AsyncEnumerableExtensions.cs | 56 ++++++++++++++ .../AsyncAutomata/CheckStateAggregator.cs | 5 +- src/GalaxyCheck/Runners/Print.cs | 16 ++++ 7 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 src/GalaxyCheck.Tests/Timeouts.cs diff --git a/src/GalaxyCheck.Tests/RunnerTests/AssertTests/AboutExhaustion.cs b/src/GalaxyCheck.Tests/RunnerTests/AssertTests/AboutExhaustion.cs index 5f084614..7302a865 100644 --- a/src/GalaxyCheck.Tests/RunnerTests/AssertTests/AboutExhaustion.cs +++ b/src/GalaxyCheck.Tests/RunnerTests/AssertTests/AboutExhaustion.cs @@ -3,7 +3,9 @@ using NebulaCheck; using System; using System.Linq; +using System.Threading.Tasks; using static Tests.V2.DomainGenAttributes; +using static Tests.V2.Timeouts; namespace Tests.V2.RunnerTests.AssertTests { @@ -14,11 +16,34 @@ public class Sync [Property(Iterations = 1)] public void ItCanExhaust([Seed] int seed, [Size] int size) { - var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); + RunWithTimeout( + () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); - Action test = () => property.Assert(seed: seed, size: size, deepCheck: false); + Action test = () => property.Assert(seed: seed, size: size, deepCheck: false); - test.Should().Throw(); + test.Should().Throw(); + }, + TimeSpan.FromSeconds(20)); + } + } + + public class Async + { + [Property(Iterations = 1)] + public void ItCanExhaustAsync([Seed] int seed, [Size] int size) + { + RunWithTimeoutAsync( + async () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAllAsync(_ => Task.FromResult(true)); + + Func test = () => property.AssertAsync(seed: seed, size: size, deepCheck: false); + + await test.Should().ThrowAsync(); + }, + TimeSpan.FromSeconds(20)); } } } diff --git a/src/GalaxyCheck.Tests/RunnerTests/CheckTests/AboutExhaustion.cs b/src/GalaxyCheck.Tests/RunnerTests/CheckTests/AboutExhaustion.cs index b3344930..2cd308a9 100644 --- a/src/GalaxyCheck.Tests/RunnerTests/CheckTests/AboutExhaustion.cs +++ b/src/GalaxyCheck.Tests/RunnerTests/CheckTests/AboutExhaustion.cs @@ -3,30 +3,85 @@ using NebulaCheck; using System; using System.Linq; +using System.Threading.Tasks; using static Tests.V2.DomainGenAttributes; +using static Tests.V2.Timeouts; namespace Tests.V2.RunnerTests.CheckTests { public class AboutExhaustion { - [Property(Iterations = 1)] - public void ItExhaustsWhenGenerationIsImpossible([Seed] int seed, [Size] int size) + public class Sync { - var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); + [Property(Iterations = 1)] + public void ItExhaustsWhenGenerationIsImpossible([Seed] int seed, [Size] int size) + { + RunWithTimeout( + () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); - Action test = () => property.Check(seed: seed, size: size, deepCheck: false); + Action test = () => property.Check(seed: seed, size: size); - test.Should().Throw(); + test.Should().Throw(); + + }, + TimeSpan.FromSeconds(20)); + } + + [Property(Iterations = 1)] + public void ItExhaustsWhenPreconditionIsImpossible([Seed] int seed, [Size] int size) + { + RunWithTimeout( + () => + { + var property = GalaxyCheck.Gen.Int32().ForAll(_ => GalaxyCheck.Property.Precondition(false)); + + Action test = () => property.Check(seed: seed, size: size); + + test.Should().Throw(); + }, + TimeSpan.FromSeconds(20)); + } } - [Property(Iterations = 1)] - public void ItExhaustsWhenPreconditionIsImpossible([Seed] int seed, [Size] int size) + public class Async { - var property = GalaxyCheck.Gen.Int32().ForAll(_ => GalaxyCheck.Property.Precondition(false)); + [Property(Iterations = 1)] + public void ItExhaustsWhenGenerationIsImpossible([Seed] int seed, [Size] int size) + { + RunWithTimeoutAsync( + async () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAllAsync(_ => Task.CompletedTask); + + Func test = () => property.CheckAsync(seed: seed, size: size); + + await test.Should().ThrowAsync(); + + }, + TimeSpan.FromSeconds(20)); + } + + [Property(Iterations = 1)] + public void ItExhaustsWhenPreconditionIsImpossible([Seed] int seed, [Size] int size) + { + RunWithTimeoutAsync( + async () => + { + var property = GalaxyCheck.Gen.Int32().ForAllAsync(async _ => + { + await Task.CompletedTask; + GalaxyCheck.Property.Precondition(false); + }); + + Func test = () => property.CheckAsync(seed: seed, size: size); - Action test = () => property.Check(seed: seed, size: size, deepCheck: false); + await test.Should().ThrowAsync(); + }, + TimeSpan.FromSeconds(20)); + } - test.Should().Throw(); } } } diff --git a/src/GalaxyCheck.Tests/RunnerTests/PrintTests/AboutExhaustion.cs b/src/GalaxyCheck.Tests/RunnerTests/PrintTests/AboutExhaustion.cs index cea0acfb..1978af46 100644 --- a/src/GalaxyCheck.Tests/RunnerTests/PrintTests/AboutExhaustion.cs +++ b/src/GalaxyCheck.Tests/RunnerTests/PrintTests/AboutExhaustion.cs @@ -3,20 +3,48 @@ using NebulaCheck; using System; using System.Linq; +using System.Threading.Tasks; using static Tests.V2.DomainGenAttributes; +using static Tests.V2.Timeouts; namespace Tests.V2.RunnerTests.PrintTests { public class AboutExhaustion { - [Property(Iterations = 1)] - public void ItCanExhaust([Seed] int seed, [Size] int size) + public class Sync { - var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); + [Property(Iterations = 1)] + public void ItCanExhaust([Seed] int seed, [Size] int size) + { + RunWithTimeout( + () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAll(_ => true); - Action test = () => property.Advanced.Print(seed: seed, size: size); + Action test = () => property.Print(seed: seed, size: size); - test.Should().Throw(); + test.Should().Throw(); + }, + TimeSpan.FromSeconds(20)); + } + } + + public class Async + { + [Property(Iterations = 1)] + public void ItCanExhaustAsync([Seed] int seed, [Size] int size) + { + RunWithTimeoutAsync( + async () => + { + var property = GalaxyCheck.Gen.Int32().Where(x => false).ForAllAsync(_ => Task.FromResult(true)); + + Func test = () => property.PrintAsync(seed: seed, size: size); + + await test.Should().ThrowAsync(); + }, + TimeSpan.FromSeconds(20)); + } } } } diff --git a/src/GalaxyCheck.Tests/Timeouts.cs b/src/GalaxyCheck.Tests/Timeouts.cs new file mode 100644 index 00000000..621026e4 --- /dev/null +++ b/src/GalaxyCheck.Tests/Timeouts.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Tests.V2 +{ + public static class Timeouts + { + public static void RunWithTimeout(Action action, TimeSpan timeout) + { + var task = Task.Run(action); + try + { + var success = task.Wait(timeout); + if (!success) + { + throw new TimeoutException(); + } + } + catch (AggregateException ex) when (ex.InnerException != null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + + public static void RunWithTimeoutAsync(Func action, TimeSpan timeout) + { + var task = Task.Run(action); + try + { + var success = task.Wait(timeout); + if (!success) + { + throw new TimeoutException(); + } + } + catch (AggregateException ex) when (ex.InnerException != null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + } +} diff --git a/src/GalaxyCheck/Internal/AsyncEnumerableExtensions.cs b/src/GalaxyCheck/Internal/AsyncEnumerableExtensions.cs index cedc8f3b..73d50f75 100644 --- a/src/GalaxyCheck/Internal/AsyncEnumerableExtensions.cs +++ b/src/GalaxyCheck/Internal/AsyncEnumerableExtensions.cs @@ -42,6 +42,62 @@ public static async IAsyncEnumerable Unfold(T seed, Func } } + public static async IAsyncEnumerable<(TSource element, TAccumulator state)> ScanInParallel( + this IAsyncEnumerable input, + TAccumulator seed, + Func next) + { + var state = seed; + await foreach (var item in input) + { + state = next(state, item); + yield return (item, state); + } + } + + public static IAsyncEnumerable<(T element, int consecutiveDiscardCount)> WithConsecutiveDiscardCount( + this IAsyncEnumerable source, + Func isCounted, + Func isDiscard) + { + return source.ScanInParallel(0, (consecutiveDiscardCount, element) => + { + if (isCounted(element) == false) + { + // The element is considered neither a discard or a non-discard, don't increment or reset the + // consecutive discard count. + return consecutiveDiscardCount; + } + + if (isDiscard(element)) + { + return consecutiveDiscardCount + 1; + } + + return 0; + }); + } + + public static IAsyncEnumerable WithDiscardCircuitBreaker( + this IAsyncEnumerable source, + Func isCounted, + Func isDiscard) + { + return source + .WithConsecutiveDiscardCount(isCounted, isDiscard) + .Select((x) => + { + var (element, consecutiveDiscardCount) = x; + + if (consecutiveDiscardCount > 500) + { + throw new Exceptions.GenExhaustionException(); + } + + return element; + }); + } + public static async Task<(T? head, IAsyncEnumerable tail)> Deconstruct(this IAsyncEnumerable source) { var head = await source.FirstOrDefaultAsync(); diff --git a/src/GalaxyCheck/Runners/Check/AsyncAutomata/CheckStateAggregator.cs b/src/GalaxyCheck/Runners/Check/AsyncAutomata/CheckStateAggregator.cs index f2c1056c..94a60ebb 100644 --- a/src/GalaxyCheck/Runners/Check/AsyncAutomata/CheckStateAggregator.cs +++ b/src/GalaxyCheck/Runners/Check/AsyncAutomata/CheckStateAggregator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; namespace GalaxyCheck.Runners.Check.AsyncAutomata { @@ -54,10 +55,10 @@ private static CheckStateTransition DecorateTransition( .Select(x => x.nextTransition) .Last(); - private static async System.Threading.Tasks.Task> AggregateTransitionsAsync(IAsyncEnumerable> transitions) + private static async Task> AggregateTransitionsAsync(IAsyncEnumerable> transitions) { var mappedTransitions = await transitions - //.WithDiscardCircuitBreaker(isTransitionCountedInConsecutiveDiscardCount, isTransitionDiscard) + .WithDiscardCircuitBreaker(isTransitionCountedInConsecutiveDiscardCount, isTransitionDiscard) .Select(x => ( state: x.State, check: MapStateToIterationOrIgnore(x.State), diff --git a/src/GalaxyCheck/Runners/Print.cs b/src/GalaxyCheck/Runners/Print.cs index 7780b17e..1b8d2bb5 100644 --- a/src/GalaxyCheck/Runners/Print.cs +++ b/src/GalaxyCheck/Runners/Print.cs @@ -8,6 +8,14 @@ namespace GalaxyCheck { public static partial class Extensions { + // TODO: Delete this once C# supports extension methods after implicit conversions + public static void Print( + this Property property, + int? iterations = null, + int? seed = null, + int? size = null, + Action? stdout = null) => Print((Property)property, iterations, seed, size, stdout); + public static void Print( this Property property, int? iterations = null, @@ -23,6 +31,14 @@ public static void Print( PrintHelpers.Print(sample.Values, sample.Discards, stdout); } + // TODO: Delete this once C# supports extension methods after implicit conversions + public static Task PrintAsync( + this AsyncProperty property, + int? iterations = null, + int? seed = null, + int? size = null, + Action? stdout = null) => PrintAsync((AsyncProperty)property, iterations, seed, size, stdout); + public static async Task PrintAsync( this AsyncProperty property, int? iterations = null,