Skip to content

Instantly share code, notes, and snippets.

@Porges
Created April 5, 2016 07:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Porges/342b809c4f0a6363b9e182a81798c858 to your computer and use it in GitHub Desktop.
Save Porges/342b809c4f0a6363b9e182a81798c858 to your computer and use it in GitHub Desktop.
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