Created
April 5, 2016 07:17
-
-
Save Porges/342b809c4f0a6363b9e182a81798c858 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.CodeDom.Compiler; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Xunit; | |
using Xunit.Abstractions; | |
using Xunit.Sdk; | |
namespace AutoData | |
{ | |
[AttributeUsage(AttributeTargets.Method)] | |
[XunitTestCaseDiscoverer("AutoData.PropertyTestCaseDiscoverer", "AutoData")] | |
public sealed class PropertyAttribute : FactAttribute | |
{ | |
public uint Iterations { get; set; } = 1000; | |
public bool Exhaustive { get; set; } | |
} | |
public struct TestSettings | |
{ | |
public TestSettings | |
( uint iterations | |
, bool exhaustive | |
) | |
{ | |
Iterations = iterations; | |
Exhaustive = exhaustive; | |
} | |
public uint Iterations { get; } | |
public bool Exhaustive { get; } | |
public void Serialize(IXunitSerializationInfo info) | |
{ | |
info.AddValue("TestSettings.Iterations", Iterations); | |
info.AddValue("TestSettings.Exhaustive", Exhaustive); | |
} | |
public static TestSettings Deserialize(IXunitSerializationInfo info) | |
{ | |
return new TestSettings( | |
info.GetValue<uint>("TestSettings.Iterations"), | |
info.GetValue<bool>("TestSettings.Exhaustive") | |
); | |
} | |
} | |
public class PropertyTestCaseDiscoverer : IXunitTestCaseDiscoverer | |
{ | |
private readonly IMessageSink _diagnosticMessageSink; | |
private static IEnumerable<IEnumerable<T>> CartesianProduct<T>(IEnumerable<IEnumerable<T>> sequences) | |
{ | |
IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() }; | |
return sequences.Aggregate( | |
emptyProduct, | |
(accumulator, sequence) => | |
from accseq in accumulator | |
from item in sequence | |
select accseq.Concat(new[] { item }) | |
); | |
} | |
internal static Generator GetGenerator(ITypeInfo t) | |
{ | |
var generator = GeneratorProvider.InstanceForType(t.ToRuntimeType()); | |
if (generator != null) | |
{ | |
return generator; | |
} | |
throw new NotSupportedException($"No generator for type: {t}"); | |
} | |
public PropertyTestCaseDiscoverer(IMessageSink diagnosticMessageSink) | |
{ | |
_diagnosticMessageSink = diagnosticMessageSink; | |
} | |
public IEnumerable<IXunitTestCase> Discover( | |
ITestFrameworkDiscoveryOptions discoveryOptions, | |
ITestMethod testMethod, | |
IAttributeInfo factAttribute) | |
{ | |
var settings = GetTestSettings(factAttribute); | |
List<Generator> generators = null; | |
Exception ex = null; | |
try | |
{ | |
generators = | |
testMethod.Method.GetParameters() | |
.Select(p => GetGenerator(p.ParameterType)) | |
.ToList(); | |
} | |
catch (Exception e) | |
{ | |
ex = e; | |
// throwing here yields un-nice behaviour, | |
// so we return a test that fails instead | |
} | |
if (ex != null) | |
{ | |
//yield return new Whatever(); | |
yield break; | |
} | |
if (settings.Exhaustive || CalculatePotentialCombinations(generators) <= settings.Iterations) | |
{ | |
// generate test cases up-front | |
foreach (var row in CartesianProduct(generators.Select(g => g.AllObjects()))) | |
{ | |
yield return | |
new XunitTestCase( | |
_diagnosticMessageSink, | |
discoveryOptions.MethodDisplayOrDefault(), | |
testMethod, | |
row.ToArray()); | |
} | |
} | |
else | |
{ | |
// otherwise we will generate test cases later | |
yield return | |
new PropertyTestCase( | |
_diagnosticMessageSink, | |
discoveryOptions.MethodDisplayOrDefault(), | |
testMethod, | |
generators, | |
settings); | |
} | |
} | |
private static TestSettings GetTestSettings(IAttributeInfo factAttribute) | |
{ | |
var iterations = factAttribute.GetNamedArgument<uint>("Iterations"); | |
var exhaustive = factAttribute.GetNamedArgument<bool>("Exhaustive"); | |
return new TestSettings(iterations, exhaustive); | |
} | |
private static long? CalculatePotentialCombinations(List<Generator> generators) | |
=> generators.Aggregate((long?)1L, (product, g) => g.PotentialValues() * product); | |
} | |
public class PropertyTestCase : XunitTestCase | |
{ | |
private TestSettings _settings; | |
private IReadOnlyList<Generator> _generators; | |
[Obsolete("Only for serialization")] | |
public PropertyTestCase() | |
{ } | |
public PropertyTestCase | |
( IMessageSink diagnosticMessageSink | |
, TestMethodDisplay defaultMethodDisplay | |
, ITestMethod testMethod | |
, IReadOnlyList<Generator> generators | |
, TestSettings settings | |
) | |
: base | |
( diagnosticMessageSink | |
, defaultMethodDisplay | |
, testMethod | |
) | |
{ | |
_settings = settings; | |
_generators = generators; | |
// technically we should copy here but we control the callers | |
} | |
public override async Task<RunSummary> RunAsync( | |
IMessageSink diagnosticMessageSink, | |
IMessageBus messageBus, | |
object[] constructorArguments, | |
ExceptionAggregator aggregator, | |
CancellationTokenSource cancellationTokenSource) | |
{ | |
// we don't serialize this | |
if (_generators == null) | |
{ | |
_generators = | |
TestMethod.Method.GetParameters() | |
.Select(p => p.ParameterType.ToRuntimeType()) | |
.Select(GeneratorProvider.InstanceForType) | |
.ToList(); | |
} | |
TestMethodArguments = new object[_generators.Count]; | |
try | |
{ | |
var r = new Random(); | |
var summary = new RunSummary(); | |
for (int i = 0; i < _settings.Iterations; ++i) | |
{ | |
FillTestMethodArguments(r); | |
var tSummary = await base.RunAsync( | |
diagnosticMessageSink, | |
messageBus, | |
constructorArguments, | |
aggregator, | |
cancellationTokenSource); | |
summary.Aggregate(tSummary); | |
if (summary.Failed > 0) | |
{ | |
// short-circuit failure | |
break; | |
} | |
} | |
return summary; | |
} | |
finally | |
{ | |
TestMethodArguments = null; | |
} | |
} | |
private void FillTestMethodArguments(Random r) | |
{ | |
for (int j = 0; j < _generators.Count; ++j) | |
{ | |
TestMethodArguments[j] = _generators[j].GenerateObject(r); | |
} | |
} | |
public override void Deserialize(IXunitSerializationInfo data) | |
{ | |
base.Deserialize(data); | |
_settings = TestSettings.Deserialize(data); | |
} | |
public override void Serialize(IXunitSerializationInfo data) | |
{ | |
base.Serialize(data); | |
_settings.Serialize(data); | |
} | |
} | |
abstract class GeneratorProvider | |
{ | |
public abstract Generator ForType(Type type); | |
public static GeneratorProvider operator |(GeneratorProvider left, GeneratorProvider right) | |
=> left.OrFrom(right); | |
// hmmmmmmmm | |
private static readonly GeneratorProvider Instance = | |
new BuiltinGeneratorProvider() | new EnumGeneratorProvider(); | |
public static Generator InstanceForType(Type type) | |
=> Instance.ForType(type); | |
} | |
class BuiltinGeneratorProvider : GeneratorProvider | |
{ | |
private static readonly Dictionary<Type, Generator> Generators = | |
new Dictionary<Type, Generator> | |
{ | |
{ typeof(bool), new BoolGenerator() }, | |
{ typeof(int), new IntGenerator() }, | |
}; | |
public override Generator ForType(Type type) | |
{ | |
Generator generator; | |
Generators.TryGetValue(type, out generator); | |
return generator; | |
} | |
} | |
class EnumGeneratorProvider : GeneratorProvider | |
{ | |
public override Generator ForType(Type type) | |
{ | |
if (type.IsEnum) | |
{ | |
return new EnumGenerator(type); | |
} | |
return null; | |
} | |
} | |
internal static class GeneratorProviderExtensions | |
{ | |
public static GeneratorProvider OrFrom(this GeneratorProvider left, GeneratorProvider right) | |
=> new Combined(left, right); | |
private class Combined : GeneratorProvider | |
{ | |
private readonly GeneratorProvider _left; | |
private readonly GeneratorProvider _right; | |
public Combined(GeneratorProvider left, GeneratorProvider right) | |
{ | |
_left = left; | |
_right = right; | |
} | |
public override Generator ForType(Type type) | |
=> _left.ForType(type) ?? _right.ForType(type); | |
} | |
} | |
public abstract class Generator | |
{ | |
public abstract object GenerateObject(Random random); | |
public abstract int? PotentialValues(); | |
public abstract IEnumerable<object> AllObjects(); | |
public abstract IEnumerable<object> ShrinkObject(object o); | |
} | |
public abstract class Generator<T> : Generator | |
{ | |
public sealed override object GenerateObject(Random random) | |
=> Generate(random); | |
public sealed override IEnumerable<object> AllObjects() | |
=> AllValues().Cast<object>(); | |
public sealed override IEnumerable<object> ShrinkObject(object o) | |
=> Shrink((T)o).Cast<object>(); | |
protected abstract T Generate(Random random); | |
protected abstract IEnumerable<T> Shrink(T input); | |
protected abstract IEnumerable<T> AllValues(); | |
} | |
public sealed class BoolGenerator : Generator<bool> | |
{ | |
public override int? PotentialValues() => 2; | |
protected override bool Generate(Random random) | |
=> random.Next(2) == 0; | |
protected override IEnumerable<bool> Shrink(bool input) | |
{ | |
if (input) | |
{ | |
yield return false; | |
} | |
} | |
protected override IEnumerable<bool> AllValues() | |
{ | |
yield return false; | |
yield return true; | |
} | |
} | |
public sealed class EnumGenerator : Generator | |
{ | |
private readonly object[] _values; | |
public EnumGenerator(Type type) | |
{ | |
_values = type.GetEnumValues().Cast<object>().ToArray(); | |
if (_values.Length == 0) | |
{ | |
// UHOH | |
} | |
} | |
public override object GenerateObject(Random random) | |
{ | |
return _values[random.Next(0, _values.Length)]; | |
} | |
public override int? PotentialValues() => _values.Length; | |
public override IEnumerable<object> AllObjects() => _values; | |
public override IEnumerable<object> ShrinkObject(object o) | |
{ | |
var ix = Array.IndexOf(_values, o); | |
if (ix > 0) | |
{ | |
yield return _values[ix - 1]; | |
} | |
} | |
} | |
public sealed class IntGenerator : Generator<int> | |
{ | |
public override int? PotentialValues() => null; | |
protected override int Generate(Random random) => random.Next(); | |
protected override IEnumerable<int> Shrink(int input) | |
{ | |
// shrink in magnitude | |
if (input > 0) | |
{ | |
yield return input - 1; | |
} | |
else if (input < 0) | |
{ | |
yield return input + 1; | |
} | |
} | |
protected override IEnumerable<int> AllValues() | |
{ | |
throw new NotSupportedException(); | |
} | |
} | |
public class Tests | |
{ | |
[Property] | |
public void DeMorgans(bool x, bool y) | |
{ | |
Assert.Equal(!(x && y), !x || !y); | |
} | |
[Property] | |
public void DeMorgans2(bool x, bool y) | |
{ | |
Assert.Equal(!(x || y), !x && !y); | |
} | |
[Property] | |
public void EqualInts(int x, int y) | |
{ | |
Assert.Equal(x, y); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment