Skip to content

Instantly share code, notes, and snippets.

@kiwidev
Forked from Porges/cscheck_sketch.cs
Last active April 5, 2016 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kiwidev/d7ab6337b9fff616a32e0912cfac1bfc to your computer and use it in GitHub Desktop.
Save kiwidev/d7ab6337b9fff616a32e0912cfac1bfc to your computer and use it in GitHub Desktop.
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
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(IParameterInfo parameterInfo, ITypeInfo t)
{
if (parameterInfo is IReflectionParameterInfo)
{
var customGenerator = ((IReflectionParameterInfo) parameterInfo).ParameterInfo.CustomAttributes
.SingleOrDefault(x => typeof(CustomGeneratorAttribute).IsAssignableFrom(x.AttributeType));
if (customGenerator != null)
{
// build up the attribute
var attribute =
(CustomGeneratorAttribute) customGenerator.Constructor.Invoke(customGenerator.ConstructorArguments.Select(x => x.Value).ToArray());
if (customGenerator.NamedArguments != null)
{
foreach (var namedArgument in customGenerator.NamedArguments)
{
if (namedArgument.IsField)
{
((FieldInfo) namedArgument.MemberInfo).SetValue(attribute, namedArgument.TypedValue.Value);
}
else
{
((PropertyInfo) namedArgument.MemberInfo).SetValue(attribute, namedArgument.TypedValue.Value);
}
}
}
return attribute.BuildGenerator();
}
}
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, 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 => PropertyTestCaseDiscoverer.GetGenerator(p, p.ParameterType))
.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 BoundedIntGenerator : Generator<int>
{
private readonly int _maxValue;
private readonly int _minValue;
public BoundedIntGenerator(int minValue, int maxValue)
{
_maxValue = maxValue;
_minValue = minValue;
}
protected override IEnumerable<int> AllValues()
{
throw new NotSupportedException();
}
public override int? PotentialValues() => null;
// TODO: Work out how to include maxValue (make it inclusive)... possibly also make sure bounds are always tested
protected override int Generate(Random random) => random.Next(_minValue, _maxValue);
protected override IEnumerable<int> Shrink(int input)
{
// We want to move closer to zero while still staying within bounds
// Ideally also do it in a divide manner
// shrink in magnitude
if (input > 0)
{
if (input > _minValue)
{
yield return input - 1;
}
}
else if (input < 0)
{
if (input < _maxValue)
{
yield return input + 1;
}
}
}
}
public sealed class PositiveIntGenerator : BoundedIntGenerator
{
public PositiveIntGenerator(int maxValue, bool allowZero)
:base(allowZero ? 0 : 1, maxValue)
{
}
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class GeneratorAttribute : Attribute
{
public GeneratorAttribute(Type type, params object[] parameters)
{
}
}
public abstract class CustomGeneratorAttribute : Attribute
{
public abstract Generator BuildGenerator();
}
public class PositiveIntsAttribute : CustomGeneratorAttribute
{
public int MaxValue { get; set; } = int.MaxValue;
public bool AllowZero { get; set; }
public override Generator BuildGenerator() => new PositiveIntGenerator(MaxValue, AllowZero);
}
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);
}
[Property(Iterations = 50)]
public void PositiveInts([PositiveInts(AllowZero = false, MaxValue = 100)] int x)
{
Assert.True(x > 0);
Assert.True(x <= 100);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment