Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ jobs:
tests:
uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main
with:
coverage_assembly_filters: "+TestDataGenerator*;-*Tests*"
coverage_threshold: "100"
coverage_assembly_filters: "+DataGenerator*;-*Tests*"
coverage_threshold: "90"
4 changes: 2 additions & 2 deletions DataGenerator/Configuration/DataGenerationCustomization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace DataGenerator.Configuration;

internal sealed class DataGenerationCustomization(string locale = "ru", Action<Faker>? configureFaker = null, int recursionDepth = 2) : ICustomization
internal sealed class DataGenerationCustomization(string locale, int seed, Action<Faker>? configureFaker, int recursionDepth) : ICustomization
{
public void Customize(IFixture fixture)
{
Expand All @@ -13,6 +13,6 @@ public void Customize(IFixture fixture)
fixture.Behaviors.Remove(behavior);
}
fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth));
fixture.Customizations.Insert(0, new Core.DataGenerator(locale, configureFaker));
fixture.Customizations.Insert(0, new Core.DataGenerator(locale, seed, configureFaker));
}
}
1 change: 1 addition & 0 deletions DataGenerator/Configuration/FixtureFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static IFixture Create(string locale = "ru", int? seed = null, int recurs

fixture.Customize(new DataGenerationCustomization(
locale,
usedSeed,
faker => { faker.Random = new Randomizer(usedSeed); configureFaker?.Invoke(faker); },
recursionDepth));

Expand Down
56 changes: 47 additions & 9 deletions DataGenerator/Core/DataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ namespace DataGenerator.Core;

internal sealed class DataGenerator : ISpecimenBuilder
{
private const float NullProbability = 0.2f;

private readonly IReadOnlyList<ITypeDataGenerator> _generators;
private readonly Faker _faker;
private readonly NullabilityInfoContext _nullabilityContext;

public DataGenerator(string locale, Action<Faker>? configureFaker)
public DataGenerator(string locale, int seed, Action<Faker>? configureFaker)
{
_faker = new Faker(locale)
{
DateTimeReference = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc)
DateTimeReference = DateTime.UnixEpoch.AddSeconds(seed)
};
configureFaker?.Invoke(_faker);

Expand All @@ -36,6 +39,8 @@ public DataGenerator(string locale, Action<Faker>? configureFaker)
new EnumerableGenerator(_faker),
new DictionaryGenerator(_faker),
];

_nullabilityContext = new();
}

public object Create(object request, ISpecimenContext context)
Expand All @@ -57,6 +62,10 @@ private object CreateForProperty(PropertyInfo propertyInfo, ISpecimenContext con
var type = propertyInfo.PropertyType;
var name = propertyInfo.Name;

if (ShouldGenerateNull(propertyInfo, type))
{
return null!;
}
if (TryGenerateKnown(type, name, context, out var value))
{
return value!;
Expand All @@ -75,20 +84,49 @@ private object CreateForType(Type type, ISpecimenContext context)
return new NoSpecimen();
}

private bool TryGenerateKnown(Type type, string? name, ISpecimenContext context, out object? result)
private bool ShouldGenerateNull(PropertyInfo property, Type type)
{
if (TryGenerateNullForNullableValueType(type))
{
return true;
}
if (TryGenerateNullForNullableReferenceType(property, type))
{
return true;
}

return false;
}

private bool TryGenerateNullForNullableValueType(Type type)
{
var underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
if (_faker.Random.Bool(0.2f))
{
result = null;
return true;
}
return _faker.Random.Bool(NullProbability);
}

return false;
}

type = underlying;
private bool TryGenerateNullForNullableReferenceType(PropertyInfo property, Type type)
{
if (type.IsValueType)
{
return false;
}

var info = _nullabilityContext.Create(property);
if (info.ReadState == NullabilityState.Nullable)
{
return _faker.Random.Bool(NullProbability);
}

return false;
}

private bool TryGenerateKnown(Type type, string? name, ISpecimenContext context, out object? result)
{
foreach (var generator in _generators)
{
if (generator.TryGenerate(type, name, context, out result))
Expand Down
4 changes: 3 additions & 1 deletion DataGenerator/DataFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ public DataFacade(
int? seed = null,
Action<Faker>? configureFaker = null)
{
var normalizedDepth = Math.Max(recursionDepth, 1);

var scopeHash = string.IsNullOrEmpty(scope) ? 0 : CryptoHelper.GetHash(scope);
var usedSeed = (seed ?? FixtureFactory.DefaultSeed) ^ scopeHash;

Fixture = FixtureFactory.Create(locale, usedSeed, recursionDepth, configureFaker);
Fixture = FixtureFactory.Create(locale, usedSeed, normalizedDepth, configureFaker);
}

public T Create<T>()
Expand Down
5 changes: 5 additions & 0 deletions DataGenerator/Generators/Implementations/ArrayGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o
{
return false;
}
if (context.Resolve(elemType!) is OmitSpecimen or NoSpecimen)
{
value = Array.CreateInstance(elemType!, 0);
return true;
}

var count = faker.Random.Int(2, 6);
var array = Array.CreateInstance(elemType!, count);
Expand Down
8 changes: 4 additions & 4 deletions DataGenerator/Generators/Implementations/BoolGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ private bool GenerateSmartBool(string propertyName)
{
var name = propertyName.ToLowerInvariant();

if (name.StartsWith("is") || name.StartsWith("has") || name.Contains("enabled") || name.Contains("active"))
{
return faker.Random.Bool(0.7f);
}
if (name.Contains("deleted") || name.Contains("disabled") || name.Contains("blocked"))
{
return faker.Random.Bool(0.1f);
}
if (name.StartsWith("is") || name.StartsWith("has") || name.Contains("enabled") || name.Contains("active"))
{
return faker.Random.Bool(0.7f);
}

return faker.Random.Bool();
}
Expand Down
149 changes: 122 additions & 27 deletions DataGenerator/Generators/Implementations/DictionaryGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System.Collections.Concurrent;

using AutoFixture.Kernel;

Expand All @@ -18,45 +19,43 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o
{
return false;
}
if (ShouldReturnEmptyDictionary(keyType!, valueType!, context))
{
value = CreateEmptyDictionary(type, keyType!, valueType!);
return true;
}

var count = faker.Random.Int(2, 5);
var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType!, valueType!);
var dictionary = (IDictionary)Activator.CreateInstance(dictionaryType)!;
var dictionaryInstance = CreateEmptyDictionary(type, keyType!, valueType!)!;

Comment on lines 28 to 30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Casting generated dictionaries to non-generic IDictionary breaks added types

The generator now treats SortedDictionary<,> and ConcurrentDictionary<,> as supported, but the instance created in CreateEmptyDictionary is immediately cast to IDictionary. These concrete types do not implement the non-generic IDictionary interface, so requesting a SortedDictionary<TKey,TValue> or ConcurrentDictionary<TKey,TValue> will throw InvalidCastException at line 29 instead of returning a populated dictionary.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex поправил, посмотри пожалуйста

int safety = 0;
while (dictionary.Count < count && safety < count * 4)
if (dictionaryInstance.GetType().IsGenericType && dictionaryInstance.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>))
{
var key = GenerateKey(keyType!, context);
if (key is null)
{
safety++;
continue;
}

var val = context.Resolve(valueType!);

if (!dictionary.Contains(key))
{
dictionary.Add(key, val);
}

safety++;
FillConcurrent((dynamic)dictionaryInstance, keyType!, valueType!, context, count);
value = dictionaryInstance;
return true;
}
else
{
var dictionary = (IDictionary)dictionaryInstance;
FillDictionary(dictionary, keyType!, valueType!, context, count);
value = dictionary;
}
value = dictionary;

return true;
}

private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? valueType)
public static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? valueType)
{
keyType = valueType = null;

if (type.IsGenericType)
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(IDictionary<,>) ||
genericTypeDefinition == typeof(Dictionary<,>) ||
genericTypeDefinition == typeof(IReadOnlyDictionary<,>))
if (genericTypeDefinition == typeof(Dictionary<,>) ||
genericTypeDefinition == typeof(IDictionary<,>) ||
genericTypeDefinition == typeof(IReadOnlyDictionary<,>) ||
genericTypeDefinition == typeof(SortedDictionary<,>) ||
genericTypeDefinition == typeof(ConcurrentDictionary<,>))
{
var args = type.GetGenericArguments();
keyType = args[0];
Expand All @@ -75,6 +74,7 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type
var args = @interface.GetGenericArguments();
keyType = args[0];
valueType = args[1];

return true;
}
}
Expand All @@ -85,11 +85,13 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type

private object? GenerateKey(Type keyType, ISpecimenContext context)
{
var t = Nullable.GetUnderlyingType(keyType) ?? keyType;
var type = Nullable.GetUnderlyingType(keyType) ?? keyType;

var gen = CreateScalarGenerator(t);
if (gen is not null && gen.TryGenerate(t, name: null, context, out var value))
var generator = CreateScalarGenerator(type);
if (generator is not null && generator.TryGenerate(type, name: null, context, out var value))
{
return value;
}

return context.Resolve(keyType);
}
Expand Down Expand Up @@ -134,4 +136,97 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type

return null;
}

private static bool ShouldReturnEmptyDictionary(Type keyType, Type valueType, ISpecimenContext context)
{
var keyProbe = context.Resolve(keyType);
if (keyProbe is OmitSpecimen or NoSpecimen)
{
return true;
}

var valProbe = context.Resolve(valueType);
return valProbe is OmitSpecimen or NoSpecimen;
}

private static object CreateEmptyDictionary(Type requestType, Type keyType, Type valueType)
{
if (!requestType.IsInterface && !requestType.IsAbstract)
{
return Activator.CreateInstance(requestType)!;
}
if (requestType.GetGenericTypeDefinition() == typeof(SortedDictionary<,>))
{
return Activator.CreateInstance(typeof(SortedDictionary<,>).MakeGenericType(keyType, valueType))!;
}
if (requestType.GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>))
{
return Activator.CreateInstance(typeof(ConcurrentDictionary<,>).MakeGenericType(keyType, valueType))!;
}
if (requestType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
{
return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!;
}
if (requestType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!;
}

return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!;
}

private void FillDictionary(IDictionary dictionary, Type keyType, Type valueType, ISpecimenContext context, int count)
{
FillDictionaryCore(
getCount: () => dictionary.Count,
generateKey: () => GenerateKey(keyType, context),
generateValue: () => context.Resolve(valueType),
tryAdd: (key, value) =>
{
if (!dictionary.Contains(key))
{
dictionary.Add(key, value);
return true;
}
return false;
},
count: count
);
}

private void FillConcurrent<TKey, TValue>(ConcurrentDictionary<TKey, TValue> dictionary, Type keyType, Type valueType, ISpecimenContext context, int count)
where TKey : notnull
{
FillDictionaryCore(
getCount: () => dictionary.Count,
generateKey: () => GenerateKey(keyType, context),
generateValue: () => context.Resolve(valueType),
tryAdd: (key, value) => dictionary.TryAdd((TKey)key!, (TValue)value!),
count: count
);
}

private static void FillDictionaryCore(
Func<int> getCount,
Func<object?> generateKey,
Func<object?> generateValue,
Func<object, object?, bool> tryAdd,
int count)
{
int safety = 0;
while (getCount() < count && safety < count * 4)
{
var key = generateKey();
if (key is null)
{
safety++;
continue;
}

var value = generateValue();
tryAdd(key, value);

safety++;
}
}
}
Loading