From 61a396939d36e4827892448c93e84f3508104d37 Mon Sep 17 00:00:00 2001 From: nth-commit Date: Sun, 14 Aug 2022 14:36:23 +1200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=8D=20#367=20First=20implementation=20?= =?UTF-8?q?of=20[GenSnapshot]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GalaxyCheck.Xunit.Tests.csproj | 8 +- .../AboutAssertSnapshotMatches.cs | 47 ++++ .../AboutFactory.cs | 201 ++++++++++++++++++ .../AboutInvalidReturnTypes.cs | 47 ++++ .../AboutIterations.cs | 42 ++++ .../AboutMemberGenAttribute.cs | 134 ++++++++++++ .../GenSnapshotInitializerTests/AboutSize.cs | 42 ++++ .../GenSnapshotInitializerTests/TestProxy.cs | 43 ++++ .../AboutLocatingGlobalConfiguration.cs | 4 +- .../IntegrationSample/ConfigureGalaxyCheck.cs | 12 -- .../IntegrationTests.cs | 127 ----------- .../GenSnapshotTests.Snapshooter.cs | 34 +++ .../IntegrationTests/PropertyTests.cs | 66 ++++++ .../IntegrationTests/TestSuiteFixture.cs | 79 +++++++ .../PropertyInitializerTests/AboutFactory.cs | 20 +- .../AboutIterations.cs | 4 +- .../AboutMemberGenAttribute.cs | 6 +- .../PropertyInitializerTests/AboutReplay.cs | 2 +- .../PropertyInitializerTests/AboutSeed.cs | 4 +- .../AboutShrinkLimit.cs | 4 +- .../PropertyInitializerTests/AboutSize.cs | 4 +- .../IntegrationSample/AsyncTests.cs | 0 .../IntegrationSample.csproj | 6 +- .../IntegrationSample/IntegrationSample.sln | 4 +- .../{ => samples}/IntegrationSample/Tests.cs | 0 .../AsyncGenSnapshotTests.cs | 24 +++ .../ConfigureGalaxyCheck.cs | 22 ++ .../GenSnapshotTests.cs | 15 ++ ...rationSample_GenSnaphot_Snapshooter.csproj | 29 +++ ...tegrationSample_GenSnaphot_Snapshooter.sln | 37 ++++ .../AsyncGenSnapshotTests.TaskOutput_0.snap | 1 + ...syncGenSnapshotTests.ValueTaskOuput_0.snap | 1 + .../GenSnapshotTests.ObjectOutput_0.snap | 1 + .../GenSnapshotTests.TypedOutput_0.snap | 5 + .../Configuration/IGlobalConfiguration.cs | 34 ++- src/GalaxyCheck.Xunit/GenSnapshotAttribute.cs | 57 +++++ .../Internal/DefaultGenSnapshotFactory.cs | 12 ++ .../Internal/GenSnapshotDiscoverer.cs | 28 +++ .../Internal/GenSnapshotInitializer.cs | 74 +++++++ .../Internal/GenSnapshotRunner.cs | 107 ++++++++++ .../Internal/GenSnapshotTestCase.cs | 102 +++++++++ .../Internal/GlobalConfiguration.cs | 6 +- .../GlobalGenSnapshotConfiguration.cs | 15 ++ .../Internal/GlobalPropertyConfiguration.cs | 11 + .../Internal/IParametersGenFactory.cs | 14 ++ .../Internal/PropertyInitializer.cs | 67 +----- .../Internal/PropertyTestCase.cs | 2 +- .../Internal/ReflectionHelpers.cs | 66 ++++++ src/GalaxyCheck/Gens/ParametersGen.cs | 11 +- 49 files changed, 1436 insertions(+), 245 deletions(-) create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutAssertSnapshotMatches.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutFactory.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutInvalidReturnTypes.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutIterations.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutMemberGenAttribute.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutSize.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/TestProxy.cs delete mode 100644 src/GalaxyCheck.Xunit.Tests/IntegrationSample/ConfigureGalaxyCheck.cs delete mode 100644 src/GalaxyCheck.Xunit.Tests/IntegrationTests.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/IntegrationTests/GenSnapshotTests.Snapshooter.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/IntegrationTests/PropertyTests.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/IntegrationTests/TestSuiteFixture.cs rename src/GalaxyCheck.Xunit.Tests/{ => samples}/IntegrationSample/AsyncTests.cs (100%) rename src/GalaxyCheck.Xunit.Tests/{ => samples}/IntegrationSample/IntegrationSample.csproj (85%) rename src/GalaxyCheck.Xunit.Tests/{ => samples}/IntegrationSample/IntegrationSample.sln (90%) rename src/GalaxyCheck.Xunit.Tests/{ => samples}/IntegrationSample/Tests.cs (100%) create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/AsyncGenSnapshotTests.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/ConfigureGalaxyCheck.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/GenSnapshotTests.cs create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.csproj create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.sln create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.TaskOutput_0.snap create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.ValueTaskOuput_0.snap create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.ObjectOutput_0.snap create mode 100644 src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.TypedOutput_0.snap create mode 100644 src/GalaxyCheck.Xunit/GenSnapshotAttribute.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/DefaultGenSnapshotFactory.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GenSnapshotDiscoverer.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GenSnapshotInitializer.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GenSnapshotRunner.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GenSnapshotTestCase.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GlobalGenSnapshotConfiguration.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/GlobalPropertyConfiguration.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/IParametersGenFactory.cs create mode 100644 src/GalaxyCheck.Xunit/Internal/ReflectionHelpers.cs diff --git a/src/GalaxyCheck.Xunit.Tests/GalaxyCheck.Xunit.Tests.csproj b/src/GalaxyCheck.Xunit.Tests/GalaxyCheck.Xunit.Tests.csproj index c8547a69..a9dc625c 100644 --- a/src/GalaxyCheck.Xunit.Tests/GalaxyCheck.Xunit.Tests.csproj +++ b/src/GalaxyCheck.Xunit.Tests/GalaxyCheck.Xunit.Tests.csproj @@ -2,16 +2,16 @@ net6.0 - 9.0 + 10.0 false Tests enable - - - + + + diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutAssertSnapshotMatches.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutAssertSnapshotMatches.cs new file mode 100644 index 00000000..bd7d3b7c --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutAssertSnapshotMatches.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using GalaxyCheck; +using GalaxyCheck.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutAssertSnapshotMatches + { +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshots +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + public object GenSnapshot() => new { }; + } + + [Fact] + public void ItPassesThroughAssertSnapshotMatches() + { + Func assertSnapshotMatches = (_) => Task.CompletedTask; + + var result = TestProxy.Initialize( + typeof(GenSnapshots), + nameof(GenSnapshots.GenSnapshot), + configure: config => config.AssertSnapshotMatches = assertSnapshotMatches); + + result.AssertSnapshotMatches.Should().Be(assertSnapshotMatches); + } + + [Fact] + public void ItThrowsIfAssertSnapshotMatchesIsNull() + { + var action = () => TestProxy.Initialize( + typeof(GenSnapshots), + nameof(GenSnapshots.GenSnapshot), + configure: config => config.AssertSnapshotMatches = null); + + action.Should().Throw().WithMessage("Configuration value \"AssertSnapshotMatches\" needs to be set in order to use gen snapshots"); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutFactory.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutFactory.cs new file mode 100644 index 00000000..58a6358d --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutFactory.cs @@ -0,0 +1,201 @@ +using GalaxyCheck; +using GalaxyCheck.Gens; +using GalaxyCheck.Internal; +using Moq; +using System; +using System.Collections.Generic; +using System.Reflection; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutFactory + { + [Theory] + [InlineData(nameof(GenSnapshotsWithoutFactoryConfig.GenSnapshotWithoutFactoryConfig))] + public void WhenClassNorMethodHasFactoryConfig_ItPassesThroughNull(string testMethodName) + { + var mockParametersGenFactory = MockParametersGenFactory(); + + TestProxy.Initialize( + typeof(GenSnapshotsWithoutFactoryConfig), + testMethodName, + parametersGenFactory: mockParametersGenFactory.Object); + + VerifyFactoryNotPassedThrough(mockParametersGenFactory); + } + + [Theory] + [InlineData(nameof(GenSnapshotsWithoutFactoryConfig.GenSnapshotWithFactoryConfig))] + public void ItPassesThroughTheFactoryFromTheMethod(string testMethodName) + { + var mockParametersGenFactory = MockParametersGenFactory(); + + TestProxy.Initialize( + typeof(GenSnapshotsWithoutFactoryConfig), + testMethodName, + parametersGenFactory: mockParametersGenFactory.Object); + + VerifyFactoryPassedThrough(mockParametersGenFactory, typeof(GenFactory1)); + } + + [Theory] + [InlineData(nameof(GenSnapshotsWithFactoryConfig.GenSnapshotWithoutFactoryConfig))] + public void ItPassesThroughTheFactoryFromTheClass(string testMethodName) + { + var mockParametersGenFactory = MockParametersGenFactory(); + + TestProxy.Initialize( + typeof(GenSnapshotsWithFactoryConfig), + testMethodName, + parametersGenFactory: mockParametersGenFactory.Object); + + VerifyFactoryPassedThrough(mockParametersGenFactory, typeof(GenFactory2)); + } + + [Theory] + [InlineData(nameof(GenSnapshotsWithFactoryConfig.GenSnapshotWithFactoryConfig))] + public void AMethodLevelFactoryIsPreferredOverAClassLevelFactory(string testMethodName) + { + var mockParametersGenFactory = MockParametersGenFactory(); + + TestProxy.Initialize( + typeof(GenSnapshotsWithFactoryConfig), + testMethodName, + parametersGenFactory: mockParametersGenFactory.Object); + + VerifyFactoryPassedThrough(mockParametersGenFactory, typeof(GenFactory1)); + } + + private static Mock MockParametersGenFactory() + { + var mock = new Mock(); + + return mock; + } + + private static void VerifyFactoryPassedThrough(Mock mockParametersGenFactory, Type expectedFactoryType) + { + mockParametersGenFactory.Verify( + x => x.CreateParametersGen( + It.IsAny(), + It.Is(factory => factory != null && factory.GetType() == expectedFactoryType), + It.IsAny>()), + Times.Once); + } + + private static void VerifyFactoryNotPassedThrough(Mock mockParametersGenFactory) + { + mockParametersGenFactory.Verify( + x => x.CreateParametersGen( + It.IsAny(), + It.Is(x => x == null), + It.IsAny>()), + Times.Once); + } + + private class GenFactory1 : IGenFactory + { + public IReflectedGen Create(NullabilityInfo? nullabilityInfo = null) where T : notnull + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Type type, IGen gen) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(IGen gen) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Func> genFunc) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Type type, Func genFunc) + { + throw new NotImplementedException(); + } + } + + private class GenFactory1Attribute : GenFactoryAttribute + { + public override IGenFactory Value => new GenFactory1(); + } + + private class GenFactory2 : IGenFactory + { + public IReflectedGen Create(NullabilityInfo? nullabilityInfo = null) where T : notnull + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Type type, IGen gen) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(IGen gen) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Func> genFunc) + { + throw new NotImplementedException(); + } + + public IGenFactory RegisterType(Type type, Func genFunc) + { + throw new NotImplementedException(); + } + } + + private class GenFactory2Attribute : GenFactoryAttribute + { + public override IGenFactory Value => new GenFactory2(); + } + +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshotsWithoutFactoryConfig +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + [GenFactory1] + public object GenSnapshotWithFactoryConfig() => new { }; + + [GenSnapshot] + public object GenSnapshotWithoutFactoryConfig() => new { }; + } + + [GenFactory2] +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshotsWithFactoryConfig +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + [GenFactory1] + public object GenSnapshotWithFactoryConfig() => new { }; + + [GenSnapshot] + public object GenSnapshotWithoutFactoryConfig() => new { }; + } + + + private static MethodInfo GetMethod(Type type, string name) + { + var methodInfo = type.GetMethod(name, BindingFlags.Public | BindingFlags.Instance); + + if (methodInfo == null) + { + throw new Exception("Unable to locate method"); + } + + return methodInfo; + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutInvalidReturnTypes.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutInvalidReturnTypes.cs new file mode 100644 index 00000000..b6ceb1df --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutInvalidReturnTypes.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using GalaxyCheck; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutInvalidReturnTypes + { +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshots +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + public void GenSnapshotVoid() + { + } + + [GenSnapshot] + public Task GenSnapshotTaskVoid() + { + return Task.CompletedTask; + } + + [GenSnapshot] + public ValueTask GenSnapshotValueTaskVoid() + { + return ValueTask.CompletedTask; + } + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotVoid))] + [InlineData(nameof(GenSnapshots.GenSnapshotTaskVoid))] + [InlineData(nameof(GenSnapshots.GenSnapshotValueTaskVoid))] + public void ItPassesThroughSeedsForTheIterations(string testMethodName) + { + var action = () => TestProxy.Initialize(typeof(GenSnapshots), testMethodName); + + action.Should().Throw().WithMessage("Test method must return a value for snapshotting, or a task containing a value"); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutIterations.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutIterations.cs new file mode 100644 index 00000000..d8941971 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutIterations.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using GalaxyCheck; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutIterations + { +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshots +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + public object GenSnapshotWithDefaultIterations() => new { }; + + [GenSnapshot(Iterations = 5)] + public object GenSnapshotWith5Iterations() => new { }; + + [GenSnapshot(Iterations = 10)] + public object GenSnapshotWith10Iterations() => new { }; + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWith5Iterations), new int[] { 0, 1, 2, 3, 4 })] + [InlineData(nameof(GenSnapshots.GenSnapshotWith10Iterations), new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })] + public void ItPassesThroughSeedsForTheIterations(string testMethodName, int[] expectedSeeds) + { + var result = TestProxy.Initialize(typeof(GenSnapshots), testMethodName, configure: config => config.DefaultIterations = 999); + + result.Seeds.Should().BeEquivalentTo(expectedSeeds); + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithDefaultIterations))] + public void ItPassesThroughSeedsFromTheDefaultIterationsIfNotExplicitlySet(string testMethodName) + { + var result = TestProxy.Initialize(typeof(GenSnapshots), testMethodName, configure: config => config.DefaultIterations = 5); + + result.Seeds.Should().BeEquivalentTo(new [] { 0, 1, 2, 3, 4 }); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutMemberGenAttribute.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutMemberGenAttribute.cs new file mode 100644 index 00000000..873d7223 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutMemberGenAttribute.cs @@ -0,0 +1,134 @@ +using FluentAssertions; +using GalaxyCheck; +using GalaxyCheck.Gens; +using GalaxyCheck.Internal; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutMemberGenAttribute + { + + private static readonly IGen Gen = GalaxyCheck.Gen.Int32(); + +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshots +#pragma warning restore xUnit1000 // Test classes must be public + { + public object NotAGen => new object(); + + [GenSnapshot] + public object GenSnapshotWithNonGenMemberGen([MemberGen(nameof(NotAGen))] int x) => new { }; + + public IGen GenSnapshotMemberGen => Gen; + + [GenSnapshot] + public object GenSnapshotWithGenSnapshotMemberGen([MemberGen(nameof(GenSnapshotMemberGen))] int x) => new { }; + + public IGen FieldMemberGen = Gen; + + [GenSnapshot] + public object GenSnapshotWithFieldMemberGen([MemberGen(nameof(FieldMemberGen))] int x) => new { }; + + public IGen PublicMemberGen => Gen; + + [GenSnapshot] + public object GenSnapshotWithPublicMemberGen([MemberGen(nameof(PublicMemberGen))] int x) => new { }; + + private IGen PrivateMemberGen => Gen; + + [GenSnapshot] + public object GenSnapshotWithPrivateMemberGen([MemberGen(nameof(PrivateMemberGen))] int x) => new { }; + + public IGen InstanceMemberGen => Gen; + + [GenSnapshot] + public object GenSnapshotWithInstanceMemberGen([MemberGen(nameof(InstanceMemberGen))] int x) => new { }; + + public static IGen StaticMemberGen => Gen; + + [GenSnapshot] + public object GenSnapshotWithStaticMemberGen([MemberGen(nameof(StaticMemberGen))] int x) => new { }; + + [GenSnapshot] + public object GenSnapshotWithMemberGenAtIndex0([MemberGen(nameof(GenSnapshotMemberGen))] int x, int y) => new { }; + + [GenSnapshot] + public object GenSnapshotWithMemberGenAtIndex1(int x, [MemberGen(nameof(GenSnapshotMemberGen))] int y) => new { }; + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithNonGenMemberGen))] + public void ItErrorsWhenReferenceGenSnapshotIsNotAGen(string testMethodName) + { + Action test = () => TestProxy.Initialize(typeof(GenSnapshots), testMethodName); + + test.Should() + .Throw() + .WithMessage("Expected member 'NotAGen' to be an instance of 'GalaxyCheck.IGen', but it had a value of type 'System.Object'"); + } + + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithGenSnapshotMemberGen))] + [InlineData(nameof(GenSnapshots.GenSnapshotWithFieldMemberGen))] + [InlineData(nameof(GenSnapshots.GenSnapshotWithPublicMemberGen))] + [InlineData(nameof(GenSnapshots.GenSnapshotWithPrivateMemberGen))] + [InlineData(nameof(GenSnapshots.GenSnapshotWithInstanceMemberGen))] + [InlineData(nameof(GenSnapshots.GenSnapshotWithStaticMemberGen))] + public void ItPassesThroughGen(string testMethodName) + { + var mockGenSnapshotFactory = MockGenSnapshotFactory(); + + TestProxy.Initialize(typeof(GenSnapshots), testMethodName, parametersGenFactory: mockGenSnapshotFactory.Object); + + VerifyGenPassedThrough(mockGenSnapshotFactory, Gen, 0); + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithMemberGenAtIndex0), 0)] + [InlineData(nameof(GenSnapshots.GenSnapshotWithMemberGenAtIndex1), 1)] + public void ItPassesThroughGenKeyedToCorrectIndex(string testMethodName, int expectedIndex) + { + var mockGenSnapshotFactory = MockGenSnapshotFactory(); + + TestProxy.Initialize(typeof(GenSnapshots), testMethodName, parametersGenFactory: mockGenSnapshotFactory.Object); + + VerifyGenPassedThrough(mockGenSnapshotFactory, Gen, expectedIndex); + } + + private static MethodInfo GetMethod(string name) + { + var methodInfo = typeof(GenSnapshots).GetMethod(name, BindingFlags.Instance | BindingFlags.Public); + + if (methodInfo == null) + { + throw new Exception("Unable to locate method"); + } + + return methodInfo; + } + + private static Mock MockGenSnapshotFactory() + { + var mock = new Mock(); + + return mock; + } + + private static void VerifyGenPassedThrough(Mock mockGenSnapshotFactory, IGen expectedGen, int expectedParameterIndex) + { + mockGenSnapshotFactory.Verify( + x => x.CreateParametersGen( + It.IsAny(), + It.IsAny(), + It.Is>(x => x.SequenceEqual(new [] { new KeyValuePair(expectedParameterIndex, expectedGen) }))), + Times.Once); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutSize.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutSize.cs new file mode 100644 index 00000000..a2903369 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/AboutSize.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using GalaxyCheck; +using Xunit; + +namespace Tests.GenSnapshotInitializerTests +{ + public class AboutSize + { +#pragma warning disable xUnit1000 // Test classes must be public + private class GenSnapshots +#pragma warning restore xUnit1000 // Test classes must be public + { + [GenSnapshot] + public object GenSnapshotWithDefaultSize() => new { }; + + [GenSnapshot(Size = 0)] + public object GenSnapshotWithSize0() => new { }; + + [GenSnapshot(Size = 10)] + public object GenSnapshotWithSize10() => new { }; + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithSize0), 0)] + [InlineData(nameof(GenSnapshots.GenSnapshotWithSize10), 10)] + public void ItPassesThroughSize(string testMethodName, int expectedSize) + { + var result = TestProxy.Initialize(typeof(GenSnapshots), testMethodName); + + result.Size.Should().Be(expectedSize); + } + + [Theory] + [InlineData(nameof(GenSnapshots.GenSnapshotWithDefaultSize))] + public void ItPassesThroughTheDefaultSizeIfNotExplicitlySet(string testMethodName) + { + var result = TestProxy.Initialize(typeof(GenSnapshots), testMethodName, configure: config => config.DefaultSize = 66); + + result.Size.Should().Be(66); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/TestProxy.cs b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/TestProxy.cs new file mode 100644 index 00000000..5f692391 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/GenSnapshotInitializerTests/TestProxy.cs @@ -0,0 +1,43 @@ +using GalaxyCheck.Internal; +using Moq; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Tests.GenSnapshotInitializerTests +{ + internal static class TestProxy + { + internal static GenSnapshotInitializationResult Initialize( + Type testClassType, + string testMethodName, + IParametersGenFactory? parametersGenFactory = null, + Action? configure = null) + { + var config = new GlobalGenSnapshotConfiguration + { + AssertSnapshotMatches = (_) => Task.CompletedTask + }; + configure?.Invoke(config); + + return GenSnapshotInitializer.Initialize( + testClassType, + GetMethod(testClassType, testMethodName), + new object[] { }, + parametersGenFactory ?? new Mock().Object, + config); + } + + private static MethodInfo GetMethod(Type type, string name) + { + var methodInfo = type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public); + + if (methodInfo == null) + { + throw new Exception("Unable to locate method"); + } + + return methodInfo; + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/GlobalConfigurationTests/AboutLocatingGlobalConfiguration.cs b/src/GalaxyCheck.Xunit.Tests/GlobalConfigurationTests/AboutLocatingGlobalConfiguration.cs index 42e1c6c0..1ebdb360 100644 --- a/src/GalaxyCheck.Xunit.Tests/GlobalConfigurationTests/AboutLocatingGlobalConfiguration.cs +++ b/src/GalaxyCheck.Xunit.Tests/GlobalConfigurationTests/AboutLocatingGlobalConfiguration.cs @@ -12,7 +12,7 @@ public void ItWorks() { var instance = GlobalConfiguration.Instance; - instance.DefaultIterations.Should().Be(420); + instance.Properties.DefaultIterations.Should().Be(420); } } @@ -20,7 +20,7 @@ public class MyConfigureGlobal : IConfigureGlobal { public void Configure(IGlobalConfiguration instance) { - instance.DefaultIterations = 420; + instance.Properties.DefaultIterations = 420; } } } diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/ConfigureGalaxyCheck.cs b/src/GalaxyCheck.Xunit.Tests/IntegrationSample/ConfigureGalaxyCheck.cs deleted file mode 100644 index 31dc7221..00000000 --- a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/ConfigureGalaxyCheck.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GalaxyCheck.Configuration; - -namespace IntegrationSample -{ - internal class ConfigureGalaxyCheck : IConfigureGlobal - { - public void Configure(IGlobalConfiguration instance) - { - instance.DefaultIterations = 100; - } - } -} diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationTests.cs b/src/GalaxyCheck.Xunit.Tests/IntegrationTests.cs deleted file mode 100644 index 786ce897..00000000 --- a/src/GalaxyCheck.Xunit.Tests/IntegrationTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -using FluentAssertions; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; -using Xunit; - -namespace Tests -{ - public class IntegrationTests : IClassFixture - { - private readonly TestSuiteFixture _fixture; - - public IntegrationTests(TestSuiteFixture fixture) - { - _fixture = fixture; - } - - [Theory] - [InlineData("InfallibleVoidProperty")] - [InlineData("InfallibleVoidPropertyAsync")] - [InlineData("InfallibleBooleanProperty")] - [InlineData("InfallibleBooleanPropertyAsync")] - [InlineData("InfallibleReturnedProperty")] - [InlineData("InfallibleReturnedPropertyAsync")] - [InlineData("PropertyWithGenFromGenFactory")] - [InlineData("PropertyWithGenFromGenFactoryAsync")] - [InlineData("PropertyWithGenFromMemberGen")] - [InlineData("PropertyWithGenFromMemberGenAsync")] - public void PassingProperties(string testName) - { - var testResult = _fixture.FindTestResult(testName); - - testResult.Outcome.Should().Be("Passed"); - } - - [Theory] - [InlineData("FallibleVoidProperty")] - [InlineData("FallibleVoidPropertyAsync")] - [InlineData("FallibleBooleanProperty")] - [InlineData("FallibleBooleanPropertyAsync")] - [InlineData("FallibleReturnedProperty")] - [InlineData("FallibleReturnedPropertyAsync")] - public void FailingProperties(string testName) - { - var testResult = _fixture.FindTestResult(testName); - - testResult.Outcome.Should().Be("Failed"); - testResult.Message.Should().StartWith("GalaxyCheck.Runners.PropertyFailedException"); - } - - [Theory] - [InlineData("SampleVoidProperty")] - [InlineData("SampleVoidPropertyAsync")] - public void SampledProperties(string testName) - { - var testResult = _fixture.FindTestResult(testName); - - testResult.Outcome.Should().Be("Failed"); - testResult.Message.Should().StartWith("GalaxyCheck.SampleException : Test case failed to prevent false-positives."); - } - - public record Invocation(object[] InjectedParameters); - - public record TestResult(string TestName, string Outcome, string Message, ImmutableList Invocations); - - public class TestSuiteFixture : IAsyncLifetime - { - private static readonly string CurrentDirectory = Directory.GetCurrentDirectory(); - private static readonly string TestReportFileName = Path.Combine(CurrentDirectory, $"test_result_{DateTime.UtcNow.Ticks}.trx"); - - public ImmutableList TestResults { get; private set; } = default!; - - public async Task InitializeAsync() - { - var currentProjectDir = Enumerable - .Range(0, 3) - .Aggregate(new DirectoryInfo(CurrentDirectory), (acc, _) => acc.Parent!); - var integrationProjectDir = Path.Combine(currentProjectDir.FullName, "IntegrationSample"); - - var testProcess = Process.Start(new ProcessStartInfo - { - FileName = "dotnet", - Arguments = $"test {integrationProjectDir} --logger \"trx;LogFileName={TestReportFileName}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - }); - - var standardOutput = await testProcess!.StandardOutput.ReadToEndAsync(); - var standardError = await testProcess!.StandardError.ReadToEndAsync(); - - var testReport = File.ReadAllText(TestReportFileName); - - var testResults = - from descendent in XElement.Parse(testReport).Descendants() - where descendent.Name.LocalName == "UnitTestResult" - select ElementToTestResult(descendent); - - TestResults = testResults.ToImmutableList(); - } - - public Task DisposeAsync() => Task.CompletedTask; - - public TestResult FindTestResult(string testName) => TestResults.Single(x => x.TestName == testName); - - private static TestResult ElementToTestResult(XElement element) - { - var attributes = element.Attributes(); - - var message = ( - from desc in element.Descendants() - where desc.Name.LocalName.Contains("Message") - select desc.Value).SingleOrDefault(); - - return new TestResult( - attributes.Single(a => a.Name.LocalName.Contains("testName")).Value.Split('.').Last(), - attributes.Single(a => a.Name.LocalName.Contains("outcome")).Value, - message!, - new List().ToImmutableList()); - } - } - } -} diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationTests/GenSnapshotTests.Snapshooter.cs b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/GenSnapshotTests.Snapshooter.cs new file mode 100644 index 00000000..c16fc0f1 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/GenSnapshotTests.Snapshooter.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using Xunit; + +namespace Tests.IntegrationTests +{ + public class GenSnapshotTestsSnapshooter : IClassFixture + { + public class Fixture : TestSuiteFixture + { + public Fixture() : base("IntegrationSample_GenSnaphot_Snapshooter") + { + } + } + + private readonly Fixture _fixture; + + public GenSnapshotTestsSnapshooter(Fixture fixture) + { + _fixture = fixture; + } + + [Theory] + [InlineData("TypedOutput")] + [InlineData("ObjectOutput")] + [InlineData("TaskOutput")] + [InlineData("ValueTaskOuput")] + public void Snapshots(string testName) + { + var testResult = _fixture.FindTestResult(testName); + + testResult.Outcome.Should().Be("Passed"); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationTests/PropertyTests.cs b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/PropertyTests.cs new file mode 100644 index 00000000..25af8e50 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/PropertyTests.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using Xunit; + +namespace Tests.IntegrationTests +{ + public class PropertyTests : IClassFixture + { + public class Fixture : TestSuiteFixture + { + public Fixture() : base("IntegrationSample") + { + } + } + + private readonly Fixture _fixture; + + public PropertyTests(Fixture fixture) + { + _fixture = fixture; + } + + [Theory] + [InlineData("InfallibleVoidProperty")] + [InlineData("InfallibleVoidPropertyAsync")] + [InlineData("InfallibleBooleanProperty")] + [InlineData("InfallibleBooleanPropertyAsync")] + [InlineData("InfallibleReturnedProperty")] + [InlineData("InfallibleReturnedPropertyAsync")] + [InlineData("PropertyWithGenFromGenFactory")] + [InlineData("PropertyWithGenFromGenFactoryAsync")] + [InlineData("PropertyWithGenFromMemberGen")] + [InlineData("PropertyWithGenFromMemberGenAsync")] + public void PassingProperties(string testName) + { + var testResult = _fixture.FindTestResult(testName); + + testResult.Outcome.Should().Be("Passed"); + } + + [Theory] + [InlineData("FallibleVoidProperty")] + [InlineData("FallibleVoidPropertyAsync")] + [InlineData("FallibleBooleanProperty")] + [InlineData("FallibleBooleanPropertyAsync")] + [InlineData("FallibleReturnedProperty")] + [InlineData("FallibleReturnedPropertyAsync")] + public void FailingProperties(string testName) + { + var testResult = _fixture.FindTestResult(testName); + + testResult.Outcome.Should().Be("Failed"); + testResult.Message.Should().StartWith("GalaxyCheck.Runners.PropertyFailedException"); + } + + [Theory] + [InlineData("SampleVoidProperty")] + [InlineData("SampleVoidPropertyAsync")] + public void SampledProperties(string testName) + { + var testResult = _fixture.FindTestResult(testName); + + testResult.Outcome.Should().Be("Failed"); + testResult.Message.Should().StartWith("GalaxyCheck.SampleException : Test case failed to prevent false-positives."); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationTests/TestSuiteFixture.cs b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/TestSuiteFixture.cs new file mode 100644 index 00000000..339d5c57 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/IntegrationTests/TestSuiteFixture.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Tests.IntegrationTests +{ + public record Invocation(object[] InjectedParameters); + + public record TestResult(string TestName, string Outcome, string Message, ImmutableList Invocations); + + public abstract class TestSuiteFixture : IAsyncLifetime + { + private readonly string _currentDirectory = Directory.GetCurrentDirectory(); + private readonly string _testReportFileName; + private readonly string _testSuiteName; + + public TestSuiteFixture(string testSuiteName) + { + _testReportFileName = Path.Combine(_currentDirectory, $"test_result_{testSuiteName}_{DateTime.UtcNow.Ticks}.trx"); + _testSuiteName = testSuiteName; + } + + public ImmutableList TestResults { get; private set; } = default!; + + public async Task InitializeAsync() + { + var currentProjectDir = Enumerable + .Range(0, 3) + .Aggregate(new DirectoryInfo(_currentDirectory), (acc, _) => acc.Parent!); + var integrationProjectDir = Path.Combine(currentProjectDir.FullName, "samples", _testSuiteName); + + var testProcess = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"test {integrationProjectDir} --logger \"trx;LogFileName={_testReportFileName}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + }); + + var standardOutput = await testProcess!.StandardOutput.ReadToEndAsync(); + var standardError = await testProcess!.StandardError.ReadToEndAsync(); + + var testReport = File.ReadAllText(_testReportFileName); + + var testResults = + from descendent in XElement.Parse(testReport).Descendants() + where descendent.Name.LocalName == "UnitTestResult" + select ElementToTestResult(descendent); + + TestResults = testResults.ToImmutableList(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + public TestResult FindTestResult(string testName) => TestResults.Single(x => x.TestName == testName); + + private static TestResult ElementToTestResult(XElement element) + { + var attributes = element.Attributes(); + + var message = ( + from desc in element.Descendants() + where desc.Name.LocalName.Contains("Message") + select desc.Value).SingleOrDefault(); + + return new TestResult( + attributes.Single(a => a.Name.LocalName.Contains("testName")).Value.Split('.').Last(), + attributes.Single(a => a.Name.LocalName.Contains("outcome")).Value, + message!, + new List().ToImmutableList()); + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutFactory.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutFactory.cs index 2595d851..4facc712 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutFactory.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutFactory.cs @@ -18,14 +18,14 @@ public void WhenClassNorMethodHasFactoryConfig_ItPassesThroughNull(string testMe { var mockPropertyFactory = MockPropertyFactory(); var testClassType = typeof(PropertiesWithoutFactoryConfig); - var testMethodInfo = GetMethod(testMethodName); + var testMethodInfo = GetMethod(testClassType, testMethodName); PropertyInitializer.Initialize( testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); VerifyFactoryNotPassedThrough(mockPropertyFactory); } @@ -37,14 +37,14 @@ public void ItPassesThroughTheFactoryFromTheMethod(string testMethodName) { var mockPropertyFactory = MockPropertyFactory(); var testClassType = typeof(PropertiesWithoutFactoryConfig); - var testMethodInfo = GetMethod(testMethodName); + var testMethodInfo = GetMethod(testClassType, testMethodName); PropertyInitializer.Initialize( testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); VerifyFactoryPassedThrough(mockPropertyFactory, typeof(GenFactory1)); } @@ -56,14 +56,14 @@ public void ItPassesThroughTheFactoryFromTheClass(string testMethodName) { var mockPropertyFactory = MockPropertyFactory(); var testClassType = typeof(PropertiesWithFactoryConfig); - var testMethodInfo = GetMethod(testMethodName); + var testMethodInfo = GetMethod(testClassType, testMethodName); PropertyInitializer.Initialize( testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); VerifyFactoryPassedThrough(mockPropertyFactory, typeof(GenFactory2)); } @@ -75,14 +75,14 @@ public void AMethodLevelFactoryIsPreferredOverAClassLevelFactory(string testMeth { var mockPropertyFactory = MockPropertyFactory(); var testClassType = typeof(PropertiesWithFactoryConfig); - var testMethodInfo = GetMethod(testMethodName); + var testMethodInfo = GetMethod(testClassType, testMethodName); PropertyInitializer.Initialize( testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); VerifyFactoryPassedThrough(mockPropertyFactory, typeof(GenFactory1)); } @@ -238,9 +238,9 @@ public void SampleWithoutFactoryConfig() } - private static MethodInfo GetMethod(string name) + private static MethodInfo GetMethod(Type type, string name) { - var methodInfo = typeof(PropertiesWithoutFactoryConfig).GetMethod(name, BindingFlags.Public | BindingFlags.Instance); + var methodInfo = type.GetMethod(name, BindingFlags.Public | BindingFlags.Instance); if (methodInfo == null) { diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutIterations.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutIterations.cs index f0c01c18..fe5dd414 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutIterations.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutIterations.cs @@ -47,7 +47,7 @@ public void ItPassesThroughIterations(string testMethodName, int expectedIterati testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration { DefaultIterations = 999 }); + new GlobalPropertyConfiguration { DefaultIterations = 999 }); result.Parameters.Iterations.Should().Be(expectedIterations); } @@ -66,7 +66,7 @@ public void ItPassesThroughTheDefaultIterationsIfNotExplicitlySet(string testMet testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration { DefaultIterations = defaultIterations }); + new GlobalPropertyConfiguration { DefaultIterations = defaultIterations }); result.Parameters.Iterations.Should().Be(defaultIterations); } diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutMemberGenAttribute.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutMemberGenAttribute.cs index e445e20f..5a0242fa 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutMemberGenAttribute.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutMemberGenAttribute.cs @@ -92,7 +92,7 @@ public void ItErrorsWhenReferencePropertyIsNotAGen(string testMethodName) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); test.Should() .Throw() @@ -118,7 +118,7 @@ public void ItPassesThroughGen(string testMethodName) testMethodInfo, new object[] { }, mockPropertyFactory.Object, - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); VerifyGenPassedThrough(mockPropertyFactory, Gen, 0); } @@ -132,7 +132,7 @@ public void ItPassesThroughGenKeyedToCorrectIndex(string testMethodName, int exp var testClassType = typeof(Properties); var testMethodInfo = GetMethod(testMethodName); - PropertyInitializer.Initialize(testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, new GlobalConfiguration()); + PropertyInitializer.Initialize(testClassType, testMethodInfo, new object[] { }, mockPropertyFactory.Object, new GlobalPropertyConfiguration()); VerifyGenPassedThrough(mockPropertyFactory, Gen, expectedIndex); } diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutReplay.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutReplay.cs index bb40b819..56e09cc2 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutReplay.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutReplay.cs @@ -51,7 +51,7 @@ public void ItPassesThroughReplay(string testMethodName, string? expectedReplay) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); result.Parameters.Replay.Should().Be(expectedReplay); } diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSeed.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSeed.cs index 19c22858..3f800d60 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSeed.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSeed.cs @@ -71,7 +71,7 @@ public void ItPassesThroughSeed(string testMethodName, int expectedSeed) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); result.Parameters.Seed.Should().Be(expectedSeed); } @@ -91,7 +91,7 @@ public void ItDoesNotPassThroughSeed(string testMethodName) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); result.Parameters.Seed.Should().Be(null); } diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutShrinkLimit.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutShrinkLimit.cs index 53e382fc..152ab7f3 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutShrinkLimit.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutShrinkLimit.cs @@ -47,7 +47,7 @@ public void ItPassesThroughShrinkLimit(string testMethodName, int expectedShrink testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration() { DefaultShrinkLimit = 999 }); + new GlobalPropertyConfiguration() { DefaultShrinkLimit = 999 }); result.Parameters.ShrinkLimit.Should().Be(expectedShrinkLimit); } @@ -66,7 +66,7 @@ public void ItPassesThroughTheDefaultShrinkLimitIfNotExplicitlySet(string testMe testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration { DefaultShrinkLimit = defaultShrinkLimit }); + new GlobalPropertyConfiguration { DefaultShrinkLimit = defaultShrinkLimit }); result.Parameters.ShrinkLimit.Should().Be(defaultShrinkLimit); } diff --git a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSize.cs b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSize.cs index b126113d..88468b47 100644 --- a/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSize.cs +++ b/src/GalaxyCheck.Xunit.Tests/PropertyInitializerTests/AboutSize.cs @@ -71,7 +71,7 @@ public void ItPassesThroughSize(string testMethodName, int expectedSize) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); result.Parameters.Size.Should().Be(expectedSize); } @@ -91,7 +91,7 @@ public void ItDoesNotPassThroughSize(string testMethodName) testMethodInfo, new object[] { }, new DefaultPropertyFactory(), - new GlobalConfiguration()); + new GlobalPropertyConfiguration()); result.Parameters.Size.Should().Be(null); } diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/AsyncTests.cs b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/AsyncTests.cs similarity index 100% rename from src/GalaxyCheck.Xunit.Tests/IntegrationSample/AsyncTests.cs rename to src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/AsyncTests.cs diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.csproj b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.csproj similarity index 85% rename from src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.csproj rename to src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.csproj index 0a52f549..d4261cf9 100644 --- a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.csproj +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -21,7 +21,7 @@ - + - + \ No newline at end of file diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.sln b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.sln similarity index 90% rename from src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.sln rename to src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.sln index 8694a259..9e3b2887 100644 --- a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/IntegrationSample.sln +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/IntegrationSample.sln @@ -5,9 +5,9 @@ VisualStudioVersion = 16.0.31005.135 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationSample", "IntegrationSample.csproj", "{055541A6-7C18-4DE2-AD25-F0A35DF5D658}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit", "..\..\..\src\GalaxyCheck.Xunit\GalaxyCheck.Xunit.csproj", "{A20D447D-C4FA-4834-BDA5-DD585FA94AC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit", "..\..\..\..\src\GalaxyCheck.Xunit\GalaxyCheck.Xunit.csproj", "{A20D447D-C4FA-4834-BDA5-DD585FA94AC4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck", "..\..\..\src\GalaxyCheck\GalaxyCheck.csproj", "{D688A425-1F1F-45E4-AAB3-4C00096B403A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck", "..\..\..\..\src\GalaxyCheck\GalaxyCheck.csproj", "{D688A425-1F1F-45E4-AAB3-4C00096B403A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/GalaxyCheck.Xunit.Tests/IntegrationSample/Tests.cs b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/Tests.cs similarity index 100% rename from src/GalaxyCheck.Xunit.Tests/IntegrationSample/Tests.cs rename to src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample/Tests.cs diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/AsyncGenSnapshotTests.cs b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/AsyncGenSnapshotTests.cs new file mode 100644 index 00000000..cce56988 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/AsyncGenSnapshotTests.cs @@ -0,0 +1,24 @@ +using GalaxyCheck; +using System.Threading.Tasks; + +namespace IntegrationSample +{ + public class AsyncGenSnapshotTests + { + public record GeneratedInput(int Integer, int Integer2, int Integer3); + + [GenSnapshot(Iterations = 1)] + public async Task TaskOutput(object input) + { + await Task.Delay(1); + return input; + } + + [GenSnapshot(Iterations = 1)] + public async ValueTask ValueTaskOuput(object input) + { + await Task.Delay(1); + return input; + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/ConfigureGalaxyCheck.cs b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/ConfigureGalaxyCheck.cs new file mode 100644 index 00000000..994d0039 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/ConfigureGalaxyCheck.cs @@ -0,0 +1,22 @@ +using GalaxyCheck.Configuration; +using Snapshooter; +using Snapshooter.Xunit; +using System.IO; +using System.Threading.Tasks; + +namespace IntegrationSample +{ + internal class ConfigureGalaxyCheck : IConfigureGlobal + { + public void Configure(IGlobalConfiguration instance) + { + instance.GenSnapshots.AssertSnapshotMatches = (snapshot) => + { + var snapshotName = $"{snapshot.TestClassName}.{snapshot.TestMethodName}_{snapshot.Seed}.snap"; + var directoryName = Path.GetDirectoryName(snapshot.TestFileName)!; + Snapshot.Match(snapshot.Output, new SnapshotFullName(snapshotName, directoryName)); + return Task.CompletedTask; + }; + } + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/GenSnapshotTests.cs b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/GenSnapshotTests.cs new file mode 100644 index 00000000..0a77362b --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/GenSnapshotTests.cs @@ -0,0 +1,15 @@ +using GalaxyCheck; + +namespace IntegrationSample +{ + public class GenSnapshotTests + { + public record GeneratedInput(int Integer, int Integer2, int Integer3); + + [GenSnapshot(Iterations = 1)] + public GeneratedInput TypedOutput(GeneratedInput input) => input; + + [GenSnapshot(Iterations = 1)] + public object ObjectOutput(object input) => input; + } +} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.csproj b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.csproj new file mode 100644 index 00000000..2a0d64d0 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + 9.0 + false + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.sln b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.sln new file mode 100644 index 00000000..46fb3bd1 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/IntegrationSample_GenSnaphot_Snapshooter.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationSample_GenSnaphot_Snapshooter", "IntegrationSample_GenSnaphot_Snapshooter.csproj", "{055541A6-7C18-4DE2-AD25-F0A35DF5D658}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit", "..\..\..\..\src\GalaxyCheck.Xunit\GalaxyCheck.Xunit.csproj", "{A20D447D-C4FA-4834-BDA5-DD585FA94AC4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck", "..\..\..\..\src\GalaxyCheck\GalaxyCheck.csproj", "{D688A425-1F1F-45E4-AAB3-4C00096B403A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {055541A6-7C18-4DE2-AD25-F0A35DF5D658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {055541A6-7C18-4DE2-AD25-F0A35DF5D658}.Debug|Any CPU.Build.0 = Debug|Any CPU + {055541A6-7C18-4DE2-AD25-F0A35DF5D658}.Release|Any CPU.ActiveCfg = Release|Any CPU + {055541A6-7C18-4DE2-AD25-F0A35DF5D658}.Release|Any CPU.Build.0 = Release|Any CPU + {A20D447D-C4FA-4834-BDA5-DD585FA94AC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A20D447D-C4FA-4834-BDA5-DD585FA94AC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A20D447D-C4FA-4834-BDA5-DD585FA94AC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A20D447D-C4FA-4834-BDA5-DD585FA94AC4}.Release|Any CPU.Build.0 = Release|Any CPU + {D688A425-1F1F-45E4-AAB3-4C00096B403A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D688A425-1F1F-45E4-AAB3-4C00096B403A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D688A425-1F1F-45E4-AAB3-4C00096B403A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D688A425-1F1F-45E4-AAB3-4C00096B403A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9CDA2348-7831-4F31-BD45-B8FE6DD80313} + EndGlobalSection +EndGlobal diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.TaskOutput_0.snap b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.TaskOutput_0.snap new file mode 100644 index 00000000..f1177889 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.TaskOutput_0.snap @@ -0,0 +1 @@ +{} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.ValueTaskOuput_0.snap b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.ValueTaskOuput_0.snap new file mode 100644 index 00000000..f1177889 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/AsyncGenSnapshotTests.ValueTaskOuput_0.snap @@ -0,0 +1 @@ +{} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.ObjectOutput_0.snap b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.ObjectOutput_0.snap new file mode 100644 index 00000000..f1177889 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.ObjectOutput_0.snap @@ -0,0 +1 @@ +{} diff --git a/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.TypedOutput_0.snap b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.TypedOutput_0.snap new file mode 100644 index 00000000..c1757de7 --- /dev/null +++ b/src/GalaxyCheck.Xunit.Tests/samples/IntegrationSample_GenSnaphot_Snapshooter/__snapshots__/GenSnapshotTests.TypedOutput_0.snap @@ -0,0 +1,5 @@ +{ + "Integer": 1502200049, + "Integer2": -940475173, + "Integer3": -917883947 +} diff --git a/src/GalaxyCheck.Xunit/Configuration/IGlobalConfiguration.cs b/src/GalaxyCheck.Xunit/Configuration/IGlobalConfiguration.cs index c845bec5..8706c444 100644 --- a/src/GalaxyCheck.Xunit/Configuration/IGlobalConfiguration.cs +++ b/src/GalaxyCheck.Xunit/Configuration/IGlobalConfiguration.cs @@ -1,15 +1,43 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace GalaxyCheck.Configuration { public interface IGlobalConfiguration + { + IGlobalPropertyConfiguration Properties { get; } + + IGlobalGenSnapshotConfiguration GenSnapshots { get; } + } + + public interface IGlobalPropertyConfiguration { int DefaultIterations { get; set; } int DefaultShrinkLimit { get; set; } } + + public interface IGlobalGenSnapshotConfiguration + { + int DefaultIterations { get; set; } + + int DefaultSize { get; set; } + + Func? AssertSnapshotMatches { get; set; } + } + + public interface ISnapshot + { + object?[] Input { get; } + + object? Output { get; } + + string TestFileName { get; } + + string TestClassName { get; } + + string TestMethodName { get; } + + int Seed { get; } + } } diff --git a/src/GalaxyCheck.Xunit/GenSnapshotAttribute.cs b/src/GalaxyCheck.Xunit/GenSnapshotAttribute.cs new file mode 100644 index 00000000..0fbc5395 --- /dev/null +++ b/src/GalaxyCheck.Xunit/GenSnapshotAttribute.cs @@ -0,0 +1,57 @@ +using System; +using System.Runtime.CompilerServices; +using Xunit; +using Xunit.Sdk; + +namespace GalaxyCheck +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("GalaxyCheck.Internal.GenSnapshotDiscoverer", "GalaxyCheck.Xunit")] + public sealed class GenSnapshotAttribute : FactAttribute + { + public GenSnapshotAttribute([CallerFilePath] string? testFilePath = null) + { + TestFileName = testFilePath; + } + + public string? TestFileName { get; } + + public int Iterations + { + get + { + if (NullableIterations == null) + { + throw new InvalidOperationException($"{nameof(Iterations)} was not set, and does not have a default value"); + } + + throw new InvalidOperationException(""); + } + set + { + NullableIterations = value; + } + } + + internal int? NullableSize { get; private set; } + + public int Size + { + get + { + if (NullableSize == null) + { + throw new InvalidOperationException($"{nameof(Size)} was not set, and does not have a default value"); + } + + throw new InvalidOperationException(""); + } + set + { + NullableSize = value; + } + } + + internal int? NullableIterations { get; private set; } + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/DefaultGenSnapshotFactory.cs b/src/GalaxyCheck.Xunit/Internal/DefaultGenSnapshotFactory.cs new file mode 100644 index 00000000..254b33d9 --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/DefaultGenSnapshotFactory.cs @@ -0,0 +1,12 @@ +using GalaxyCheck.Gens; +using System.Collections.Generic; +using System.Reflection; + +namespace GalaxyCheck.Internal +{ + internal class DefaultParametersGenFactory : IParametersGenFactory + { + public IGen CreateParametersGen(MethodInfo methodInfo, IGenFactory? genFactory, IReadOnlyDictionary customGens) => + Gen.Parameters(methodInfo, genFactory, customGens); + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GenSnapshotDiscoverer.cs b/src/GalaxyCheck.Xunit/Internal/GenSnapshotDiscoverer.cs new file mode 100644 index 00000000..2129421d --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GenSnapshotDiscoverer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace GalaxyCheck.Internal +{ + public class GenSnapshotDiscoverer : IXunitTestCaseDiscoverer + { + private readonly IMessageSink _messageSink; + + public GenSnapshotDiscoverer(IMessageSink messageSink) + { + _messageSink = messageSink; + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + yield return new GenSnapshotTestCase( + _messageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod); + } + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GenSnapshotInitializer.cs b/src/GalaxyCheck.Xunit/Internal/GenSnapshotInitializer.cs new file mode 100644 index 00000000..318f810d --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GenSnapshotInitializer.cs @@ -0,0 +1,74 @@ +using GalaxyCheck.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace GalaxyCheck.Internal +{ + internal record GenSnapshotInitializationResult( + object TestClassInstance, + string TestFileName, + Func AssertSnapshotMatches, + IGen ParametersGen, + IReadOnlyList Seeds, + int Size, + string? SkipReason) + { + public bool ShouldSkip => SkipReason != null; + } + + internal static class GenSnapshotInitializer + { + private static readonly IReadOnlySet InvalidReturnTypes = new HashSet(new[] { typeof(void), typeof(Task), typeof(ValueTask) }); + + public static GenSnapshotInitializationResult Initialize( + Type testClassType, + MethodInfo testMethodInfo, + object[] constructorArguments, + IParametersGenFactory parametersGenFactory, + IGlobalGenSnapshotConfiguration config) + { + var assertSnapshotMatches = config.AssertSnapshotMatches; + if (assertSnapshotMatches == null) + { + // TODO: Help article in exception message + throw new Exception($"Configuration value \"{nameof(IGlobalGenSnapshotConfiguration.AssertSnapshotMatches)}\" needs to be set in order to use gen snapshots"); + } + + if (InvalidReturnTypes.Contains(testMethodInfo.ReturnType)) + { + throw new Exception("Test method must return a value for snapshotting, or a task containing a value"); + } + + var testClassInstance = Activator.CreateInstance(testClassType, constructorArguments)!; + var genSnapshotAttribute = testMethodInfo.GetCustomAttributes(inherit: true).Single(); + + var testFileName = genSnapshotAttribute.TestFileName; + if (testFileName == null) + { + // TODO: GitHub issues link + throw new Exception("Test is incompatible for some reason. Please raise an issue in GitHub."); + } + + var genFactory = ReflectionHelpers.TryResolveGenFactory(testClassType, testMethodInfo); + var customGens = ReflectionHelpers.ResolveCustomGens(testClassInstance, testMethodInfo); + var parametersGen = parametersGenFactory.CreateParametersGen(testMethodInfo, genFactory, customGens); + + var iterations = genSnapshotAttribute.NullableIterations ?? config.DefaultIterations; + var seeds = Enumerable.Range(0, iterations).ToList(); + + var size = genSnapshotAttribute.NullableSize ?? config.DefaultSize; + + return new GenSnapshotInitializationResult( + testClassInstance, + testFileName, + assertSnapshotMatches, + parametersGen, + seeds, + size, + genSnapshotAttribute.Skip?.Length > 0 ? genSnapshotAttribute.Skip : null); + } + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GenSnapshotRunner.cs b/src/GalaxyCheck.Xunit/Internal/GenSnapshotRunner.cs new file mode 100644 index 00000000..d1e988c6 --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GenSnapshotRunner.cs @@ -0,0 +1,107 @@ +using GalaxyCheck.Configuration; +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace GalaxyCheck.Internal +{ + internal static class GenSnapshotRunner + { + public static Task<(Exception?, decimal)> Run( + GenSnapshotInitializationResult initialization, + Type testClass, + MethodInfo testMethod, + int seedIndex) => RunTestFunction(() => RunSnapshot( + initialization.TestFileName, + testClass.Name, + testMethod.Name, + (parameters) => InvokeTestMethod(initialization.TestClassInstance, testMethod, parameters), + initialization.AssertSnapshotMatches, + initialization.ParametersGen, + initialization.Seeds[seedIndex], + initialization.Size)); + + private static async Task<(Exception?, decimal)> RunTestFunction(Func testFunction) + { + var startTime = DateTime.UtcNow; + + Exception? exception = null; + decimal executionTime; + try + { + await testFunction(); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + executionTime = (decimal)(DateTime.UtcNow - startTime).TotalSeconds; + } + + return (exception, executionTime); + } + + private static async Task InvokeTestMethod( + object testClassInstance, + MethodInfo testMethodInfo, + object[] parameters) + { + var result = testMethodInfo.Invoke(testClassInstance, parameters); + + return await DynamicTaskUnwrap(result); + } + + private static async Task DynamicTaskUnwrap(object? result) + { + if (result == null) return result; + + var resultType = result.GetType(); + if (resultType.IsGenericType == false) return result; + + var genericArgument = resultType.GetGenericArguments().First(); + + var genericTask = typeof(Task<>).MakeGenericType(genericArgument); + if (resultType.IsAssignableTo(genericTask)) + { + await(Task)result; + return genericTask.GetProperty(nameof(Task.Result))!.GetValue(result); + } + + var genericValueTask = typeof(ValueTask<>).MakeGenericType(genericArgument); + if (resultType.IsAssignableTo(genericValueTask)) + { + await((Task)genericValueTask.GetMethod(nameof(ValueTask.AsTask))!.Invoke(result, Array.Empty())!); + return genericValueTask.GetProperty(nameof(ValueTask.Result))!.GetValue(result); + } + + return result; + } + + private static async Task RunSnapshot( + string testFileName, + string testClassName, + string testMethodName, + Func> invokeTestMethod, + Func assertSnapshotMatches, + IGen parametersGen, + int seed, + int size) + { + var input = parametersGen.Advanced.SetRngWaypoint().SampleOne(seed: seed, size: size); + var output = await invokeTestMethod(input); + var snapshot = new Snapshot(input, output, testFileName, testClassName, testMethodName, seed); + await assertSnapshotMatches(snapshot); + } + + private record Snapshot( + object?[] Input, + object? Output, + string TestFileName, + string TestClassName, + string TestMethodName, + int Seed) : ISnapshot; + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GenSnapshotTestCase.cs b/src/GalaxyCheck.Xunit/Internal/GenSnapshotTestCase.cs new file mode 100644 index 00000000..ff473c20 --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GenSnapshotTestCase.cs @@ -0,0 +1,102 @@ +using GalaxyCheck; +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace GalaxyCheck.Internal +{ + public class GenSnapshotTestCase : XunitTestCase + { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public GenSnapshotTestCase() + { + } + + public GenSnapshotTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + } + + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + EnsureInitialized(); + + messageBus.QueueMessage(new TestCaseStarting(this)); + + var startTime = DateTime.UtcNow; + var test = new XunitTest(this, DisplayName); + var testOutputHelper = constructorArguments + .OfType() + .FirstOrDefault() ?? new TestOutputHelper(); + testOutputHelper.Initialize(messageBus, test); + + Task Fail(Exception ex) + { + var executionTime = (decimal)(DateTime.UtcNow - startTime).TotalSeconds; + messageBus.QueueMessage(new TestFailed(test, executionTime, testOutputHelper!.Output, ex)); + return Task.FromResult(new RunSummary() { Total = 0, Failed = 1 }); + } + + var testClassType = TestMethod.TestClass.Class.ToRuntimeType(); + var testMethod = TestMethod.Method.ToRuntimeMethod(); + + GenSnapshotInitializationResult? initialization; + try + { + initialization = GenSnapshotInitializer.Initialize( + testClassType, + testMethod, + constructorArguments, + new DefaultParametersGenFactory(), + GlobalConfiguration.Instance.GenSnapshots); + } + catch (Exception exception) + { + return await Fail(exception); + } + + if (initialization.ShouldSkip) + { + messageBus.QueueMessage(new TestSkipped(test, initialization.SkipReason)); + return new RunSummary() { Skipped = initialization.Seeds.Count }; + } + + var results = await Task.WhenAll(initialization.Seeds + .Select(async (_, seedIndex) => + { + var (exception, executionTime) = await GenSnapshotRunner.Run(initialization, testClassType, testMethod, seedIndex); + + if (exception == null) + { + messageBus.QueueMessage(new TestPassed(test, executionTime, testOutputHelper!.Output)); + return true; + } + else + { + messageBus.QueueMessage(new TestFailed(test, executionTime, testOutputHelper!.Output, exception)); + return false; + } + }) + .ToList()); + + // TODO: Properly measure execution time + var failures = results.Count(result => result == false); + return new RunSummary() { Total = results.Count(), Failed = failures }; + } + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GlobalConfiguration.cs b/src/GalaxyCheck.Xunit/Internal/GlobalConfiguration.cs index ab624979..ff132a95 100644 --- a/src/GalaxyCheck.Xunit/Internal/GlobalConfiguration.cs +++ b/src/GalaxyCheck.Xunit/Internal/GlobalConfiguration.cs @@ -7,9 +7,10 @@ namespace GalaxyCheck.Internal { internal class GlobalConfiguration : IGlobalConfiguration { - public int DefaultIterations { get; set; } = 100; + public IGlobalPropertyConfiguration Properties { get; } = new GlobalPropertyConfiguration(); + + public IGlobalGenSnapshotConfiguration GenSnapshots { get; } = new GlobalGenSnapshotConfiguration(); - public int DefaultShrinkLimit { get; set; } = 100; private static object _instanceLock = new(); private static GlobalConfiguration? _instance = null; @@ -60,5 +61,6 @@ where type.GetInterfaces().Contains(typeof(IConfigureGlobal)) return instance; } + } } diff --git a/src/GalaxyCheck.Xunit/Internal/GlobalGenSnapshotConfiguration.cs b/src/GalaxyCheck.Xunit/Internal/GlobalGenSnapshotConfiguration.cs new file mode 100644 index 00000000..81a7e661 --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GlobalGenSnapshotConfiguration.cs @@ -0,0 +1,15 @@ +using GalaxyCheck.Configuration; +using System; +using System.Threading.Tasks; + +namespace GalaxyCheck.Internal +{ + internal class GlobalGenSnapshotConfiguration : IGlobalGenSnapshotConfiguration + { + public int DefaultIterations { get; set; } = 3; + + public int DefaultSize { get; set; } = 100; + + public Func? AssertSnapshotMatches { get; set; } = null; + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/GlobalPropertyConfiguration.cs b/src/GalaxyCheck.Xunit/Internal/GlobalPropertyConfiguration.cs new file mode 100644 index 00000000..4d46c5ad --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/GlobalPropertyConfiguration.cs @@ -0,0 +1,11 @@ +using GalaxyCheck.Configuration; + +namespace GalaxyCheck.Internal +{ + internal class GlobalPropertyConfiguration : IGlobalPropertyConfiguration + { + public int DefaultIterations { get; set; } = 100; + + public int DefaultShrinkLimit { get; set; } = 100; + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/IParametersGenFactory.cs b/src/GalaxyCheck.Xunit/Internal/IParametersGenFactory.cs new file mode 100644 index 00000000..824355b9 --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/IParametersGenFactory.cs @@ -0,0 +1,14 @@ +using GalaxyCheck.Gens; +using System.Collections.Generic; +using System.Reflection; + +namespace GalaxyCheck.Internal +{ + public interface IParametersGenFactory + { + IGen CreateParametersGen( + MethodInfo methodInfo, + IGenFactory? genFactory, + IReadOnlyDictionary customGens); + } +} diff --git a/src/GalaxyCheck.Xunit/Internal/PropertyInitializer.cs b/src/GalaxyCheck.Xunit/Internal/PropertyInitializer.cs index 1a65ea68..382f2fa2 100644 --- a/src/GalaxyCheck.Xunit/Internal/PropertyInitializer.cs +++ b/src/GalaxyCheck.Xunit/Internal/PropertyInitializer.cs @@ -1,7 +1,5 @@ using GalaxyCheck.Configuration; -using GalaxyCheck.Gens; using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -22,20 +20,20 @@ public static PropertyInitializationResult Initialize( MethodInfo testMethodInfo, object[] constructorArguments, IPropertyFactory propertyFactory, - IGlobalConfiguration globalConfig) + IGlobalPropertyConfiguration config) { var testClassInstance = Activator.CreateInstance(testClassType, constructorArguments)!; var propertyAttribute = testMethodInfo.GetCustomAttributes(inherit: true).Single(); - var genFactory = TryResolveGenFactory(testClassType, testMethodInfo); - var customGens = ResolveCustomGens(testClassInstance, testMethodInfo); + var genFactory = ReflectionHelpers.TryResolveGenFactory(testClassType, testMethodInfo); + var customGens = ReflectionHelpers.ResolveCustomGens(testClassInstance, testMethodInfo); var property = propertyFactory.CreateProperty(testMethodInfo, testClassInstance, genFactory, customGens); var replay = testMethodInfo.GetCustomAttributes().SingleOrDefault()?.Replay; var propertyRunParameters = new PropertyRunParameters( Property: property, - Iterations: propertyAttribute.NullableIterations ?? globalConfig.DefaultIterations, - ShrinkLimit: propertyAttribute.NullableShrinkLimit ?? globalConfig.DefaultShrinkLimit, + Iterations: propertyAttribute.NullableIterations ?? config.DefaultIterations, + ShrinkLimit: propertyAttribute.NullableShrinkLimit ?? config.DefaultShrinkLimit, Replay: replay, Seed: replay == null ? propertyAttribute.NullableSeed : null, Size: replay == null ? propertyAttribute.NullableSize : null); @@ -45,60 +43,5 @@ public static PropertyInitializationResult Initialize( propertyRunParameters, propertyAttribute.Skip?.Length > 0 ? propertyAttribute.Skip : null); } - - private static IGenFactory? TryResolveGenFactory(Type testClassType, MethodInfo testMethodInfo) - { - var genFactoryAttribute = - testMethodInfo.GetCustomAttributes(inherit: true).FirstOrDefault() ?? - testClassType.GetCustomAttributes(inherit: true).FirstOrDefault(); - - return genFactoryAttribute?.Value; - } - - private static IReadOnlyDictionary ResolveCustomGens(object testClassInstance, MethodInfo methodInfo) - { - return methodInfo - .GetParameters() - .Select((parameter, i) => - { - var memberGenAttribute = parameter.GetCustomAttribute(); - return memberGenAttribute == null - ? null : - new { gen = ResolveMemberGen(testClassInstance, memberGenAttribute.MemberName), parameterIndex = i }; - }) - .Where(x => x != null) - .ToDictionary(x => x!.parameterIndex, x => x!.gen); - } - - private static IGen ResolveMemberGen(object target, string memberName) - { - var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; - - var property = target.GetType().GetProperty(memberName, bindingFlags); - if (property != null) - { - return CastToGen(property.GetValue(target)!, memberName); - } - - var field = target.GetType().GetField(memberName, bindingFlags); - if (field != null) - { - return CastToGen(field.GetValue(target)!, memberName); - } - - return Gen.Constant(null); - - static IGen CastToGen(object value, string memberName) - { - var gen = value as IGen; - - if (gen == null) - { - throw new Exception($"Expected member '{memberName}' to be an instance of '{typeof(IGen).FullName}', but it had a value of type '{value.GetType().FullName}'"); - } - - return gen; - } - } } } diff --git a/src/GalaxyCheck.Xunit/Internal/PropertyTestCase.cs b/src/GalaxyCheck.Xunit/Internal/PropertyTestCase.cs index e15d6a10..4eb30cbf 100644 --- a/src/GalaxyCheck.Xunit/Internal/PropertyTestCase.cs +++ b/src/GalaxyCheck.Xunit/Internal/PropertyTestCase.cs @@ -73,7 +73,7 @@ RunSummary Skip(string reason) TestMethod.Method.ToRuntimeMethod(), constructorArguments, new DefaultPropertyFactory(), - GlobalConfiguration.Instance); + GlobalConfiguration.Instance.Properties); } catch (Exception exception) { diff --git a/src/GalaxyCheck.Xunit/Internal/ReflectionHelpers.cs b/src/GalaxyCheck.Xunit/Internal/ReflectionHelpers.cs new file mode 100644 index 00000000..fe8913ac --- /dev/null +++ b/src/GalaxyCheck.Xunit/Internal/ReflectionHelpers.cs @@ -0,0 +1,66 @@ +using GalaxyCheck.Gens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GalaxyCheck.Internal +{ + internal static class ReflectionHelpers + { + public static IGenFactory? TryResolveGenFactory(Type testClassType, MethodInfo testMethodInfo) + { + var genFactoryAttribute = + testMethodInfo.GetCustomAttributes(inherit: true).FirstOrDefault() ?? + testClassType.GetCustomAttributes(inherit: true).FirstOrDefault(); + + return genFactoryAttribute?.Value; + } + + public static IReadOnlyDictionary ResolveCustomGens(object testClassInstance, MethodInfo methodInfo) + { + return methodInfo + .GetParameters() + .Select((parameter, i) => + { + var memberGenAttribute = parameter.GetCustomAttribute(); + return memberGenAttribute == null + ? null : + new { gen = ResolveMemberGen(testClassInstance, memberGenAttribute.MemberName), parameterIndex = i }; + }) + .Where(x => x != null) + .ToDictionary(x => x!.parameterIndex, x => x!.gen); + } + + private static IGen ResolveMemberGen(object target, string memberName) + { + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + + var property = target.GetType().GetProperty(memberName, bindingFlags); + if (property != null) + { + return CastToGen(property.GetValue(target)!, memberName); + } + + var field = target.GetType().GetField(memberName, bindingFlags); + if (field != null) + { + return CastToGen(field.GetValue(target)!, memberName); + } + + return Gen.Constant(null); + + static IGen CastToGen(object value, string memberName) + { + var gen = value as IGen; + + if (gen == null) + { + throw new Exception($"Expected member '{memberName}' to be an instance of '{typeof(IGen).FullName}', but it had a value of type '{value.GetType().FullName}'"); + } + + return gen; + } + } + } +} diff --git a/src/GalaxyCheck/Gens/ParametersGen.cs b/src/GalaxyCheck/Gens/ParametersGen.cs index bd034458..9335ee3a 100644 --- a/src/GalaxyCheck/Gens/ParametersGen.cs +++ b/src/GalaxyCheck/Gens/ParametersGen.cs @@ -67,11 +67,12 @@ private static IGen CreateGen( $"parameter index '{invalidIndicies.First()}' of custom generator was not valid"); } - var gens = parameters.Select((parameterInfo, parameterIndex) => - { - var customGen = customGens.ContainsKey(parameterIndex) ? customGens[parameterIndex] : null; - return ResolveGen(genFactory, customGen, parameterInfo); - }); + var gens = parameters + .Select((parameterInfo, parameterIndex) => + { + var customGen = customGens.ContainsKey(parameterIndex) ? customGens[parameterIndex] : null; + return ResolveGen(genFactory, customGen, parameterInfo).Advanced.ReferenceRngWaypoint(rng => rng.Influence(parameterIndex)); + }); return Gen.Zip(gens).Select(xs => xs.ToArray()); }