Skip to content

Commit

Permalink
version 4, SampleParallel with model (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyLloyd authored Aug 28, 2024
1 parent 56cb680 commit 67c0280
Show file tree
Hide file tree
Showing 16 changed files with 593 additions and 265 deletions.
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

0 comments on commit 67c0280

Please sign in to comment.