Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 4 #22

Merged
merged 15 commits into from
Aug 28, 2024
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
2 changes: 1 addition & 1 deletion Comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Size is also a better representation of comparison especially for collections or
There are examples where increasing on one axis while decreasing on others can lead to smaller cases e.g. if Version fails for `2 * ma + mi + bu ≥ 255 * 2`
CsCheck will be able to shrink to `255.0.0` but [Hedgehog](https://github.com/hedgehogqa) won't.

For concurrency testing random shrinkers also has an advantage. Concurrency tests may not fail deterministically.
For parallel testing random shrinkers also has an advantage. Parallel tests may not fail deterministically.
This is a real problem for path explorer shrinkers. The only solution is to repeat each test multiple times (10 for QuickCheck) since they need to follow defined paths.
For a random shrinker you can just continue testing different random cases until one fails and limit the size to that each time.

Expand Down
499 changes: 406 additions & 93 deletions CsCheck/Check.cs

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions CsCheck/CsCheck.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,29 @@ This gives the following advantages:
- Random testing and shrinking are parallelized. This and PCG make it very fast.
- Shrunk cases have a seed value. Simpler examples can easily be reproduced.
- Shrinking can be continued later to give simpler cases for high dimensional problems.
- Concurrency testing and random shrinking work well together.
- Parallel testing and random shrinking work well together.

CsCheck also makes concurrency, performance and regression testing simple and fast.
CsCheck also makes parallel, performance and regression testing simple and fast.
</Description>
<Authors>Anthony Lloyd</Authors>
<Owners>Anthony Lloyd</Owners>
<Copyright>Copyright 2024</Copyright>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>http://github.com/AnthonyLloyd/CsCheck</PackageProjectUrl>
<PackageIcon>CsCheck.png</PackageIcon>
<PackageTags>quickcheck;random;model-based;metamorphic;concurrency;performance;causal-profiling;regression;testing</PackageTags>
<Version>3.2.2</Version>
<PackageTags>quickcheck;random;model-based;metamorphic;parallel;performance;causal-profiling;regression;testing</PackageTags>
<Version>4.0.0</Version>
<PackageReleaseNotes>
Added option to Dbg.Time stats for completed and running.
Added SampleParallel overloads using a sequencially run model for linearization.
Added running operations sequentially before parallel to SampleParallel.
Added maxSequentialOperations and maxParallelOperations parameters to SampleParallel.

BREAKING CHANGES:
- Now targets net8.0, version 3.2.2 was the last to target net6.0.
- SampleConcurrent renamed to SampleParallel.
- GenOperation&lt;Actual,Model&gt; takes separate actions for actual and model.
</PackageReleaseNotes>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningLevel>9999</WarningLevel>
Expand All @@ -41,13 +48,13 @@ Added option to Dbg.Time stats for completed and running.
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<NoWarn>CS1591</NoWarn>
<NoWarn>CS1591,MA0143</NoWarn>
<PackageReadmeFile>README.md</PackageReadmeFile>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.120" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.163" PrivateAssets="All" />
<None Include="../CsCheck.png" Pack="true" PackagePath="" Visible="False" />
<None Include="../README.md" Pack="true" PackagePath="" Visible="False" />
</ItemGroup>
Expand Down
43 changes: 24 additions & 19 deletions CsCheck/Gen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ public abstract class Gen<T> : IGen<T>
public GenOperation<S> Operation<S>(Func<T, string> name, Func<S, T, Task> async) => GenOperation.Create(this, name, (S s, T t) => async(s, t).GetAwaiter().GetResult());
public GenOperation<S> Operation<S>(Action<S, T> action) => GenOperation.Create(this, action);
public GenOperation<S> Operation<S>(Func<S, T, Task> async) => GenOperation.Create(this, (S s, T t) => async(s, t).GetAwaiter().GetResult());
public GenOperation<Actual, Model> Operation<Actual, Model>(Func<T, string> name, Action<Actual, Model, T> action) => GenOperation.Create(this, name, action);
public GenOperation<Actual, Model> Operation<Actual, Model>(Action<Actual, Model, T> action) => GenOperation.Create(this, action);
public GenMetamorphic<S> Metamorphic<S>(Func<T, string> name, Action<S, T> action1, Action<S, T> action2) => GenOperation.Create(this, name, action1, action2);
public GenMetamorphic<S> Metamorphic<S>(Action<S, T> action1, Action<S, T> action2) => GenOperation.Create(this, Check.Print, action1, action2);
public GenOperation<Actual, Model> Operation<Actual, Model>(Func<T, string> name, Action<Actual, T> actual, Action<Model, T> model) => GenOperation.Create(this, name, actual, model);
public GenOperation<Actual, Model> Operation<Actual, Model>(Action<Actual, T> actual, Action<Model, T> model) => GenOperation.Create(this, actual, model);
public GenMetamorphic<S> Metamorphic<S>(Func<T, string> name, Action<S, T> action1, Action<S, T> action2) => GenMetamorphic.Create(this, name, action1, action2);
public GenMetamorphic<S> Metamorphic<S>(Action<S, T> action1, Action<S, T> action2) => GenMetamorphic.Create(this, Check.Print, action1, action2);

/// <summary>Generator for an array of <typeparamref name="T"/></summary>
public GenArray<T> Array => new(this);
Expand Down Expand Up @@ -1456,8 +1456,8 @@ sealed class GenNull<T>(Gen<T> gen, uint nullLimit) : Gen<T?> where T : class
public static GenOperation<T> Operation<T>(string name, Func<T, Task> async) => GenOperation.Create(name, (T t) => async(t).GetAwaiter().GetResult());
public static GenOperation<T> Operation<T>(Action<T> action) => GenOperation.Create(action);
public static GenOperation<T> Operation<T>(Func<T, Task> async) => GenOperation.Create((T t) => async(t).GetAwaiter().GetResult());
public static GenOperation<Actual, Model> Operation<Actual, Model>(string name, Action<Actual, Model> action) => GenOperation.Create(name, action);
public static GenOperation<Actual, Model> Operation<Actual, Model>(Action<Actual, Model> action) => GenOperation.Create(action);
public static GenOperation<Actual, Model> Operation<Actual, Model>(string name, Action<Actual> actual, Action<Model> model) => GenOperation.Create(name, actual, model);
public static GenOperation<Actual, Model> Operation<Actual, Model>(Action<Actual> actual, Action<Model> model) => GenOperation.Create(actual, model);

/// <summary>Generator for bool.</summary>
public static readonly GenBool Bool = new();
Expand Down Expand Up @@ -1939,7 +1939,8 @@ static Gen<int> GenInt(float start, float finish)
lower--;
var rational = Gen.Int[lower + 1, denominator]
.SelectMany(den => GenInt(start * den, finish * den)
.Select(num => (float)num / den));
.Select(num => (float)num / den))
.Where(r => r >= start && r <= finish);
myGens[1] = (1, rational);
}
Gen<float>? exponential = null;
Expand Down Expand Up @@ -2851,16 +2852,16 @@ internal GenOperation(Gen<(string, Action<T>)> gen, bool addOpNumber)
public override (string, Action<T>) Generate(PCG pcg, Size? min, out Size size) => gen.Generate(pcg, min, out size);
}

public sealed class GenOperation<T1, T2> : Gen<(string, Action<T1, T2>)>
public sealed class GenOperation<Actual, Model> : Gen<(string, Action<Actual>, Action<Model>)>
{
readonly Gen<(string, Action<Actual>, Action<Model>)> gen;
public bool AddOpNumber;
readonly Gen<(string, Action<T1, T2>)> gen;
internal GenOperation(Gen<(string, Action<T1, T2>)> gen, bool addOpNumber)
internal GenOperation(Gen<(string, Action<Actual>, Action<Model>)> gen, bool addOpNumber)
{
this.gen = gen;
AddOpNumber = addOpNumber;
}
public override (string, Action<T1, T2>) Generate(PCG pcg, Size? min, out Size size) => gen.Generate(pcg, min, out size);
public override (string, Action<Actual>, Action<Model>) Generate(PCG pcg, Size? min, out Size size) => gen.Generate(pcg, min, out size);
}

public sealed class GenMetamorphic<T> : Gen<(string, Action<T>, Action<T>)>
Expand All @@ -2876,18 +2877,22 @@ public static GenOperation<S> Create<S, T>(Gen<T> gen, Action<S, T> action) =>
new(gen.Select<T, (string, Action<S>)>(t => (" " + Check.Print(t), s => action(s, t))), true);
public static GenOperation<S> Create<S, T>(Gen<T> gen, Func<T, string> name, Action<S, T> action) =>
new(gen.Select<T, (string, Action<S>)>(t => (name(t), s => action(s, t))), false);
public static GenOperation<S1, S2> Create<S1, S2, T>(Gen<T> gen, Action<S1, S2, T> action) =>
new(gen.Select<T, (string, Action<S1, S2>)>(t => (" " + Check.Print(t), (s1, s2) => action(s1, s2, t))), true);
public static GenOperation<S1, S2> Create<S1, S2, T>(Gen<T> gen, Func<T, string> name, Action<S1, S2, T> action) =>
new(gen.Select<T, (string, Action<S1, S2>)>(t => (name(t), (s1, s2) => action(s1, s2, t))), false);
public static GenOperation<Actual, Model> Create<Actual, Model, T>(Gen<T> gen, Action<Actual, T> actual, Action<Model, T> model) =>
new(gen.Select<T, (string, Action<Actual>, Action<Model>)>(t => (" " + Check.Print(t), a => actual(a, t), m => model(m, t))), true);
public static GenOperation<Actual, Model> Create<Actual, Model, T>(Gen<T> gen, Func<T, string> name, Action<Actual, T> actual, Action<Model, T> model) =>
new(gen.Select<T, (string, Action<Actual>, Action<Model>)>(t => (name(t), a => actual(a, t), m => model(m, t))), false);
public static GenOperation<Actual, Model> Create<Actual, Model>(Action<Actual> actual, Action<Model> model)
=> new(Gen.Const(("", actual, model)), true);
public static GenOperation<Actual, Model> Create<Actual, Model>(string name, Action<Actual> actual, Action<Model> model)
=> new(Gen.Const((name, actual, model)), false);
public static GenOperation<T> Create<T>(Action<T> action)
=> new(Gen.Const(("", action)), true);
public static GenOperation<T> Create<T>(string name, Action<T> action)
=> new(Gen.Const((name, action)), false);
public static GenOperation<T1, T2> Create<T1, T2>(Action<T1, T2> action)
=> new(Gen.Const(("", action)), true);
public static GenOperation<T1, T2> Create<T1, T2>(string name, Action<T1, T2> action)
=> new(Gen.Const((name, action)), false);
}

public static class GenMetamorphic
{
public static GenMetamorphic<S> Create<S, T>(Gen<T> gen, Func<T, string> name, Action<S, T> action1, Action<S, T> action2) =>
new(gen.Select<T, (string, Action<S>, Action<S>)>(t => (name(t), s => action1(s, t), s => action2(s, t))));
}
95 changes: 56 additions & 39 deletions CsCheck/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace CsCheck;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System;

public sealed class CsCheckException : Exception
{
Expand Down Expand Up @@ -475,67 +477,82 @@ public static bool ModelEqual<T, M>(T actual, M model)
return actual.Equals(model);
}

static readonly int[] DummyArray = new int[MAX_CONCURRENT_OPERATIONS];
internal static void Run<T>(T concurrentState, (string, Action<T>)[] operations, int threads, int[]? threadIds = null)
sealed class RunWorker<T>(T state, (string, Action<T>)[] parallelOperations, int[]? threadIds) : IThreadPoolWorkItem
{
threadIds ??= DummyArray;
Exception? exception = null;
var opId = -1;
var runners = new Thread[threads];
while (--threads >= 0)
int opId = -1, threadId = -1;
public volatile bool Hold = true;
public Exception? Exception;
public void Execute()
{
runners[threads] = new Thread(threadId =>
int i, tid = Interlocked.Increment(ref threadId);
while (Hold) { }
while ((i = Interlocked.Increment(ref opId)) < parallelOperations.Length)
{
int i, tid = (int)threadId!;
while ((i = Interlocked.Increment(ref opId)) < operations.Length)
if (threadIds is not null) threadIds[i] = tid;
try { parallelOperations[i].Item2(state); }
catch (Exception e)
{
threadIds[i] = tid;
try { operations[i].Item2(concurrentState); }
catch (Exception e)
if (Exception is null)
{
if (exception is null)
{
exception = e;
Interlocked.Exchange(ref opId, operations.Length);
}
Exception = e;
opId = 1_000_000;
}
}
});
}
}
for (int i = 0; i < runners.Length; i++) runners[i].Start(i);
for (int i = 0; i < runners.Length; i++) runners[i].Join();
if (exception is not null) throw exception;
}

internal static void RunReplay<T>(T concurrentState, (string, Action<T>)[] operations, int threads, int[] threadIds)
internal static void Run<T>(T state, (string, Action<T>)[] sequencialOperations, (string, Action<T>)[] parallelOperations, int threads, int[]? threadIds = null)
{
Exception? exception = null;
for (int i = 0; i < sequencialOperations.Length; i++)
sequencialOperations[i].Item2(state);
var worker = new RunWorker<T>(state, parallelOperations, threadIds);
var runners = new Thread[threads];
while (--threads >= 0)
for (int i = 0; i < runners.Length; i++) runners[i] = new Thread(worker.Execute);
for (int i = 0; i < runners.Length; i++) runners[i].Start();
worker.Hold = false;
for (int i = 0; i < runners.Length; i++) runners[i].Join();
if (worker.Exception is not null) throw worker.Exception;
}

sealed class RunReplayWorker<T>(T state, (string, Action<T>)[] parallelOperations, int[] threadIds) : IThreadPoolWorkItem
{
int threadId = -1;
public volatile bool Hold = true;
public Exception? Exception;
public void Execute()
{
runners[threads] = new Thread(threadId =>
int i, opId = -1, tid = Interlocked.Increment(ref threadId);
while (Hold) { }
while ((i = Interlocked.Increment(ref opId)) < parallelOperations.Length)
{
int opId = -1, i = -1, tid = (int)threadId!;
while ((i = Interlocked.Increment(ref opId)) < operations.Length)
if (threadIds[i] == tid)
{
if (threadIds[i] == tid)
try { parallelOperations[i].Item2(state); }
catch (Exception e)
{
try { operations[i].Item2(concurrentState); }
catch (Exception e)
if (Exception is null)
{
if (exception is null)
{
exception = e;
Interlocked.Exchange(ref opId, operations.Length);
}
Exception = e;
opId = 1_000_000;
}
}
}
});
}
}
for (int i = 0; i < runners.Length; i++) runners[i].Start(i);
}

internal static void RunReplay<T>(T state, (string, Action<T>)[] sequencialOperations, (string, Action<T>)[] parallelOperations, int threads, int[] threadIds)
{
for (int i = 0; i < sequencialOperations.Length; i++)
sequencialOperations[i].Item2(state);
var worker = new RunReplayWorker<T>(state, parallelOperations, threadIds);
var runners = new Thread[threads];
for (int i = 0; i < runners.Length; i++) runners[i] = new Thread(worker.Execute);
for (int i = 0; i < runners.Length; i++) runners[i].Start();
worker.Hold = false;
for (int i = 0; i < runners.Length; i++) runners[i].Join();
if (exception is not null) throw exception;
if (worker.Exception is not null) throw worker.Exception;
}

internal static IEnumerable<T[]> Permutations<T>(int[] threadIds, T[] sequence)
Expand Down
Loading
Loading