From 48555dc61bc30c94d4bbf842fdb600c3bf2010cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 10:58:00 +0400 Subject: [PATCH 01/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B5=D0=BA=D1=82=D0=BE=D1=80?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20%=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/Tests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 68420ed..33d7ff4 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 32385ee56e92e5dcd81d8af865aa8b6356913045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 11:09:33 +0400 Subject: [PATCH 02/10] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c495edd..ebe6485 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,3 +8,9 @@ on: jobs: format: uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-format.yml@main + + tests: + uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main + with: + coverage_assembly_filters: "+DataGenerator*;-*Tests*" + coverage_threshold: "100" \ No newline at end of file From 91021582942e674ccb11c56915db3f1908f46f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 11:30:26 +0400 Subject: [PATCH 03/10] ci --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebe6485..7447c35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,4 +13,5 @@ jobs: uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main with: coverage_assembly_filters: "+DataGenerator*;-*Tests*" - coverage_threshold: "100" \ No newline at end of file + coverage_threshold: "100" + \ No newline at end of file From 42d7ca7f4037217cd8a70b12a2d572bc55023559 Mon Sep 17 00:00:00 2001 From: PANiXiDA <94934320+PANiXiDA@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:38:03 +0400 Subject: [PATCH 04/10] Remove redundant newline in ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7447c35..3984886 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,4 +14,3 @@ jobs: with: coverage_assembly_filters: "+DataGenerator*;-*Tests*" coverage_threshold: "100" - \ No newline at end of file From d00133f5392dffe771f30186ebf477d06f9701cd Mon Sep 17 00:00:00 2001 From: PANiXiDA <94934320+PANiXiDA@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:52:31 +0400 Subject: [PATCH 05/10] Update ci.yml From a1c9ccc41888a74cdcd06d9ce90c986930146b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 13:52:30 +0400 Subject: [PATCH 06/10] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B1=D0=B0=D0=B3=20=D1=81=D0=BE=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80=D1=8F=D0=BC=D0=B8=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D0=BC=D0=B8=20=D0=9D=D0=B0=D1=87=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D1=82=D1=8C=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataGenerationCustomization.cs | 4 +- DataGenerator/Configuration/FixtureFactory.cs | 1 + DataGenerator/Core/DataGenerator.cs | 56 ++++++++++++++--- .../Implementations/DictionaryGenerator.cs | 2 +- .../Implementations/EnumerableGenerator.cs | 4 ++ Tests/DataGeneratorTests.cs | 46 ++++++++++++++ Tests/DateTimeGeneratorTests.cs | 61 +++++++++++++++++++ Tests/MutateTests.cs | 32 +++++----- Tests/NullableTests.cs | 34 +++++++++++ Tests/TestDataFacadeTests.cs | 12 ++-- Tests/TestModels/DateTimeTestModel.cs | 20 ++++++ Tests/TestModels/NullableModel.cs | 7 +++ Tests/TestModels/SampleModel.cs | 34 +++++++++++ Tests/TestModels/User.cs | 17 ------ 14 files changed, 279 insertions(+), 51 deletions(-) create mode 100644 Tests/DataGeneratorTests.cs create mode 100644 Tests/DateTimeGeneratorTests.cs create mode 100644 Tests/NullableTests.cs create mode 100644 Tests/TestModels/DateTimeTestModel.cs create mode 100644 Tests/TestModels/NullableModel.cs create mode 100644 Tests/TestModels/SampleModel.cs delete mode 100644 Tests/TestModels/User.cs diff --git a/DataGenerator/Configuration/DataGenerationCustomization.cs b/DataGenerator/Configuration/DataGenerationCustomization.cs index bd8e8e8..de4021b 100644 --- a/DataGenerator/Configuration/DataGenerationCustomization.cs +++ b/DataGenerator/Configuration/DataGenerationCustomization.cs @@ -4,7 +4,7 @@ namespace DataGenerator.Configuration; -internal sealed class DataGenerationCustomization(string locale = "ru", Action? configureFaker = null, int recursionDepth = 2) : ICustomization +internal sealed class DataGenerationCustomization(string locale, int seed, Action? configureFaker, int recursionDepth) : ICustomization { public void Customize(IFixture fixture) { @@ -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)); } } diff --git a/DataGenerator/Configuration/FixtureFactory.cs b/DataGenerator/Configuration/FixtureFactory.cs index 9eb7be3..3841b0d 100644 --- a/DataGenerator/Configuration/FixtureFactory.cs +++ b/DataGenerator/Configuration/FixtureFactory.cs @@ -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)); diff --git a/DataGenerator/Core/DataGenerator.cs b/DataGenerator/Core/DataGenerator.cs index f02da3b..bcced65 100644 --- a/DataGenerator/Core/DataGenerator.cs +++ b/DataGenerator/Core/DataGenerator.cs @@ -11,14 +11,17 @@ namespace DataGenerator.Core; internal sealed class DataGenerator : ISpecimenBuilder { + private const float NullProbability = 0.2f; + private readonly IReadOnlyList _generators; private readonly Faker _faker; + private readonly NullabilityInfoContext _nullabilityContext; - public DataGenerator(string locale, Action? configureFaker) + public DataGenerator(string locale, int seed, Action? configureFaker) { _faker = new Faker(locale) { - DateTimeReference = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc) + DateTimeReference = DateTime.UnixEpoch.AddSeconds(seed) }; configureFaker?.Invoke(_faker); @@ -36,6 +39,8 @@ public DataGenerator(string locale, Action? configureFaker) new EnumerableGenerator(_faker), new DictionaryGenerator(_faker), ]; + + _nullabilityContext = new(); } public object Create(object request, ISpecimenContext context) @@ -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!; @@ -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)) diff --git a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs index 23a7c74..0248185 100644 --- a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs +++ b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs @@ -47,7 +47,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o 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; diff --git a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs index 0f8c5fc..69dd6c2 100644 --- a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs +++ b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs @@ -18,6 +18,10 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o { return false; } + if (DictionaryGenerator.TryGetDictionaryTypes(type, out var _, out var _)) + { + return false; + } if (!TryGetEnumerableElementType(type, out var elemType)) { return false; diff --git a/Tests/DataGeneratorTests.cs b/Tests/DataGeneratorTests.cs new file mode 100644 index 0000000..5ae627e --- /dev/null +++ b/Tests/DataGeneratorTests.cs @@ -0,0 +1,46 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests; + +public sealed class DataGeneratorTests +{ + [Fact(DisplayName = "Create should populate all supported property types")] + public void Create_PopulatesAllTypes() + { + var facade = new DataFacade(); + + var generated = facade.Create(); + + generated.ExternalId.Should().NotBe(Guid.Empty); + + generated.Login.Should().NotBeNullOrWhiteSpace(); + generated.Password.Should().NotBeNullOrWhiteSpace(); + + generated.Email.Should().Contain("@"); + generated.DisplayName.Should().NotBeNullOrWhiteSpace(); + + generated.Age.Should().BeInRange(18, 70); + + generated.CreatedAt.Should().BeBefore(DateTime.UtcNow); + generated.UpdatedAt.Should().BeBefore(DateTime.UtcNow); + + generated.AvatarUrl.Should().NotBeNull(); + + generated.Status.Should().NotBe(Sample.Unknown); + + generated.Scores.Should().NotBeNull(); + generated.Scores.Length.Should().BeGreaterThan(0); + + generated.Tags.Should().NotBeNull(); + generated.Tags.Should().NotBeEmpty(); + + generated.Metadata.Should().NotBeNull(); + generated.Metadata.Should().NotBeEmpty(); + } +} diff --git a/Tests/DateTimeGeneratorTests.cs b/Tests/DateTimeGeneratorTests.cs new file mode 100644 index 0000000..46dc8eb --- /dev/null +++ b/Tests/DateTimeGeneratorTests.cs @@ -0,0 +1,61 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests; + +public sealed class DateTimeGeneratorTests +{ + private static readonly DateTime Now = DateTime.UtcNow; + + [Fact(DisplayName = "DateTimeGenerator should generate dates based on naming conventions")] + public void Create_GeneratesExpectedDateTimeValues() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + generated.CreatedAt.Should().BeBefore(Now); + generated.UpdatedAt.Should().BeBefore(Now); + generated.ModifiedAt.Should().BeBefore(Now); + + if (generated.DeletedAt is not null) + { + generated.DeletedAt.Value.Should().BeBefore(Now); + } + + (Now.Year - generated.Birthday.Year).Should().BeInRange(18, 120); + (Now.Year - generated.Dob.Year).Should().BeInRange(18, 120); + + generated.RegisteredAt.Should().BeBefore(Now); + generated.SignupDate.Should().BeBefore(Now); + } + + [Fact(DisplayName = "DateTimeGenerator should generate TimeSpan values")] + public void Create_GeneratesTimeSpan() + { + var generated = new DataFacade().Create(); + + generated.Duration.Should().NotBe(default); + generated.Duration.TotalSeconds.Should().BeGreaterThan(0); + } + + [Fact(DisplayName = "DateTimeGenerator should generate DateOnly values")] + public void Create_GeneratesDateOnly() + { + var generated = new DataFacade().Create(); + generated.BirthDateOnly.Should().NotBe(default); + } + + [Fact(DisplayName = "DateTimeGenerator should generate TimeOnly values")] + public void Create_GeneratesTimeOnly() + { + var generated = new DataFacade().Create(); + + generated.WakeUpTime.Hour.Should().BeInRange(0, 23); + generated.WakeUpTime.Minute.Should().BeInRange(0, 59); + } +} diff --git a/Tests/MutateTests.cs b/Tests/MutateTests.cs index 8d07c42..65fc665 100644 --- a/Tests/MutateTests.cs +++ b/Tests/MutateTests.cs @@ -14,47 +14,47 @@ public sealed class MutateTests public void Mutate_ChangesAtLeastOneProperty() { var facade = new DataFacade(); - var user = facade.Create(); + var generated = facade.Create(); - var mutated = facade.Mutate(user); + var mutated = facade.Mutate(generated); - mutated.Should().NotBeEquivalentTo(user); + mutated.Should().NotBeEquivalentTo(generated); } [Fact(DisplayName = "Mutate should not change system fields (Id, CreatedAt, UpdatedAt, DeletedAt)")] public void Mutate_DoesNotChangeSystemFields() { var facade = new DataFacade(); - var user = facade.Create(); + var generated = facade.Create(); - var mutated = facade.Mutate(user); + var mutated = facade.Mutate(generated); - mutated.Id.Should().Be(user.Id); - mutated.CreatedAt.Should().Be(user.CreatedAt); - mutated.UpdatedAt.Should().Be(user.UpdatedAt); - mutated.DeletedAt.Should().Be(user.DeletedAt); + mutated.Id.Should().Be(generated.Id); + mutated.CreatedAt.Should().Be(generated.CreatedAt); + mutated.UpdatedAt.Should().Be(generated.UpdatedAt); + mutated.DeletedAt.Should().Be(generated.DeletedAt); } [Fact(DisplayName = "Mutate should respect explicitly excluded properties")] public void Mutate_RespectsExcludeProperties() { var facade = new DataFacade(); - var user = facade.Create(); + var generated = facade.Create(); - var mutated = facade.Mutate(user, excludeProperties: [nameof(User.Email)]); + var mutated = facade.Mutate(generated, excludeProperties: [nameof(SampleModel.Email)]); - mutated.Email.Should().Be(user.Email); + mutated.Email.Should().Be(generated.Email); } [Fact(DisplayName = "Mutate should always change Login and Password")] public void Mutate_AlwaysChangesLoginAndPassword() { var facade = new DataFacade(); - var user = facade.Create(); + var generated = facade.Create(); - var mutated = facade.Mutate(user); + var mutated = facade.Mutate(generated); - mutated.Login.Should().NotBe(user.Login); - mutated.Password.Should().NotBe(user.Password); + mutated.Login.Should().NotBe(generated.Login); + mutated.Password.Should().NotBe(generated.Password); } } diff --git a/Tests/NullableTests.cs b/Tests/NullableTests.cs new file mode 100644 index 0000000..0462c18 --- /dev/null +++ b/Tests/NullableTests.cs @@ -0,0 +1,34 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests; + +public sealed class NullableTests +{ + [Fact(DisplayName = "Nullable fields should sometimes be null and sometimes have values")] + public void Nullable_Generation_CanBeNull_And_NotNull() + { + var facade = new DataFacade(); + + var middleNames = new List(); + var deletedDates = new List(); + + for (int i = 0; i < 10; i++) + { + var generated = facade.Create(); + middleNames.Add(generated.MiddleName); + deletedDates.Add(generated.DeletedAt); + } + + middleNames.Should().Contain(x => x == null); + middleNames.Should().Contain(x => x != null); + + deletedDates.Should().Contain(x => x == null); + deletedDates.Should().Contain(x => x != null); + } +} diff --git a/Tests/TestDataFacadeTests.cs b/Tests/TestDataFacadeTests.cs index 2e4730d..c8d008e 100644 --- a/Tests/TestDataFacadeTests.cs +++ b/Tests/TestDataFacadeTests.cs @@ -16,10 +16,10 @@ public void Create_WithSameSeed_ProducesSameData() var a = new DataFacade(seed: 123); var b = new DataFacade(seed: 123); - var user1 = a.Create(); - var user2 = b.Create(); + var generated1 = a.Create(); + var generated2 = b.Create(); - user1.Should().BeEquivalentTo(user2); + generated1.Should().BeEquivalentTo(generated2); } [Fact(DisplayName = "Different seeds should produce different results")] @@ -28,9 +28,9 @@ public void Create_WithDifferentSeed_ProducesDifferentData() var a = new DataFacade(seed: 1); var b = new DataFacade(seed: 2); - var user1 = a.Create(); - var user2 = b.Create(); + var generated1 = a.Create(); + var generated2 = b.Create(); - user1.Should().NotBeEquivalentTo(user2); + generated1.Should().NotBeEquivalentTo(generated2); } } diff --git a/Tests/TestModels/DateTimeTestModel.cs b/Tests/TestModels/DateTimeTestModel.cs new file mode 100644 index 0000000..d6e55f7 --- /dev/null +++ b/Tests/TestModels/DateTimeTestModel.cs @@ -0,0 +1,20 @@ +namespace Tests.TestModels; + +public sealed class DateTimeTestModel +{ + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime ModifiedAt { get; set; } + public DateTime? DeletedAt { get; set; } + + public DateTime Birthday { get; set; } + public DateTime Dob { get; set; } + + public DateTime RegisteredAt { get; set; } + public DateTime SignupDate { get; set; } + + public TimeSpan Duration { get; set; } + + public DateOnly BirthDateOnly { get; set; } + public TimeOnly WakeUpTime { get; set; } +} diff --git a/Tests/TestModels/NullableModel.cs b/Tests/TestModels/NullableModel.cs new file mode 100644 index 0000000..91763e4 --- /dev/null +++ b/Tests/TestModels/NullableModel.cs @@ -0,0 +1,7 @@ +namespace Tests.TestModels; + +public sealed class NullableModel +{ + public string? MiddleName { get; set; } + public DateTime? DeletedAt { get; set; } +} diff --git a/Tests/TestModels/SampleModel.cs b/Tests/TestModels/SampleModel.cs new file mode 100644 index 0000000..dc53a78 --- /dev/null +++ b/Tests/TestModels/SampleModel.cs @@ -0,0 +1,34 @@ +using DataGenerator.MutationProperties; + +namespace Tests.TestModels; + +public sealed class SampleModel : IMutationIgnoreProperties, IMutationForceProperties +{ + public required int Id { get; set; } + public required DateTime CreatedAt { get; set; } + public required DateTime UpdatedAt { get; set; } + public DateTime? DeletedAt { get; set; } + + public required Guid ExternalId { get; set; } + public required string Login { get; set; } + public required string Password { get; set; } + public required string Email { get; set; } + public required string DisplayName { get; set; } + public required int Age { get; set; } + public required bool IsActive { get; set; } + public required Uri AvatarUrl { get; set; } + public required Sample Status { get; set; } + + public required int[] Scores { get; set; } = []; + public required List Tags { get; set; } = []; + public required Dictionary Metadata { get; set; } = []; + public required IDictionary Metadata2 { get; set; } + public required IReadOnlyDictionary Metadata3 { get; set; } +} + +public enum Sample +{ + Unknown = 0, + Active, + Disabled +} \ No newline at end of file diff --git a/Tests/TestModels/User.cs b/Tests/TestModels/User.cs deleted file mode 100644 index 50372d1..0000000 --- a/Tests/TestModels/User.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DataGenerator.MutationProperties; - -namespace Tests.TestModels; - -public sealed class User : IMutationIgnoreProperties, IMutationForceProperties -{ - public int Id { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public DateTime? DeletedAt { get; set; } - - public required string Login { get; set; } - public required string Password { get; set; } - - public required string Email { get; set; } - public required string DisplayName { get; set; } -} From 4dec23d7f52d1237858bc74bc29200b4eba52560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 16:34:02 +0400 Subject: [PATCH 07/10] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D1=88=D0=B8=D0=B5=D1=81?= =?UTF-8?q?=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF=D0=BE=20=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=D0=BC=20?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D0=B3?= =?UTF-8?q?=D0=BB=D1=83=D0=B1=D0=B8=D0=BD=D1=83=20=D0=B3=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataGenerator/DataFacade.cs | 4 +- .../Implementations/ArrayGenerator.cs | 5 + .../Implementations/BoolGenerator.cs | 8 +- .../Implementations/DictionaryGenerator.cs | 56 +++++- .../Implementations/EnumerableGenerator.cs | 21 ++- .../Implementations/NumericGenerator.cs | 12 +- Tests/DataGeneratorTests.cs | 46 ----- Tests/TestModels/ArrayTestModel.cs | 21 +++ Tests/TestModels/BoolTestModel.cs | 12 ++ Tests/TestModels/CharTestModel.cs | 8 + Tests/TestModels/Common/Enums/Basic.cs | 9 + Tests/TestModels/Common/Enums/NoZero.cs | 8 + Tests/TestModels/Common/Enums/Sample.cs | 8 + Tests/TestModels/Common/Enums/Weird.cs | 8 + Tests/TestModels/Common/SimpleItem.cs | 6 + Tests/TestModels/DateTimeTestModel.cs | 22 +-- Tests/TestModels/DictionaryTestModel.cs | 23 +++ Tests/TestModels/EnumTestModel.cs | 10 + Tests/TestModels/EnumerableTestModel.cs | 27 +++ Tests/TestModels/GuidTestModel.cs | 8 + Tests/TestModels/NumericTestModel.cs | 26 +++ Tests/TestModels/SampleModel.cs | 35 ++-- Tests/TestModels/StringTestModel.cs | 52 ++++++ Tests/TestModels/UriTestModel.cs | 8 + Tests/Tests/ArrayGeneratorTests.cs | 148 +++++++++++++++ Tests/Tests/BoolGeneratorTests.cs | 53 ++++++ Tests/Tests/CharGeneratorTests.cs | 58 ++++++ Tests/Tests/DataGeneratorRecursionTests.cs | 114 ++++++++++++ Tests/{ => Tests}/DateTimeGeneratorTests.cs | 2 +- Tests/Tests/DictionaryGeneratorTests.cs | 153 +++++++++++++++ Tests/Tests/EnumGeneratorTests.cs | 50 +++++ Tests/Tests/EnumerableGeneratorTests.cs | 176 ++++++++++++++++++ Tests/Tests/GuidGeneratorTests.cs | 34 ++++ Tests/{ => Tests}/MutateTests.cs | 2 +- Tests/{ => Tests}/NullableTests.cs | 10 +- Tests/Tests/NumericGeneratorTests.cs | 63 +++++++ Tests/Tests/StringGeneratorTests.cs | 67 +++++++ Tests/{ => Tests}/TestDataFacadeTests.cs | 2 +- Tests/Tests/UriGeneratorTests.cs | 26 +++ 39 files changed, 1304 insertions(+), 97 deletions(-) delete mode 100644 Tests/DataGeneratorTests.cs create mode 100644 Tests/TestModels/ArrayTestModel.cs create mode 100644 Tests/TestModels/BoolTestModel.cs create mode 100644 Tests/TestModels/CharTestModel.cs create mode 100644 Tests/TestModels/Common/Enums/Basic.cs create mode 100644 Tests/TestModels/Common/Enums/NoZero.cs create mode 100644 Tests/TestModels/Common/Enums/Sample.cs create mode 100644 Tests/TestModels/Common/Enums/Weird.cs create mode 100644 Tests/TestModels/Common/SimpleItem.cs create mode 100644 Tests/TestModels/DictionaryTestModel.cs create mode 100644 Tests/TestModels/EnumTestModel.cs create mode 100644 Tests/TestModels/EnumerableTestModel.cs create mode 100644 Tests/TestModels/GuidTestModel.cs create mode 100644 Tests/TestModels/NumericTestModel.cs create mode 100644 Tests/TestModels/StringTestModel.cs create mode 100644 Tests/TestModels/UriTestModel.cs create mode 100644 Tests/Tests/ArrayGeneratorTests.cs create mode 100644 Tests/Tests/BoolGeneratorTests.cs create mode 100644 Tests/Tests/CharGeneratorTests.cs create mode 100644 Tests/Tests/DataGeneratorRecursionTests.cs rename Tests/{ => Tests}/DateTimeGeneratorTests.cs (98%) create mode 100644 Tests/Tests/DictionaryGeneratorTests.cs create mode 100644 Tests/Tests/EnumGeneratorTests.cs create mode 100644 Tests/Tests/EnumerableGeneratorTests.cs create mode 100644 Tests/Tests/GuidGeneratorTests.cs rename Tests/{ => Tests}/MutateTests.cs (98%) rename Tests/{ => Tests}/NullableTests.cs (71%) create mode 100644 Tests/Tests/NumericGeneratorTests.cs create mode 100644 Tests/Tests/StringGeneratorTests.cs rename Tests/{ => Tests}/TestDataFacadeTests.cs (97%) create mode 100644 Tests/Tests/UriGeneratorTests.cs diff --git a/DataGenerator/DataFacade.cs b/DataGenerator/DataFacade.cs index 2c68af7..6f7dc80 100644 --- a/DataGenerator/DataFacade.cs +++ b/DataGenerator/DataFacade.cs @@ -25,10 +25,12 @@ public DataFacade( int? seed = null, Action? 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() diff --git a/DataGenerator/Generators/Implementations/ArrayGenerator.cs b/DataGenerator/Generators/Implementations/ArrayGenerator.cs index 2c1614a..2a78f9d 100644 --- a/DataGenerator/Generators/Implementations/ArrayGenerator.cs +++ b/DataGenerator/Generators/Implementations/ArrayGenerator.cs @@ -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); diff --git a/DataGenerator/Generators/Implementations/BoolGenerator.cs b/DataGenerator/Generators/Implementations/BoolGenerator.cs index 9883db5..c9601c5 100644 --- a/DataGenerator/Generators/Implementations/BoolGenerator.cs +++ b/DataGenerator/Generators/Implementations/BoolGenerator.cs @@ -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(); } diff --git a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs index 0248185..7735deb 100644 --- a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs +++ b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using AutoFixture.Kernel; @@ -18,10 +19,14 @@ 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 dictionary = (IDictionary)CreateEmptyDictionary(type, keyType!, valueType!)!; int safety = 0; while (dictionary.Count < count && safety < count * 4) @@ -54,9 +59,11 @@ public static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? 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]; @@ -75,6 +82,7 @@ public static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? var args = @interface.GetGenericArguments(); keyType = args[0]; valueType = args[1]; + return true; } } @@ -85,11 +93,13 @@ public 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); } @@ -134,4 +144,34 @@ public 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(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))!; + } } diff --git a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs index 69dd6c2..f0aeec5 100644 --- a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs +++ b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs @@ -26,6 +26,11 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o { return false; } + if (ShouldReturnEmptyCollection(elemType!, context)) + { + value = CreateEmptyListOf(elemType!); + return true; + } var count = faker.Random.Int(2, 6); var listType = typeof(List<>).MakeGenericType(elemType!); @@ -34,7 +39,10 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o for (int i = 0; i < count; i++) { var item = context.Resolve(elemType!); - if (item is null && !elemType!.IsValueType) continue; + if (item is null && !elemType!.IsValueType) + { + continue; + } list.Add(item); } value = list; @@ -74,4 +82,15 @@ private static bool TryGetEnumerableElementType(Type type, out Type? elementType return false; } + + private static bool ShouldReturnEmptyCollection(Type elemType, ISpecimenContext context) + { + var probe = context.Resolve(elemType); + return probe is OmitSpecimen or NoSpecimen; + } + + private static object CreateEmptyListOf(Type elemType) + { + return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elemType))!; + } } diff --git a/DataGenerator/Generators/Implementations/NumericGenerator.cs b/DataGenerator/Generators/Implementations/NumericGenerator.cs index ae5180d..c04760a 100644 --- a/DataGenerator/Generators/Implementations/NumericGenerator.cs +++ b/DataGenerator/Generators/Implementations/NumericGenerator.cs @@ -17,7 +17,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(uint)) { - value = name is not null ? GenerateSmartInt(name) : faker.Random.Int(0, 10_000); + value = name is not null ? (uint)GenerateSmartInt(name) : faker.Random.UInt(0, 10_000); return true; } if (type == typeof(long)) @@ -27,17 +27,17 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(ulong)) { - value = (ulong)faker.Random.Long(0, long.MaxValue); + value = faker.Random.ULong(0, long.MaxValue); return true; } if (type == typeof(short)) { - value = (short)faker.Random.Int(short.MinValue, short.MaxValue); + value = faker.Random.Short(short.MinValue, short.MaxValue); return true; } if (type == typeof(ushort)) { - value = (ushort)faker.Random.Int(0, ushort.MaxValue); + value = faker.Random.UShort(0, ushort.MaxValue); return true; } if (type == typeof(byte)) @@ -47,7 +47,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(sbyte)) { - value = (sbyte)faker.Random.Int(sbyte.MinValue, sbyte.MaxValue); + value = faker.Random.SByte(sbyte.MinValue, sbyte.MaxValue); return true; } if (type == typeof(double)) @@ -57,7 +57,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(float)) { - value = (float)Math.Round(faker.Random.Double(0, 100_000), 4); + value = (float)Math.Round(faker.Random.Float(0, 100_000), 4); return true; } if (type == typeof(decimal)) diff --git a/Tests/DataGeneratorTests.cs b/Tests/DataGeneratorTests.cs deleted file mode 100644 index 5ae627e..0000000 --- a/Tests/DataGeneratorTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using DataGenerator; - -using FluentAssertions; - -using Tests.TestModels; - -using Xunit; - -namespace Tests; - -public sealed class DataGeneratorTests -{ - [Fact(DisplayName = "Create should populate all supported property types")] - public void Create_PopulatesAllTypes() - { - var facade = new DataFacade(); - - var generated = facade.Create(); - - generated.ExternalId.Should().NotBe(Guid.Empty); - - generated.Login.Should().NotBeNullOrWhiteSpace(); - generated.Password.Should().NotBeNullOrWhiteSpace(); - - generated.Email.Should().Contain("@"); - generated.DisplayName.Should().NotBeNullOrWhiteSpace(); - - generated.Age.Should().BeInRange(18, 70); - - generated.CreatedAt.Should().BeBefore(DateTime.UtcNow); - generated.UpdatedAt.Should().BeBefore(DateTime.UtcNow); - - generated.AvatarUrl.Should().NotBeNull(); - - generated.Status.Should().NotBe(Sample.Unknown); - - generated.Scores.Should().NotBeNull(); - generated.Scores.Length.Should().BeGreaterThan(0); - - generated.Tags.Should().NotBeNull(); - generated.Tags.Should().NotBeEmpty(); - - generated.Metadata.Should().NotBeNull(); - generated.Metadata.Should().NotBeEmpty(); - } -} diff --git a/Tests/TestModels/ArrayTestModel.cs b/Tests/TestModels/ArrayTestModel.cs new file mode 100644 index 0000000..2f2309a --- /dev/null +++ b/Tests/TestModels/ArrayTestModel.cs @@ -0,0 +1,21 @@ +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class ArrayTestModel +{ + public required int[] Ints { get; set; } = []; + public required double[] Doubles { get; set; } = []; + public required bool[] Bools { get; set; } = []; + public required char[] Chars { get; set; } = []; + public required Guid[] Guids { get; set; } = []; + public required Uri[] Uris { get; set; } = []; + public required DateTime[] Dates { get; set; } = []; + public required DateOnly[] DateOnlys { get; set; } = []; + public required TimeOnly[] TimeOnlys { get; set; } = []; + public required TimeSpan[] TimeSpans { get; set; } = []; + public required Sample[] Enums { get; set; } = []; + public required string[] Strings { get; set; } = []; + public required SimpleItem[] Items { get; set; } = []; +} diff --git a/Tests/TestModels/BoolTestModel.cs b/Tests/TestModels/BoolTestModel.cs new file mode 100644 index 0000000..6300c5a --- /dev/null +++ b/Tests/TestModels/BoolTestModel.cs @@ -0,0 +1,12 @@ +namespace Tests.TestModels; + +public sealed class BoolTestModel +{ + public required bool IsActive { get; set; } + public required bool HasAccess { get; set; } + public required bool FeatureEnabled { get; set; } + public required bool IsDeleted { get; set; } + public required bool IsDisabled { get; set; } + public required bool IsBlocked { get; set; } + public required bool RandomFlag { get; set; } +} diff --git a/Tests/TestModels/CharTestModel.cs b/Tests/TestModels/CharTestModel.cs new file mode 100644 index 0000000..9781787 --- /dev/null +++ b/Tests/TestModels/CharTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class CharTestModel +{ + public required char Letter { get; set; } + public required char Digit { get; set; } + public required char Any { get; set; } +} diff --git a/Tests/TestModels/Common/Enums/Basic.cs b/Tests/TestModels/Common/Enums/Basic.cs new file mode 100644 index 0000000..574c4d4 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Basic.cs @@ -0,0 +1,9 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Basic +{ + Unknown = 0, + First, + Second, + Third +} diff --git a/Tests/TestModels/Common/Enums/NoZero.cs b/Tests/TestModels/Common/Enums/NoZero.cs new file mode 100644 index 0000000..88550d4 --- /dev/null +++ b/Tests/TestModels/Common/Enums/NoZero.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum NoZero +{ + A = 1, + B = 2, + C = 3 +} diff --git a/Tests/TestModels/Common/Enums/Sample.cs b/Tests/TestModels/Common/Enums/Sample.cs new file mode 100644 index 0000000..b480227 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Sample.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Sample +{ + Unknown = 0, + Active, + Disabled +} diff --git a/Tests/TestModels/Common/Enums/Weird.cs b/Tests/TestModels/Common/Enums/Weird.cs new file mode 100644 index 0000000..753b7a8 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Weird.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Weird +{ + Zero = 0, + Ten = 10, + Minus = -5 +} diff --git a/Tests/TestModels/Common/SimpleItem.cs b/Tests/TestModels/Common/SimpleItem.cs new file mode 100644 index 0000000..b8b9871 --- /dev/null +++ b/Tests/TestModels/Common/SimpleItem.cs @@ -0,0 +1,6 @@ +namespace Tests.TestModels.Common; + +public sealed class SimpleItem +{ + public required string Name { get; set; } +} diff --git a/Tests/TestModels/DateTimeTestModel.cs b/Tests/TestModels/DateTimeTestModel.cs index d6e55f7..ca1be47 100644 --- a/Tests/TestModels/DateTimeTestModel.cs +++ b/Tests/TestModels/DateTimeTestModel.cs @@ -2,19 +2,19 @@ public sealed class DateTimeTestModel { - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public DateTime ModifiedAt { get; set; } - public DateTime? DeletedAt { get; set; } + public required DateTime CreatedAt { get; set; } + public required DateTime UpdatedAt { get; set; } + public required DateTime ModifiedAt { get; set; } + public required DateTime? DeletedAt { get; set; } - public DateTime Birthday { get; set; } - public DateTime Dob { get; set; } + public required DateTime Birthday { get; set; } + public required DateTime Dob { get; set; } - public DateTime RegisteredAt { get; set; } - public DateTime SignupDate { get; set; } + public required DateTime RegisteredAt { get; set; } + public required DateTime SignupDate { get; set; } - public TimeSpan Duration { get; set; } + public required TimeSpan Duration { get; set; } - public DateOnly BirthDateOnly { get; set; } - public TimeOnly WakeUpTime { get; set; } + public required DateOnly BirthDateOnly { get; set; } + public required TimeOnly WakeUpTime { get; set; } } diff --git a/Tests/TestModels/DictionaryTestModel.cs b/Tests/TestModels/DictionaryTestModel.cs new file mode 100644 index 0000000..5a84d37 --- /dev/null +++ b/Tests/TestModels/DictionaryTestModel.cs @@ -0,0 +1,23 @@ +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class DictionaryTestModel +{ + public required Dictionary IntStringDictionary { get; set; } = []; + public required Dictionary DoubleStringDictionary { get; set; } = []; + public required Dictionary StringGuidDictionary { get; set; } = []; + public required Dictionary CharBoolDictionary { get; set; } = []; + public required Dictionary BoolDoubleDictionary { get; set; } = []; + public required Dictionary UriIntDictionary { get; set; } = []; + public required Dictionary DateStringDictionary { get; set; } = []; + public required Dictionary DateOnlyStringDictionary { get; set; } = []; + public required Dictionary TimeOnlyStringDictionary { get; set; } = []; + public required Dictionary TimeSpanStringDictionary { get; set; } = []; + public required Dictionary EnumCharDictionary { get; set; } = []; + public required Dictionary StringComplexDictionary { get; set; } = []; + + public required IDictionary GuidIntDictionary { get; set; } = new Dictionary(); + public required IReadOnlyDictionary StringBoolDictionary { get; set; } = new Dictionary(); +} diff --git a/Tests/TestModels/EnumTestModel.cs b/Tests/TestModels/EnumTestModel.cs new file mode 100644 index 0000000..a15049b --- /dev/null +++ b/Tests/TestModels/EnumTestModel.cs @@ -0,0 +1,10 @@ +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class EnumTestModel +{ + public required Basic Basic { get; set; } + public required NoZero NoZero { get; set; } + public required Weird Weird { get; set; } +} diff --git a/Tests/TestModels/EnumerableTestModel.cs b/Tests/TestModels/EnumerableTestModel.cs new file mode 100644 index 0000000..836c613 --- /dev/null +++ b/Tests/TestModels/EnumerableTestModel.cs @@ -0,0 +1,27 @@ +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class EnumerableTestModel +{ + public required IEnumerable IntEnumerable { get; set; } = []; + public required IEnumerable DoubleEnumerable { get; set; } = []; + public required IEnumerable BoolEnumerable { get; set; } = []; + public required IEnumerable CharEnumerable { get; set; } = []; + public required IEnumerable GuidEnumerable { get; set; } = []; + public required IEnumerable UriEnumerable { get; set; } = []; + public required IEnumerable DateEnumerable { get; set; } = []; + public required IEnumerable DateOnlyEnumerable { get; set; } = []; + public required IEnumerable TimeOnlyEnumerable { get; set; } = []; + public required IEnumerable TimeSpanEnumerable { get; set; } = []; + public required IEnumerable EnumEnumerable { get; set; } = []; + public required IEnumerable StringEnumerable { get; set; } = []; + public required IEnumerable ItemEnumerable { get; set; } = []; + + public required ICollection StringCollection { get; set; } = []; + public required IList GuidList { get; set; } = []; + public required IReadOnlyCollection BoolReadOnlyCollection { get; set; } = []; + public required IReadOnlyList EnumReadOnlyList { get; set; } = []; + public required List ItemList { get; set; } = []; +} diff --git a/Tests/TestModels/GuidTestModel.cs b/Tests/TestModels/GuidTestModel.cs new file mode 100644 index 0000000..3a60d98 --- /dev/null +++ b/Tests/TestModels/GuidTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class GuidTestModel +{ + public required Guid Id { get; set; } + public required Guid ExternalId { get; set; } + public required Guid CorrelationId { get; set; } +} diff --git a/Tests/TestModels/NumericTestModel.cs b/Tests/TestModels/NumericTestModel.cs new file mode 100644 index 0000000..b10542b --- /dev/null +++ b/Tests/TestModels/NumericTestModel.cs @@ -0,0 +1,26 @@ +namespace Tests.TestModels; + +public sealed class NumericTestModel +{ + public required int Id { get; set; } + public required int UserId { get; set; } + public required int Age { get; set; } + public required int ItemCount { get; set; } + public required int Qty { get; set; } + public required int TotalNum { get; set; } + public required int Price { get; set; } + public required int OrderAmount { get; set; } + public required int TotalSum { get; set; } + public required int ReleaseYear { get; set; } + + public required uint UIntValue { get; set; } + public required long LongValue { get; set; } + public required ulong ULongValue { get; set; } + public required short ShortValue { get; set; } + public required ushort UShortValue { get; set; } + public required byte ByteValue { get; set; } + public required sbyte SByteValue { get; set; } + public required double DoubleValue { get; set; } + public required float FloatValue { get; set; } + public required decimal DecimalValue { get; set; } +} diff --git a/Tests/TestModels/SampleModel.cs b/Tests/TestModels/SampleModel.cs index dc53a78..a76b64a 100644 --- a/Tests/TestModels/SampleModel.cs +++ b/Tests/TestModels/SampleModel.cs @@ -1,4 +1,8 @@ -using DataGenerator.MutationProperties; +using System.Collections.Concurrent; + +using DataGenerator.MutationProperties; + +using Tests.TestModels.Common.Enums; namespace Tests.TestModels; @@ -9,9 +13,10 @@ public sealed class SampleModel : IMutationIgnoreProperties, IMutationForce public required DateTime UpdatedAt { get; set; } public DateTime? DeletedAt { get; set; } - public required Guid ExternalId { get; set; } public required string Login { get; set; } public required string Password { get; set; } + + public required Guid ExternalId { get; set; } public required string Email { get; set; } public required string DisplayName { get; set; } public required int Age { get; set; } @@ -20,15 +25,21 @@ public sealed class SampleModel : IMutationIgnoreProperties, IMutationForce public required Sample Status { get; set; } public required int[] Scores { get; set; } = []; - public required List Tags { get; set; } = []; - public required Dictionary Metadata { get; set; } = []; - public required IDictionary Metadata2 { get; set; } - public required IReadOnlyDictionary Metadata3 { get; set; } -} -public enum Sample -{ - Unknown = 0, - Active, - Disabled + public required SampleModel Parent { get; set; } = null!; + + public required SampleModel[] Siblings { get; set; } = []; + + public required List Children { get; set; } = []; + public required IEnumerable EnumerableChildren { get; set; } = []; + public required ICollection CollectionChildren { get; set; } = []; + public required IList ListChildren { get; set; } = []; + public required IReadOnlyCollection ReadOnlyCollectionChildren { get; set; } = []; + public required IReadOnlyList ReadOnlyListChildren { get; set; } = []; + + public required Dictionary Related { get; set; } = []; + public required IReadOnlyDictionary ReadOnlyRelated { get; set; } = new Dictionary(); + public required IDictionary InterfaceDictionaryRelated { get; set; } = new Dictionary(); + public required SortedDictionary SortedRelated { get; set; } = []; + public required ConcurrentDictionary ConcurrentRelated { get; set; } = []; } \ No newline at end of file diff --git a/Tests/TestModels/StringTestModel.cs b/Tests/TestModels/StringTestModel.cs new file mode 100644 index 0000000..6339306 --- /dev/null +++ b/Tests/TestModels/StringTestModel.cs @@ -0,0 +1,52 @@ +namespace Tests.TestModels; + +public sealed class StringTestModel +{ + public required string EmailAddress { get; set; } + + public required string Phone { get; set; } + public required string MobilePhone { get; set; } + + public required string FullName { get; set; } + public required string Name { get; set; } + public required string Fio { get; set; } + + public required string FirstName { get; set; } + public required string LastName { get; set; } + public required string Surname { get; set; } + + public required string Username { get; set; } + public required string Login { get; set; } + + public required string Password { get; set; } + public required string PwdHash { get; set; } + + public required string WebsiteUrl { get; set; } + public required string ProfileLink { get; set; } + + public required string Avatar { get; set; } + + public required string Country { get; set; } + public required string City { get; set; } + public required string StreetAddress { get; set; } + + public required string Postcode { get; set; } + public required string PostalCode { get; set; } + public required string ZipCode { get; set; } + + public required string Title { get; set; } + public required string SubjectTitle { get; set; } + + public required string Description { get; set; } + public required string MessageText { get; set; } + public required string ContentString { get; set; } + + public required string Company { get; set; } + + public required string Iban { get; set; } + public required string AccountNumber { get; set; } + + public required string Currency { get; set; } + + public required string RandomString { get; set; } +} diff --git a/Tests/TestModels/UriTestModel.cs b/Tests/TestModels/UriTestModel.cs new file mode 100644 index 0000000..7bee592 --- /dev/null +++ b/Tests/TestModels/UriTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class UriTestModel +{ + public required Uri WebsiteUri { get; set; } + public required Uri AvatarUri { get; set; } + public required Uri ApiEndpointUri { get; set; } +} diff --git a/Tests/Tests/ArrayGeneratorTests.cs b/Tests/Tests/ArrayGeneratorTests.cs new file mode 100644 index 0000000..8ebfa95 --- /dev/null +++ b/Tests/Tests/ArrayGeneratorTests.cs @@ -0,0 +1,148 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class ArrayGeneratorTests +{ + [Fact(DisplayName = "ArrayGenerator should generate not empty int[]")] + public void Create_IntArray() + { + var generated = new DataFacade().Create(); + + generated.Ints.Should().NotBeNull(); + generated.Ints.Should().NotBeEmpty(); + generated.Ints.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty double[]")] + public void Create_DoubleArray() + { + var generated = new DataFacade().Create(); + + generated.Doubles.Should().NotBeNull(); + generated.Doubles.Should().NotBeEmpty(); + generated.Doubles.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.Doubles.Should().Contain(item => Math.Abs(item) > 0.000001); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty bool[]")] + public void Create_BoolArray() + { + var generated = new DataFacade().Create(); + + generated.Bools.Should().NotBeNull(); + generated.Bools.Should().NotBeEmpty(); + + generated.Bools.Should().Contain(item => item).And.Contain(item => !item); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty char[]")] + public void Create_CharArray() + { + var generated = new DataFacade().Create(); + + generated.Chars.Should().NotBeNull(); + generated.Chars.Should().NotBeEmpty(); + + generated.Chars.Should().OnlyContain(item => + char.IsLetter(item) || char.IsDigit(item) || char.IsSymbol(item) || char.IsPunctuation(item)); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty Guid[] with valid values")] + public void Create_GuidArray() + { + var generated = new DataFacade().Create(); + + generated.Guids.Should().NotBeNull(); + generated.Guids.Should().NotBeEmpty(); + generated.Guids.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty Uri[]")] + public void Create_UriArray() + { + var generated = new DataFacade().Create(); + + generated.Uris.Should().NotBeNull(); + generated.Uris.Should().NotBeEmpty(); + generated.Uris.Should().OnlyContain(item => item != null); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty DateTime[]")] + public void Create_DateArray() + { + var generated = new DataFacade().Create(); + + generated.Dates.Should().NotBeNull(); + generated.Dates.Should().NotBeEmpty(); + generated.Dates.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty DateOnly[]")] + public void Create_DateOnlyArray() + { + var generated = new DataFacade().Create(); + + generated.DateOnlys.Should().NotBeNull(); + generated.DateOnlys.Should().NotBeEmpty(); + generated.DateOnlys.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty TimeOnly[]")] + public void Create_TimeOnlyArray() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlys.Should().NotBeNull(); + generated.TimeOnlys.Should().NotBeEmpty(); + generated.TimeOnlys.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty TimeSpan[]")] + public void Create_TimeSpanArray() + { + var generated = new DataFacade().Create(); + + generated.TimeSpans.Should().NotBeNull(); + generated.TimeSpans.Should().NotBeEmpty(); + generated.TimeSpans.Should().OnlyContain(item => item != default); + } + + + [Fact(DisplayName = "ArrayGenerator should generate not empty enum[] without Unknown")] + public void Create_EnumArray() + { + var generated = new DataFacade().Create(); + + generated.Enums.Should().NotBeNull(); + generated.Enums.Should().NotBeEmpty(); + generated.Enums.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty string[] with non-empty values")] + public void Create_StringArray() + { + var generated = new DataFacade().Create(); + + generated.Strings.Should().NotBeNull(); + generated.Strings.Should().NotBeEmpty(); + generated.Strings.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty SimpleItem[]")] + public void Create_ObjectArray() + { + var generated = new DataFacade().Create(); + + generated.Items.Should().NotBeNull(); + generated.Items.Should().NotBeEmpty(); + generated.Items.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } +} diff --git a/Tests/Tests/BoolGeneratorTests.cs b/Tests/Tests/BoolGeneratorTests.cs new file mode 100644 index 0000000..660241e --- /dev/null +++ b/Tests/Tests/BoolGeneratorTests.cs @@ -0,0 +1,53 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class BoolGeneratorTests +{ + private const int SampleCount = 50; + private const int MajorityThreshold = SampleCount / 2; + + [Fact(DisplayName = "BoolGenerator should generate mostly true for Is*/Has*/Enabled/Active names")] + public void Create_MostlyPositiveFlags() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create()) + .ToList(); + + generated.Count(item => item.IsActive).Should().BeGreaterThan(MajorityThreshold); + generated.Count(item => item.HasAccess).Should().BeGreaterThan(MajorityThreshold); + generated.Count(item => item.FeatureEnabled).Should().BeGreaterThan(MajorityThreshold); + } + + [Fact(DisplayName = "BoolGenerator should generate mostly false for Deleted/Disabled/Blocked names")] + public void Create_MostlyNegativeFlags() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create()) + .ToList(); + + generated.Count(item => item.IsDeleted).Should().BeLessThan(MajorityThreshold); + generated.Count(item => item.IsDisabled).Should().BeLessThan(MajorityThreshold); + generated.Count(item => item.IsBlocked).Should().BeLessThan(MajorityThreshold); + } + + [Fact(DisplayName = "BoolGenerator should generate both true and false for flags without naming meaning")] + public void Create_RandomFlag_ShouldContainTrueAndFalse() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().RandomFlag) + .ToList(); + + generated.Should().Contain(true); + generated.Should().Contain(false); + } +} diff --git a/Tests/Tests/CharGeneratorTests.cs b/Tests/Tests/CharGeneratorTests.cs new file mode 100644 index 0000000..6d2780c --- /dev/null +++ b/Tests/Tests/CharGeneratorTests.cs @@ -0,0 +1,58 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class CharGeneratorTests +{ + private const int SampleCount = 200; + private const int MajorityThreshold = SampleCount * 60 / 100; + + [Fact(DisplayName = "CharGenerator should generate mostly lowercase letters")] + public void Create_MostlyLetters() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Letter) + .ToList(); + + var lettersCount = generated.Count(item => item is >= 'a' and <= 'z'); + lettersCount.Should().BeGreaterThan(MajorityThreshold); + } + + [Fact(DisplayName = "CharGenerator should generate some digits")] + public void Create_ShouldGenerateDigits() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Digit) + .ToList(); + + var digitsCount = generated.Count(item => item is >= '0' and <= '9'); + digitsCount.Should().BeGreaterThan(0); + digitsCount.Should().BeLessThan(MajorityThreshold); + } + + [Fact(DisplayName = "CharGenerator should generate some printable non-alphanumeric symbols")] + public void Create_ShouldGenerateSpecialCharacters() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Any) + .ToList(); + + var specialCount = generated.Count(item => + item is >= '\u0021' and <= '\u007E' + && (item < 'a' || item > 'z') + && (item < '0' || item > '9') + ); + + specialCount.Should().BeGreaterThan(0); + specialCount.Should().BeLessThan(SampleCount / 3); + } +} diff --git a/Tests/Tests/DataGeneratorRecursionTests.cs b/Tests/Tests/DataGeneratorRecursionTests.cs new file mode 100644 index 0000000..d6af83a --- /dev/null +++ b/Tests/Tests/DataGeneratorRecursionTests.cs @@ -0,0 +1,114 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class DataGeneratorRecursionTests +{ + [Fact(DisplayName = "Recursion depth default = 3")] + public void Create_RecursionDepth_Default() + { + var generated = new DataFacade().Create(); + + CountDepth(generated).Should().Be(3); + + AssertNotEmptyCollections(generated); + AssertNotEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 2")] + public void Create_RecursionDepth_2() + { + var generated = new DataFacade(recursionDepth: 2).Create(); + + CountDepth(generated).Should().Be(2); + + generated.Parent.Should().NotBeNull(); + generated.Parent.Parent.Should().BeNull(); + + AssertNotEmptyCollections(generated); + AssertNotEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 1 (root only)")] + public void Create_RecursionDepth_1() + { + var generated = new DataFacade(recursionDepth: 1).Create(); + + CountDepth(generated).Should().Be(1); + generated.Parent.Should().BeNull(); + + AssertEmptyCollections(generated); + AssertEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 0 behaves exactly as depth = 1")] + public void Create_RecursionDepth_0() + { + var generated = new DataFacade(recursionDepth: 0).Create(); + + generated.Parent.Should().BeNull(); + + AssertEmptyCollections(generated); + AssertEmptyDictionaries(generated); + } + + private static int CountDepth(SampleModel model) + { + var depth = 1; + var current = model; + + while (current.Parent is not null) + { + depth++; + current = current.Parent; + } + + return depth; + } + + private static void AssertNotEmptyCollections(SampleModel generated) + { + generated.Children.Should().NotBeNull().And.NotBeEmpty(); + generated.Siblings.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumerableChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.CollectionChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ListChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyCollectionChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyListChildren.Should().NotBeNull().And.NotBeEmpty(); + } + + private static void AssertEmptyCollections(SampleModel generated) + { + generated.Children.Should().NotBeNull().And.BeEmpty(); + generated.Siblings.Should().NotBeNull().And.BeEmpty(); + generated.EnumerableChildren.Should().NotBeNull().And.BeEmpty(); + generated.CollectionChildren.Should().NotBeNull().And.BeEmpty(); + generated.ListChildren.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyCollectionChildren.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyListChildren.Should().NotBeNull().And.BeEmpty(); + } + + private static void AssertNotEmptyDictionaries(SampleModel generated) + { + generated.Related.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.InterfaceDictionaryRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.SortedRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.ConcurrentRelated.Should().NotBeNull().And.NotBeEmpty(); + } + + private static void AssertEmptyDictionaries(SampleModel generated) + { + generated.Related.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyRelated.Should().NotBeNull().And.BeEmpty(); + generated.InterfaceDictionaryRelated.Should().NotBeNull().And.BeEmpty(); + generated.SortedRelated.Should().NotBeNull().And.BeEmpty(); + generated.ConcurrentRelated.Should().NotBeNull().And.BeEmpty(); + } +} \ No newline at end of file diff --git a/Tests/DateTimeGeneratorTests.cs b/Tests/Tests/DateTimeGeneratorTests.cs similarity index 98% rename from Tests/DateTimeGeneratorTests.cs rename to Tests/Tests/DateTimeGeneratorTests.cs index 46dc8eb..8756db7 100644 --- a/Tests/DateTimeGeneratorTests.cs +++ b/Tests/Tests/DateTimeGeneratorTests.cs @@ -6,7 +6,7 @@ using Xunit; -namespace Tests; +namespace Tests.Tests; public sealed class DateTimeGeneratorTests { diff --git a/Tests/Tests/DictionaryGeneratorTests.cs b/Tests/Tests/DictionaryGeneratorTests.cs new file mode 100644 index 0000000..938fff4 --- /dev/null +++ b/Tests/Tests/DictionaryGeneratorTests.cs @@ -0,0 +1,153 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class DictionaryGeneratorTests +{ + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_IntStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.IntStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.IntStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.IntStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DoubleStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DoubleStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DoubleStringDictionary.Keys.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.DoubleStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_StringGuidDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringGuidDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringGuidDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringGuidDictionary.Values.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_CharBoolDictionary() + { + var generated = new DataFacade().Create(); + + generated.CharBoolDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.CharBoolDictionary.Keys.Should().OnlyContain(item => item != default); + generated.CharBoolDictionary.Values.Should().OnlyContain(item => item || !item); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_BoolDoubleDictionary() + { + var generated = new DataFacade().Create(); + + generated.BoolDoubleDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolDoubleDictionary.Keys.Should().Contain(true).And.Contain(false); + generated.BoolDoubleDictionary.Values.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_UriIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.UriIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.UriIntDictionary.Keys.Should().OnlyContain(item => item != null); + generated.UriIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DateStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DateStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DateStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.DateStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DateOnlyStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DateOnlyStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DateOnlyStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.DateOnlyStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_TimeOnlyStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlyStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeOnlyStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.TimeOnlyStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_TimeSpanStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.TimeSpanStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeSpanStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.TimeSpanStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_EnumCharDictionary() + { + var generated = new DataFacade().Create(); + + generated.EnumCharDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumCharDictionary.Keys.Should().OnlyContain(item => item != Sample.Unknown); + generated.EnumCharDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary with valid values")] + public void Create_StringComplexDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringComplexDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringComplexDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringComplexDictionary.Values.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty IDictionary")] + public void Create_GuidIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.GuidIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidIntDictionary.Keys.Should().OnlyContain(item => item != Guid.Empty); + generated.GuidIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty IReadOnlyDictionary")] + public void Create_StringBoolDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringBoolDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringBoolDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringBoolDictionary.Values.Should().Contain(item => item).And.Contain(item => !item); + } +} \ No newline at end of file diff --git a/Tests/Tests/EnumGeneratorTests.cs b/Tests/Tests/EnumGeneratorTests.cs new file mode 100644 index 0000000..fecab1d --- /dev/null +++ b/Tests/Tests/EnumGeneratorTests.cs @@ -0,0 +1,50 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class EnumGeneratorTests +{ + [Fact(DisplayName = "EnumGenerator should never generate zero-value for enums starting with Unknown/None")] + public void Create_ShouldSkipZeroValue_WhenEnumStartsWithZero() + { + var generated = new DataFacade().Create(); + + generated.Basic.Should().NotBe(Basic.Unknown); + } + + [Fact(DisplayName = "EnumGenerator should generate valid values for enums without zero")] + public void Create_ShouldGenerate_WhenEnumHasNoZero() + { + var generated = new DataFacade().Create(); + + generated.NoZero.Should().BeOneOf(NoZero.A, NoZero.B, NoZero.C); + } + + [Fact(DisplayName = "EnumGenerator should generate any value except zero entry when zero exists but is not 'Unknown' conceptually")] + public void Create_ShouldSkipZero_WhenZeroExistsButNotMeaningful() + { + var generated = new DataFacade().Create(); + + generated.Weird.Should().BeOneOf(Weird.Ten, Weird.Minus); + } + + [Fact(DisplayName = "EnumGenerator should produce different values statistically over multiple generations")] + public void Create_ShouldProduceDiversity() + { + var facade = new DataFacade(); + + var results = Enumerable.Range(0, 50) + .Select(_ => facade.Create().Basic) + .Distinct() + .ToList(); + + results.Count.Should().BeGreaterThan(1); + } +} diff --git a/Tests/Tests/EnumerableGeneratorTests.cs b/Tests/Tests/EnumerableGeneratorTests.cs new file mode 100644 index 0000000..3eb7f63 --- /dev/null +++ b/Tests/Tests/EnumerableGeneratorTests.cs @@ -0,0 +1,176 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class EnumerableGeneratorTests +{ + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_IntEnumerable() + { + var generated = new DataFacade().Create(); + + generated.IntEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.IntEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DoubleEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DoubleEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DoubleEnumerable.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.DoubleEnumerable.Should().Contain(item => Math.Abs(item) > 0.000001); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_BoolEnumerable() + { + var generated = new DataFacade().Create(); + + generated.BoolEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolEnumerable.Should().Contain(item => item).And.Contain(item => !item); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_CharEnumerable() + { + var generated = new DataFacade().Create(); + + generated.CharEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.CharEnumerable.Should().OnlyContain(item => char.IsLetter(item) || char.IsDigit(item) || char.IsSymbol(item) || char.IsPunctuation(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_GuidEnumerable() + { + var generated = new DataFacade().Create(); + + generated.GuidEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidEnumerable.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_UriEnumerable() + { + var generated = new DataFacade().Create(); + + generated.UriEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.UriEnumerable.Should().OnlyContain(item => item != null); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DateEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DateEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DateEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DateOnlyEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DateOnlyEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DateOnlyEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_TimeOnlyEnumerable() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlyEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeOnlyEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_TimeSpanEnumerable() + { + var generated = new DataFacade().Create(); + + generated.TimeSpanEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeSpanEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_EnumEnumerable() + { + var generated = new DataFacade().Create(); + + generated.EnumEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumEnumerable.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_StringEnumerable() + { + var generated = new DataFacade().Create(); + + generated.StringEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.StringEnumerable.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable with valid content")] + public void Create_ItemEnumerable() + { + var generated = new DataFacade().Create(); + + generated.ItemEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.ItemEnumerable.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty ICollection")] + public void Create_StringCollection() + { + var generated = new DataFacade().Create(); + + generated.StringCollection.Should().NotBeNull().And.NotBeEmpty(); + generated.StringCollection.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IList")] + public void Create_GuidList() + { + var generated = new DataFacade().Create(); + + generated.GuidList.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidList.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IReadOnlyCollection")] + public void Create_BoolReadOnlyCollection() + { + var generated = new DataFacade().Create(); + + generated.BoolReadOnlyCollection.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolReadOnlyCollection.Should().OnlyContain(item => item || !item); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IReadOnlyList")] + public void Create_EnumReadOnlyList() + { + var generated = new DataFacade().Create(); + + generated.EnumReadOnlyList.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumReadOnlyList.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty List with valid values")] + public void Create_ItemList() + { + var generated = new DataFacade().Create(); + + generated.ItemList.Should().NotBeNull().And.NotBeEmpty(); + generated.ItemList.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } +} \ No newline at end of file diff --git a/Tests/Tests/GuidGeneratorTests.cs b/Tests/Tests/GuidGeneratorTests.cs new file mode 100644 index 0000000..9507eda --- /dev/null +++ b/Tests/Tests/GuidGeneratorTests.cs @@ -0,0 +1,34 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class GuidGeneratorTests +{ + [Fact(DisplayName = "GuidGenerator should generate non-empty Guid values")] + public void Create_GeneratesNonEmptyGuids() + { + var generated = new DataFacade().Create(); + + generated.Id.Should().NotBe(Guid.Empty); + generated.ExternalId.Should().NotBe(Guid.Empty); + generated.CorrelationId.Should().NotBe(Guid.Empty); + } + + [Fact(DisplayName = "GuidGenerator should generate different Guid values across multiple samples")] + public void Create_GeneratesDifferentValues_Statistically() + { + var facade = new DataFacade(); + + var results = Enumerable.Range(0, 30) + .Select(_ => facade.Create().Id) + .ToList(); + + results.Distinct().Count().Should().BeGreaterThan(1); + } +} diff --git a/Tests/MutateTests.cs b/Tests/Tests/MutateTests.cs similarity index 98% rename from Tests/MutateTests.cs rename to Tests/Tests/MutateTests.cs index 65fc665..f83f749 100644 --- a/Tests/MutateTests.cs +++ b/Tests/Tests/MutateTests.cs @@ -6,7 +6,7 @@ using Xunit; -namespace Tests; +namespace Tests.Tests; public sealed class MutateTests { diff --git a/Tests/NullableTests.cs b/Tests/Tests/NullableTests.cs similarity index 71% rename from Tests/NullableTests.cs rename to Tests/Tests/NullableTests.cs index 0462c18..6b63c92 100644 --- a/Tests/NullableTests.cs +++ b/Tests/Tests/NullableTests.cs @@ -6,7 +6,7 @@ using Xunit; -namespace Tests; +namespace Tests.Tests; public sealed class NullableTests { @@ -25,10 +25,10 @@ public void Nullable_Generation_CanBeNull_And_NotNull() deletedDates.Add(generated.DeletedAt); } - middleNames.Should().Contain(x => x == null); - middleNames.Should().Contain(x => x != null); + middleNames.Should().Contain(item => item == null); + middleNames.Should().Contain(item => item != null); - deletedDates.Should().Contain(x => x == null); - deletedDates.Should().Contain(x => x != null); + deletedDates.Should().Contain(item => item == null); + deletedDates.Should().Contain(item => item != null); } } diff --git a/Tests/Tests/NumericGeneratorTests.cs b/Tests/Tests/NumericGeneratorTests.cs new file mode 100644 index 0000000..ad125aa --- /dev/null +++ b/Tests/Tests/NumericGeneratorTests.cs @@ -0,0 +1,63 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class NumericGeneratorTests +{ + [Fact(DisplayName = "NumericGenerator should generate correct int values based on naming rules")] + public void Create_IntNamingPatterns() + { + var generated = new DataFacade().Create(); + + generated.Id.Should().Be(0); + generated.UserId.Should().Be(0); + + generated.Age.Should().BeInRange(18, 70); + + generated.ItemCount.Should().BeInRange(0, 1000); + generated.Qty.Should().BeInRange(0, 1000); + generated.TotalNum.Should().BeInRange(0, 1000); + + generated.Price.Should().BeInRange(1, 100_000); + generated.OrderAmount.Should().BeInRange(1, 100_000); + generated.TotalSum.Should().BeInRange(1, 100_000); + + generated.ReleaseYear.Should().BeLessThanOrEqualTo(DateTime.UtcNow.Year); + } + + [Fact(DisplayName = "NumericGenerator should generate correct numeric values for all numeric types")] + public void Create_NumericTypes() + { + var generated = new DataFacade().Create(); + + generated.UIntValue.Should().BeGreaterThanOrEqualTo(0u); + generated.LongValue.Should().BeGreaterThanOrEqualTo(0); + generated.ULongValue.Should().BeGreaterThanOrEqualTo(0ul); + generated.ShortValue.Should().BeInRange(short.MinValue, short.MaxValue); + generated.UShortValue.Should().BeInRange(ushort.MinValue, ushort.MaxValue); + generated.ByteValue.Should().BeInRange(byte.MinValue, byte.MaxValue); + generated.SByteValue.Should().BeInRange(sbyte.MinValue, sbyte.MaxValue); + + generated.DoubleValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + generated.FloatValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + generated.DecimalValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + } + + [Fact(DisplayName = "NumericGenerator should produce statistically diverse values")] + public void Create_ShouldProduceDiversity() + { + var facade = new DataFacade(); + + var values = Enumerable.Range(0, 50) + .Select(_ => facade.Create().DoubleValue) + .ToList(); + + values.Distinct().Count().Should().BeGreaterThan(1); + } +} diff --git a/Tests/Tests/StringGeneratorTests.cs b/Tests/Tests/StringGeneratorTests.cs new file mode 100644 index 0000000..6c79ca0 --- /dev/null +++ b/Tests/Tests/StringGeneratorTests.cs @@ -0,0 +1,67 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class StringGeneratorTests +{ + [Fact(DisplayName = "StringGenerator should generate valid values for all naming rules")] + public void Create_AllSmartStringPatterns() + { + var generated = new DataFacade().Create(); + + generated.EmailAddress.Should().Contain("@"); + + generated.Phone.Should().MatchRegex(@"\d"); + generated.MobilePhone.Should().MatchRegex(@"\d"); + + generated.FullName.Should().Contain(" "); + generated.Name.Should().Contain(" "); + generated.Fio.Should().Contain(" "); + + generated.FirstName.Should().NotBeNullOrWhiteSpace(); + generated.LastName.Should().NotBeNullOrWhiteSpace(); + generated.Surname.Should().NotBeNullOrWhiteSpace(); + + generated.Username.Should().NotContain(" "); + generated.Login.Should().NotContain(" "); + + generated.Password.Any(char.IsDigit).Should().BeTrue(); + generated.PwdHash.Any(char.IsDigit).Should().BeTrue(); + + Uri.TryCreate(generated.WebsiteUrl, UriKind.Absolute, out _).Should().BeTrue(); + Uri.TryCreate(generated.ProfileLink, UriKind.Absolute, out _).Should().BeTrue(); + + Uri.TryCreate(generated.Avatar, UriKind.Absolute, out _).Should().BeTrue(); + + generated.Country.Should().NotBeNullOrWhiteSpace(); + generated.City.Should().NotBeNullOrWhiteSpace(); + generated.StreetAddress.Should().NotBeNullOrWhiteSpace(); + + generated.Postcode.Should().NotBeNullOrWhiteSpace(); + generated.PostalCode.Should().NotBeNullOrWhiteSpace(); + generated.ZipCode.Should().NotBeNullOrWhiteSpace(); + + generated.Title.Should().NotBeNullOrWhiteSpace(); + generated.SubjectTitle.Should().NotBeNullOrWhiteSpace(); + + generated.Description.Should().NotBeNullOrWhiteSpace(); + generated.MessageText.Should().NotBeNullOrWhiteSpace(); + generated.ContentString.Should().NotBeNullOrWhiteSpace(); + + generated.Company.Should().NotBeNullOrWhiteSpace(); + + generated.Iban.Should().MatchRegex(@"[A-Z]{2}\w+"); + generated.AccountNumber.Should().MatchRegex(@"[A-Z]{2}\w+"); + + generated.Currency.Should().MatchRegex(@"^[A-Z]{3}$"); + + generated.RandomString.Should().NotBeNullOrWhiteSpace(); + generated.RandomString.Length.Should().BeInRange(8, 16); + } +} diff --git a/Tests/TestDataFacadeTests.cs b/Tests/Tests/TestDataFacadeTests.cs similarity index 97% rename from Tests/TestDataFacadeTests.cs rename to Tests/Tests/TestDataFacadeTests.cs index c8d008e..3ce0b49 100644 --- a/Tests/TestDataFacadeTests.cs +++ b/Tests/Tests/TestDataFacadeTests.cs @@ -6,7 +6,7 @@ using Xunit; -namespace Tests; +namespace Tests.Tests; public sealed class TestDataFacadeTests { diff --git a/Tests/Tests/UriGeneratorTests.cs b/Tests/Tests/UriGeneratorTests.cs new file mode 100644 index 0000000..6cf819f --- /dev/null +++ b/Tests/Tests/UriGeneratorTests.cs @@ -0,0 +1,26 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class UriGeneratorTests +{ + [Fact(DisplayName = "UriGenerator should generate valid absolute URIs")] + public void Create_UriProperties() + { + var generated = new DataFacade().Create(); + + generated.WebsiteUri.Should().NotBeNull(); + generated.AvatarUri.Should().NotBeNull(); + generated.ApiEndpointUri.Should().NotBeNull(); + + Uri.IsWellFormedUriString(generated.WebsiteUri.ToString(), UriKind.Absolute).Should().BeTrue(); + Uri.IsWellFormedUriString(generated.AvatarUri.ToString(), UriKind.Absolute).Should().BeTrue(); + Uri.IsWellFormedUriString(generated.ApiEndpointUri.ToString(), UriKind.Absolute).Should().BeTrue(); + } +} From f66e40878c9efb36be2afa582acd58dfe2f78f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 16:41:01 +0400 Subject: [PATCH 08/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D1=80=D0=B5=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=8C=D1=88=D0=B8=D0=BB=20=D0=B4=D0=BE?= =?UTF-8?q?=2090%=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8=20=D0=B2=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- Tests/TestModels/DictionaryTestModel.cs | 7 ++++++- Tests/Tests/DictionaryGeneratorTests.cs | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3984886..a1a7083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,4 +13,4 @@ jobs: uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main with: coverage_assembly_filters: "+DataGenerator*;-*Tests*" - coverage_threshold: "100" + coverage_threshold: "90" diff --git a/Tests/TestModels/DictionaryTestModel.cs b/Tests/TestModels/DictionaryTestModel.cs index 5a84d37..c328ae7 100644 --- a/Tests/TestModels/DictionaryTestModel.cs +++ b/Tests/TestModels/DictionaryTestModel.cs @@ -1,4 +1,6 @@ -using Tests.TestModels.Common; +using System.Collections.Concurrent; + +using Tests.TestModels.Common; using Tests.TestModels.Common.Enums; namespace Tests.TestModels; @@ -18,6 +20,9 @@ public sealed class DictionaryTestModel public required Dictionary EnumCharDictionary { get; set; } = []; public required Dictionary StringComplexDictionary { get; set; } = []; + public required SortedDictionary SortedStringIntDictionary { get; set; } = []; + public required ConcurrentDictionary ConcurrentStringDoubleDictionary { get; set; } = []; + public required IDictionary GuidIntDictionary { get; set; } = new Dictionary(); public required IReadOnlyDictionary StringBoolDictionary { get; set; } = new Dictionary(); } diff --git a/Tests/Tests/DictionaryGeneratorTests.cs b/Tests/Tests/DictionaryGeneratorTests.cs index 938fff4..0be985a 100644 --- a/Tests/Tests/DictionaryGeneratorTests.cs +++ b/Tests/Tests/DictionaryGeneratorTests.cs @@ -131,6 +131,26 @@ public void Create_StringComplexDictionary() generated.StringComplexDictionary.Values.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); } + [Fact(DisplayName = "DictionaryGenerator should generate not empty SortedDictionary")] + public void Create_SortedStringIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.SortedStringIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.SortedStringIntDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.SortedStringIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty ConcurrentDictionary")] + public void Create_ConcurrentStringDoubleDictionary() + { + var generated = new DataFacade().Create(); + + generated.ConcurrentStringDoubleDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.ConcurrentStringDoubleDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.ConcurrentStringDoubleDictionary.Values.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + } + [Fact(DisplayName = "DictionaryGenerator should generate not empty IDictionary")] public void Create_GuidIntDictionary() { From 7db855067fc723e8280a1d15119a69508d3e9ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 17:08:24 +0400 Subject: [PATCH 09/10] =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/DictionaryGenerator.cs | 84 ++++++++++++++----- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs index 7735deb..d37ff81 100644 --- a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs +++ b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs @@ -26,28 +26,20 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } var count = faker.Random.Int(2, 5); - var dictionary = (IDictionary)CreateEmptyDictionary(type, keyType!, valueType!)!; + var dictionaryInstance = CreateEmptyDictionary(type, keyType!, valueType!)!; - int safety = 0; - while (dictionary.Count < count && safety < count * 4) + if (dictionaryInstance is ConcurrentDictionary concurrentDict) { - 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(concurrentDict, keyType!, valueType!, context, count); + value = concurrentDict; + return true; + } + else + { + var dictionary = (IDictionary)dictionaryInstance; + FillDictionary(dictionary, keyType!, valueType!, context, count); + value = dictionary; } - value = dictionary; return true; } @@ -174,4 +166,58 @@ private static object CreateEmptyDictionary(Type requestType, Type keyType, Type 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(ConcurrentDictionary dictionary, Type keyType, Type valueType, ISpecimenContext context, int count) + { + FillDictionaryCore( + getCount: () => dictionary.Count, + generateKey: () => GenerateKey(keyType, context), + generateValue: () => context.Resolve(valueType), + tryAdd: (key, value) => dictionary.TryAdd(key!, value!), + count: count + ); + } + + private static void FillDictionaryCore( + Func getCount, + Func generateKey, + Func generateValue, + Func 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++; + } + } } From 1095e03379440989c5ccfd085a2dac4114affd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Fri, 7 Nov 2025 17:23:04 +0400 Subject: [PATCH 10/10] =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/DictionaryGenerator.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs index d37ff81..f509ddd 100644 --- a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs +++ b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs @@ -28,10 +28,10 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o var count = faker.Random.Int(2, 5); var dictionaryInstance = CreateEmptyDictionary(type, keyType!, valueType!)!; - if (dictionaryInstance is ConcurrentDictionary concurrentDict) + if (dictionaryInstance.GetType().IsGenericType && dictionaryInstance.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) { - FillConcurrent(concurrentDict, keyType!, valueType!, context, count); - value = concurrentDict; + FillConcurrent((dynamic)dictionaryInstance, keyType!, valueType!, context, count); + value = dictionaryInstance; return true; } else @@ -155,6 +155,14 @@ private static object CreateEmptyDictionary(Type requestType, Type keyType, Type { 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))!; @@ -186,13 +194,14 @@ private void FillDictionary(IDictionary dictionary, Type keyType, Type valueType ); } - private void FillConcurrent(ConcurrentDictionary dictionary, Type keyType, Type valueType, ISpecimenContext context, int count) + private void FillConcurrent(ConcurrentDictionary 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(key!, value!), + tryAdd: (key, value) => dictionary.TryAdd((TKey)key!, (TValue)value!), count: count ); }