From d3b20b4abb082e960b8bd9ff67d81243c0d08c58 Mon Sep 17 00:00:00 2001 From: nkosi23 Date: Wed, 11 Dec 2024 13:02:17 +0100 Subject: [PATCH] - Added additional F# unit tests for good measure - Required putting the Target type in a separate shared library to avoid a circular dependency - Moved Fsharp acceptance tests to different test classes to avoid affecting other tests due to the specific serializer used for F# --- src/FSharpTypes/FSharpTypes.fsproj | 6 +- src/FSharpTypes/Library.fs | 24 ++ .../Acceptance/Support/DefaultQueryFixture.cs | 40 ++- src/LinqTests/Acceptance/where_clauses.cs | 8 +- .../Acceptance/where_clauses_fsharp.cs | 48 ++++ src/LinqTests/LinqTests.csproj | 5 +- src/Marten.Testing/Documents/Target.cs | 255 ------------------ src/Marten.sln | 8 + src/Marten/StoreOptions.Identity.cs | 1 + 9 files changed, 123 insertions(+), 272 deletions(-) create mode 100644 src/LinqTests/Acceptance/where_clauses_fsharp.cs delete mode 100644 src/Marten.Testing/Documents/Target.cs diff --git a/src/FSharpTypes/FSharpTypes.fsproj b/src/FSharpTypes/FSharpTypes.fsproj index 186eabb173..2edfa524b2 100644 --- a/src/FSharpTypes/FSharpTypes.fsproj +++ b/src/FSharpTypes/FSharpTypes.fsproj @@ -1,13 +1,17 @@  - net8.0 true 8.0 + net8.0;net7.0;net9.0 + + + + diff --git a/src/FSharpTypes/Library.fs b/src/FSharpTypes/Library.fs index 498c8ab017..24822290ab 100644 --- a/src/FSharpTypes/Library.fs +++ b/src/FSharpTypes/Library.fs @@ -1,6 +1,9 @@ module FSharpTypes open System +open System.Linq.Expressions +open Marten.Testing.Documents +open Microsoft.FSharp.Linq.RuntimeHelpers type OrderId = Id of Guid @@ -14,3 +17,24 @@ type RecordTypeOrderId = { Part1: string; Part2: string } type ArbitraryClass() = member this.Value = "ok" + +let rec stripFSharpFunc (expression: Expression) = + match expression with + | :? MethodCallExpression as callExpression when callExpression.Method.Name = "ToFSharpFunc" -> + stripFSharpFunc callExpression.Arguments.[0] + | _ -> expression + +let toLinqExpression expr = + expr + |> LeafExpressionConverter.QuotationToExpression + |> stripFSharpFunc + |> unbox>> +let greaterThanWithFsharpDateOption = + <@ fun (o1: Target) -> o1.FSharpDateTimeOffsetOption >= Some DateTimeOffset.UtcNow @> |> toLinqExpression +let lesserThanWithFsharpDateOption = <@ (fun (o1: Target) -> o1.FSharpDateTimeOffsetOption <= Some DateTimeOffset.UtcNow ) @> |> toLinqExpression +let greaterThanWithFsharpDecimalOption = <@ (fun (o1: Target) -> o1.FSharpDecimalOption >= Some 5m ) @> |> toLinqExpression +let lesserThanWithFsharpDecimalOption = <@ (fun (o1: Target) -> o1.FSharpDecimalOption <= Some 5m ) @> |> toLinqExpression +let greaterThanWithFsharpStringOption = <@ (fun (o1: Target) -> o1.FSharpStringOption >= Some "MyString" ) @> |> toLinqExpression +let lesserThanWithFsharpStringOption = <@ (fun (o1: Target) -> o1.FSharpStringOption <= Some "MyString" ) @> |> toLinqExpression + + diff --git a/src/LinqTests/Acceptance/Support/DefaultQueryFixture.cs b/src/LinqTests/Acceptance/Support/DefaultQueryFixture.cs index 72a9c95554..8c07a9eb6f 100644 --- a/src/LinqTests/Acceptance/Support/DefaultQueryFixture.cs +++ b/src/LinqTests/Acceptance/Support/DefaultQueryFixture.cs @@ -10,17 +10,31 @@ public class DefaultQueryFixture: TargetSchemaFixture { public DefaultQueryFixture() { - Store = ProvisionStore("linq_querying", options => + Store = ProvisionStore("linq_querying"); + + DuplicatedFieldStore = ProvisionStore("duplicate_fields", o => + { + o.Schema.For() + .Duplicate(x => x.Number) + .Duplicate(x => x.Long) + .Duplicate(x => x.String) + .Duplicate(x => x.Date) + .Duplicate(x => x.Double) + .Duplicate(x => x.Flag) + .Duplicate(x => x.Color) + .Duplicate(x => x.NumberArray); + }); + + FSharpFriendlyStore = ProvisionStore("linq_querying", options => { options.RegisterFSharpOptionValueTypes(); var serializerOptions = JsonFSharpOptions.Default().WithUnwrapOption().ToJsonSerializerOptions(); options.UseSystemTextJsonForSerialization(serializerOptions); }); - - DuplicatedFieldStore = ProvisionStore("duplicate_fields", o => + FSharpFriendlyStoreWithDuplicatedField = ProvisionStore("duplicate_fields", options => { - o.Schema.For() + options.Schema.For() .Duplicate(x => x.Number) .Duplicate(x => x.Long) .Duplicate(x => x.String) @@ -30,9 +44,9 @@ public DefaultQueryFixture() .Duplicate(x => x.Color) .Duplicate(x => x.NumberArray); - o.RegisterFSharpOptionValueTypes(); + options.RegisterFSharpOptionValueTypes(); var serializerOptions = JsonFSharpOptions.Default().WithUnwrapOption().ToJsonSerializerOptions(); - o.UseSystemTextJsonForSerialization(serializerOptions); + options.UseSystemTextJsonForSerialization(serializerOptions); }); SystemTextJsonStore = ProvisionStore("stj_linq", o => @@ -45,5 +59,19 @@ public DefaultQueryFixture() public DocumentStore DuplicatedFieldStore { get; set; } + public DocumentStore FSharpFriendlyStore { get; set; } + public DocumentStore FSharpFriendlyStoreWithDuplicatedField { get; set; } + public DocumentStore Store { get; set; } } + +public static class DefaultQueryFixtureExtensions +{ + public static void UseFSharp(this DocumentStore store) + { + var o = store.Options; + o.RegisterFSharpOptionValueTypes(); + var serializerOptions = JsonFSharpOptions.Default().WithUnwrapOption().ToJsonSerializerOptions(); + o.UseSystemTextJsonForSerialization(serializerOptions); + } +} diff --git a/src/LinqTests/Acceptance/where_clauses.cs b/src/LinqTests/Acceptance/where_clauses.cs index 4ae1179efb..39af8cb436 100644 --- a/src/LinqTests/Acceptance/where_clauses.cs +++ b/src/LinqTests/Acceptance/where_clauses.cs @@ -141,13 +141,6 @@ static where_clauses() @where(x => !x.Flag == true); @where(x => !x.Flag == false); - @where(x => x.FSharpBoolOption == FSharpOption.Some(true)); - @where(x => x.FSharpBoolOption == FSharpOption.Some(false)); - @where(x => x.FSharpDateOption == FSharpOption.Some(DateTime.Now)); - @where(x => x.FSharpIntOption == FSharpOption.Some(300)); - @where(x => x.FSharpStringOption == FSharpOption.Some("My String")); - @where(x => x.FSharpLongOption == FSharpOption.Some(5_000_000)); - // Comparing multiple fields @where(x => x.Number == x.AnotherNumber); @where(x => x.Number < x.AnotherNumber); @@ -198,3 +191,4 @@ public Task with_duplicated_fields(string description) return assertTestCase(description, Fixture.DuplicatedFieldStore); } } + diff --git a/src/LinqTests/Acceptance/where_clauses_fsharp.cs b/src/LinqTests/Acceptance/where_clauses_fsharp.cs new file mode 100644 index 0000000000..00f8b73d6b --- /dev/null +++ b/src/LinqTests/Acceptance/where_clauses_fsharp.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using LinqTests.Acceptance.Support; +using Microsoft.FSharp.Core; +using Xunit.Abstractions; + +namespace LinqTests.Acceptance; + +public class where_clauses_fsharp: LinqTestContext +{ + public where_clauses_fsharp(DefaultQueryFixture fixture, ITestOutputHelper output) : base(fixture) + { + TestOutput = output; + } + + static where_clauses_fsharp() + { + + @where(x => x.FSharpBoolOption == FSharpOption.Some(true)); + @where(x => x.FSharpBoolOption == FSharpOption.Some(false)); + @where(x => x.FSharpDateOption == FSharpOption.Some(DateTime.Now)); + @where(x => x.FSharpIntOption == FSharpOption.Some(300)); + @where(x => x.FSharpStringOption == FSharpOption.Some("My String")); + @where(x => x.FSharpLongOption == FSharpOption.Some(5_000_000)); + + //Comparing options is not a valid syntax in C#, we therefore define these expressions in F# + @where(FSharpTypes.greaterThanWithFsharpDateOption); + @where(FSharpTypes.lesserThanWithFsharpDateOption); + @where(FSharpTypes.greaterThanWithFsharpStringOption); + @where(FSharpTypes.lesserThanWithFsharpStringOption); + @where(FSharpTypes.greaterThanWithFsharpDecimalOption); + @where(FSharpTypes.lesserThanWithFsharpDecimalOption); + } + + [Theory] + [MemberData(nameof(GetDescriptions))] + public Task run_query(string description) + { + return assertTestCase(description, Fixture.FSharpFriendlyStore); + } + + [Theory] + [MemberData(nameof(GetDescriptions))] + public Task with_duplicated_fields(string description) + { + return assertTestCase(description, Fixture.FSharpFriendlyStoreWithDuplicatedField); + } +} diff --git a/src/LinqTests/LinqTests.csproj b/src/LinqTests/LinqTests.csproj index 3905b6de91..e5e5fced57 100644 --- a/src/LinqTests/LinqTests.csproj +++ b/src/LinqTests/LinqTests.csproj @@ -6,6 +6,8 @@ + + @@ -56,9 +58,6 @@ Documents\StringDoc.cs - - Documents\Target.cs - Documents\TargetIntId.cs diff --git a/src/Marten.Testing/Documents/Target.cs b/src/Marten.Testing/Documents/Target.cs deleted file mode 100644 index 98bea40d9d..0000000000 --- a/src/Marten.Testing/Documents/Target.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; -using JasperFx.Core; -using Microsoft.FSharp.Core; - -#nullable enable -namespace Marten.Testing.Documents; - -public enum Colors -{ - Red, - Blue, - Green, - Purple, - Yellow, - Orange -} - -public class Target -{ - private static readonly Random _random = new Random(67); - - private static readonly string[] _strings = - { - "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Violet", "Pink", "Gray", "Black" - }; - - private static readonly string[] _otherStrings = - { - "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" - }; - - public static IEnumerable GenerateRandomData(int number) - { - var i = 0; - while (i < number) - { - yield return Random(true); - - i++; - } - } - - public static Target Random(bool deep = false) - { - var target = new Target(); - target.String = _strings[_random.Next(0, 10)]; - target.FSharpGuidOption = new FSharpOption(Guid.NewGuid()); - target.FSharpIntOption = new FSharpOption(_random.Next(0, 10)); - target.FSharpDateOption = new FSharpOption(DateTime.Now); - target.FSharpDecimalOption = new FSharpOption(_random.Next(0, 10)); - target.FSharpLongOption = new FSharpOption(_random.Next(0, 10)); - target.FSharpStringOption = new FSharpOption(_strings[_random.Next(0, 10)]); - - target.PaddedString = " " + target.String + " "; - target.AnotherString = _otherStrings[_random.Next(0, 10)]; - target.Number = _random.Next(); - target.AnotherNumber = _random.Next(); - target.OtherGuid = Guid.NewGuid(); - - target.Flag = _random.Next(0, 10) > 5; - - target.Float = float.Parse(_random.NextDouble().ToString()); - - target.NumberArray = _random.Next(0, 10) > 8 - ? new[] { _random.Next(0, 10), _random.Next(0, 10), _random.Next(0, 10) } - : Array.Empty(); - - target.NumberArray = target.NumberArray.Distinct().ToArray(); - - switch (_random.Next(0, 2)) - { - case 0: - target.Color = Colors.Blue; - break; - - case 1: - target.Color = Colors.Green; - break; - - default: - target.Color = Colors.Red; - break; - } - - var value = _random.Next(0, 100); - if (value > 10) target.NullableNumber = value; - - if (value > 20) - { - var list = new List(); - for (int i = 0; i < 5; i++) - { - list.Add(_strings[_random.Next(0, 10)]); - } - - target.StringArray = list.Distinct().ToArray(); - } - - target.Long = 100 * _random.Next(); - target.Double = _random.NextDouble(); - target.Long = _random.Next() * 10000; - - target.HowLong = TimeSpan.FromSeconds(target.Long); - - target.Date = DateTime.Today.AddDays(_random.Next(-10000, 10000)); - - if (value > 15) - { - target.NullableDateOffset = DateTimeOffset.Now.Subtract(_random.Next(-60, 60).Seconds()); - } - - if (deep) - { - target.Inner = Random(); - - var number = _random.Next(1, 10); - target.Children = new Target[number]; - for (var i = 0; i < number; i++) - { - target.Children[i] = Random(); - } - - target.StringDict = Enumerable.Range(0, _random.Next(0, 10)).ToDictionary(i => $"key{i}", i => $"value{i}"); - target.String = _strings[_random.Next(0, 10)]; - target.OtherGuid = Guid.NewGuid(); - } - - return target; - } - - public string PaddedString { get; set; } - - public Target() - { - Id = Guid.NewGuid(); - StringDict = new Dictionary(); - StringList = new List(); - GuidDict = new Dictionary(); - } - - public Guid Id { get; set; } - - public int Number { get; set; } - - public int AnotherNumber { get; set; } - - public long Long { get; set; } - public string String { get; set; } - - public FSharpOption FSharpGuidOption { get; set; } - public FSharpOption FSharpIntOption { get; set; } - public FSharpOption FSharpBoolOption { get; set; } - public FSharpOption FSharpLongOption { get; set; } - public FSharpOption FSharpDecimalOption { get; set; } - public FSharpOption FSharpStringOption { get; set; } - public FSharpOption FSharpDateOption { get; set; } - - public string AnotherString { get; set; } - - public string[] StringArray { get; set; } - - public Guid OtherGuid { get; set; } - - public Target Inner { get; set; } - - public Colors Color { get; set; } - - public Colors? NullableEnum { get; set; } - - public bool Flag { get; set; } - - [JsonInclude] // this is needed to make System.Text.Json happy - public string StringField; - - public double Double { get; set; } - public decimal Decimal { get; set; } - public DateTime Date { get; set; } - public DateTimeOffset DateOffset { get; set; } - public DateTimeOffset? NullableDateOffset { get; set; } - - [JsonInclude] // this is needed to make System.Text.Json happy - public float Float; - - public int[] NumberArray { get; set; } - - public string[] TagsArray { get; set; } - - public HashSet TagsHashSet { get; set; } - - public Target[] Children { get; set; } - - public int? NullableNumber { get; set; } - public DateTime? NullableDateTime { get; set; } - public bool? NullableBoolean { get; set; } - public Colors? NullableColor { get; set; } - - public string? NullableString { get; set; } - - public IDictionary StringDict { get; set; } - public Dictionary GuidDict { get; set; } - - public Guid UserId { get; set; } - - public List StringList { get; set; } - - public Guid[] GuidArray { get; set; } - - public TimeSpan HowLong { get; set; } -} - -public class Address -{ - public Address() - { - } - - public Address(string text) - { - var parts = text.ToDelimitedArray(); - Address1 = parts[0]; - City = parts[1]; - StateOrProvince = parts[2]; - } - - public string Address1 { get; set; } - public string Address2 { get; set; } - public string City { get; set; } - public string StateOrProvince { get; set; } - public string Country { get; set; } - public string PostalCode { get; set; } - - public bool Primary { get; set; } - public string Street { get; set; } - public string HouseNumber { get; set; } -} - -public class Squad -{ - public string Id { get; set; } -} - -public class BasketballTeam: Squad -{ -} - -public class FootballTeam: Squad -{ -} - -public class BaseballTeam: Squad -{ -} diff --git a/src/Marten.sln b/src/Marten.sln index 0a0ec93f4c..08c77f71a9 100644 --- a/src/Marten.sln +++ b/src/Marten.sln @@ -109,6 +109,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventAppenderPerfTester", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "StressTests\StressTests.csproj", "{C9D33381-3AD3-4005-B854-F04F10EA837F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinqTestsTypes", "LinqTestsTypes\LinqTestsTypes.csproj", "{0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {C9D33381-3AD3-4005-B854-F04F10EA837F}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9D33381-3AD3-4005-B854-F04F10EA837F}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9D33381-3AD3-4005-B854-F04F10EA837F}.Release|Any CPU.Build.0 = Release|Any CPU + {0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -297,6 +303,8 @@ Global {7F2B0CE1-1365-4AE2-9823-B6DBB1A42C5F} = {91D87D73-EC07-4067-8A64-26A2E4F6BC83} {29E06861-11C7-4917-BA91-162D15538029} = {91D87D73-EC07-4067-8A64-26A2E4F6BC83} {C9D33381-3AD3-4005-B854-F04F10EA837F} = {91D87D73-EC07-4067-8A64-26A2E4F6BC83} + {B1F935FC-55DC-418B-A5DC-6049A5C06871} = {91D87D73-EC07-4067-8A64-26A2E4F6BC83} + {0E4B7C9D-07D8-4F3D-8A42-C369B8D6ED84} = {91D87D73-EC07-4067-8A64-26A2E4F6BC83} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {52B7158F-0A24-47D9-9CF7-3FA94170041A} diff --git a/src/Marten/StoreOptions.Identity.cs b/src/Marten/StoreOptions.Identity.cs index badafab924..ac55123924 100644 --- a/src/Marten/StoreOptions.Identity.cs +++ b/src/Marten/StoreOptions.Identity.cs @@ -165,6 +165,7 @@ public void RegisterFSharpOptionValueTypes() RegisterValueType(typeof(FSharpOption)); RegisterValueType(typeof(FSharpOption)); RegisterValueType(typeof(FSharpOption)); + RegisterValueType(typeof(FSharpOption)); } internal List ValueTypes { get; } = new();