From 2cc6691f0fb48c4fc9e920e8a97e2cddda0bb3d8 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 10 Oct 2026 15:07:31 +0200 Subject: [PATCH] feat: add `FSharp.Azure.Cosmos.Sql` with `EquatableArray` and the `ImmutableArray` patterns Starts src/Cosmos.Sql (FSharp.Azure.Cosmos.Sql, net10.0, FSharp.Core only), the project of the Cosmos DB SQL model of ADR 0001 (sections 4, 6 and 15), with the two collection helpers that the syntax tree is built from, so that they can be reviewed on their own before the tree, the printer and the validator follow. EquatableArray<'T> is a struct over ImmutableArray<'T> with sequence equality and a combined hash code. ImmutableArray's own Equals and EqualityComparer.Default compare the reference of the wrapped array, which a struct tuple, a default-comparer HashSet or a C# caller would see, and a default array never equals an empty one. The nodes of the syntax tree will hold their children in this wrapper, so that their equality is structural under every comparer. The EquatableArray module creates and maps the arrays; its unsafeOfArray wraps an array without copying it, as ImmutableCollectionsMarshal.AsImmutableArray does, for a caller that gives the array away. Implicit conversions to and from ImmutableArray wrap and unwrap without copying; F# applies them without a warning at method arguments, and with warning FS3391 elsewhere. The struct partial active patterns Arr0, Arr1, Arr2, Arr3 and ArrN match an ImmutableArray by its length without allocating: each returns a struct value option or a bool, and its payload is a struct tuple. tests/Cosmos.Sql.Tests is the new emulator-free MSTest project, with 15 tests under the category Collections: a file for the struct, one for the module and one for the patterns. Both projects join the solution and the solution filter, and the agent instructions list them. The library is not packed yet (IsPackable is false): a push to main publishes every packable project to GitHub Packages, and the collection types alone are no package. The commit that gives the package its metadata removes the setting. Co-Authored-By: Claude Opus 5.5 --- .github/copilot-instructions.md | 4 + FSharp.Azure.Cosmos.slnf | 2 + FSharp.Azure.Cosmos.slnx | 2 + src/Cosmos.Sql/AssemblyInfo.fs | 23 ++ src/Cosmos.Sql/EquatableArray.fs | 199 ++++++++++++++++++ src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj | 30 +++ src/Cosmos.Sql/ImmutableArrayPatterns.fs | 74 +++++++ .../EquatableArrayModuleTests.fs | 57 +++++ tests/Cosmos.Sql.Tests/EquatableArrayTests.fs | 161 ++++++++++++++ .../FSharp.Azure.Cosmos.Sql.Tests.fsproj | 36 ++++ .../ImmutableArrayPatternsTests.fs | 38 ++++ tests/Cosmos.Sql.Tests/TestCategories.fs | 18 ++ tests/Cosmos.Sql.Tests/testconfig.json | 9 + 13 files changed, 653 insertions(+) create mode 100644 src/Cosmos.Sql/AssemblyInfo.fs create mode 100644 src/Cosmos.Sql/EquatableArray.fs create mode 100644 src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj create mode 100644 src/Cosmos.Sql/ImmutableArrayPatterns.fs create mode 100644 tests/Cosmos.Sql.Tests/EquatableArrayModuleTests.fs create mode 100644 tests/Cosmos.Sql.Tests/EquatableArrayTests.fs create mode 100644 tests/Cosmos.Sql.Tests/FSharp.Azure.Cosmos.Sql.Tests.fsproj create mode 100644 tests/Cosmos.Sql.Tests/ImmutableArrayPatternsTests.fs create mode 100644 tests/Cosmos.Sql.Tests/TestCategories.fs create mode 100644 tests/Cosmos.Sql.Tests/testconfig.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 46978db5..a1d9cf8e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,6 +12,9 @@ ```text / +├── src/Cosmos.Sql/ – Cosmos DB SQL model (FSharp.Azure.Cosmos.Sql) +│ ├── EquatableArray.fs – immutable array with structural equality +│ └── ImmutableArrayPatterns.fs – active patterns that match an `ImmutableArray` by its length ├── src/Cosmos/ – main library (FSharp.Azure.Cosmos) │ ├── SubStatusCodes.fs – documented Cosmos DB sub-status codes │ ├── Cosmos.fs – core types and container extensions @@ -28,6 +31,7 @@ ├── src/Shared/ – source files that every F# project under src and tests compiles │ └── ValueCollections.fs – `Seq`, `List` and `Array` functions that return `voption` and struct tuples ├── tests/Cosmos.Tests/ – MSTest integration test project +├── tests/Cosmos.Sql.Tests/ – MSTest unit tests of FSharp.Azure.Cosmos.Sql, no emulator needed ├── tests/Cosmos.Tests.Infrastructure/ – shared test fixtures, emulator settings and assertion helpers ├── build/ – FAKE build scripts └── docsSrc/ – FSharp.Formatting documentation source diff --git a/FSharp.Azure.Cosmos.slnf b/FSharp.Azure.Cosmos.slnf index 1a0488cc..c7e70fc3 100644 --- a/FSharp.Azure.Cosmos.slnf +++ b/FSharp.Azure.Cosmos.slnf @@ -2,7 +2,9 @@ "solution": { "path": "FSharp.Azure.Cosmos.slnx", "projects": [ + "src\\Cosmos.Sql\\FSharp.Azure.Cosmos.Sql.fsproj", "src\\Cosmos\\FSharp.Azure.Cosmos.fsproj", + "tests\\Cosmos.Sql.Tests\\FSharp.Azure.Cosmos.Sql.Tests.fsproj", "tests\\Cosmos.Tests.Infrastructure\\FSharp.Azure.Cosmos.Tests.Infrastructure.fsproj", "tests\\Cosmos.Tests\\FSharp.Azure.Cosmos.Tests.fsproj" ] diff --git a/FSharp.Azure.Cosmos.slnx b/FSharp.Azure.Cosmos.slnx index 4ec89bbd..50500575 100644 --- a/FSharp.Azure.Cosmos.slnx +++ b/FSharp.Azure.Cosmos.slnx @@ -10,7 +10,9 @@ + + diff --git a/src/Cosmos.Sql/AssemblyInfo.fs b/src/Cosmos.Sql/AssemblyInfo.fs new file mode 100644 index 00000000..a60652e5 --- /dev/null +++ b/src/Cosmos.Sql/AssemblyInfo.fs @@ -0,0 +1,23 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FSharp.Azure.Cosmos.Sql" + let [] AssemblyProduct = "FSharp.Azure.Cosmos" + let [] AssemblyVersion = "1.2.0" + let [] AssemblyMetadata_ReleaseDate = "2026-10-01T00:00:00.0000000+02:00" + let [] AssemblyFileVersion = "1.2.0" + let [] AssemblyInformationalVersion = "1.2.0" + let [] AssemblyMetadata_ReleaseChannel = "release" + let [] AssemblyMetadata_GitHash = "210cf041a0ce44fcc5a5df369550a77a1f75d436" diff --git a/src/Cosmos.Sql/EquatableArray.fs b/src/Cosmos.Sql/EquatableArray.fs new file mode 100644 index 00000000..eb17366e --- /dev/null +++ b/src/Cosmos.Sql/EquatableArray.fs @@ -0,0 +1,199 @@ +namespace FSharp.Azure.Cosmos.Sql + +open System +open System.Collections +open System.Collections.Generic +open System.Collections.Immutable +open System.Runtime.InteropServices + +/// +/// An immutable array with structural equality: two instances are equal when they hold equal elements in the same +/// order, and the hash code combines the hash codes of the elements. +/// +/// The own equality of , its +/// +/// that calls, compares the reference of the array +/// it wraps. Only comparers that go through , such as the equality +/// the F# compiler generates for records and unions, compare its elements; +/// , a +/// of arrays or a C# caller compares references, and a default +/// array never equals an empty one. The nodes of the syntax tree hold their child sequences in this wrapper instead +/// (the pattern Roslyn incremental generators use), so their equality is structural under every comparer, for +/// round-trip tests, cache keys and the idempotence of passes. +/// +/// +/// The default value holds no array; it behaves as an empty array and is equal to one. +/// +/// +/// The elements, in order. +[] +type EquatableArray<'T> (array : ImmutableArray<'T>) = + + /// + /// The elements; never a default , so it can be + /// enumerated and indexed without a check. + /// + member _.Items = if array.IsDefault then ImmutableArray<'T>.Empty else array + + /// The number of elements. + member _.Length = if array.IsDefault then 0 else array.Length + + /// Whether the array has no elements. + member this.IsEmpty = this.Length = 0 + + /// + /// The element at . + /// + /// The zero-based position of the element. + member this.Item + with get (index : int) = this.Items[index] + + /// + /// Returns an allocation-free enumerator over the elements, which F# loops use. + /// + member this.GetEnumerator () = this.Items.GetEnumerator () + + /// + /// Whether holds equal elements in the same order; elements are compared with + /// . + /// + /// The array to compare with. + member this.Equals (other : EquatableArray<'T>) = + let left = this.Items + let right = other.Items + + if left.Length <> right.Length then + false + else + let comparer = EqualityComparer<'T>.Default + let mutable equal = true + let mutable index = 0 + + while equal && index < left.Length do + equal <- comparer.Equals (left[index], right[index]) + index <- index + 1 + + equal + + /// + override this.Equals (other : objnull) = + match other with + | :? EquatableArray<'T> as other -> this.Equals other + | _ -> false + + /// Combines the hash codes of the elements, in order, so equal arrays have equal hash codes. + override this.GetHashCode () = + let mutable hash = HashCode () + + for item in this.Items do + hash.Add item + + hash.ToHashCode () + + /// Shows the elements in square brackets, separated by semicolons, the way F# shows an array. + override this.ToString () = + let items = this.Items |> Seq.map (fun item -> $"%A{item}") + $"""[| {String.Join ("; ", items)} |]""" + + /// + /// Wraps without copying it, so that an + /// can be given where an + /// is expected. + /// + /// The elements. + static member op_Implicit (items : ImmutableArray<'T>) : EquatableArray<'T> = EquatableArray items + + /// + /// The elements of as an , + /// without copying them and never as a default array, so that an + /// can be given where an + /// is expected. + /// + /// The array. + static member op_Implicit (array : EquatableArray<'T>) : ImmutableArray<'T> = array.Items + + interface IEquatable> with + /// + member this.Equals other = this.Equals other + + interface IReadOnlyList<'T> with + /// + member this.Count = this.Length + + /// + member this.Item + with get index = this.Items[index] + + /// + member this.GetEnumerator () : IEnumerator<'T> = (this.Items :> IEnumerable<'T>).GetEnumerator() + + /// + member this.GetEnumerator () : IEnumerator = (this.Items :> IEnumerable).GetEnumerator() + +/// +/// Functions that create and transform values. +/// +[] +[] +module EquatableArray = + + /// The array without elements. + [] + let empty<'T> : EquatableArray<'T> = EquatableArray ImmutableArray<'T>.Empty + + /// + /// Wraps without copying it. + /// + /// The elements. + let ofImmutableArray (items : ImmutableArray<'T>) = EquatableArray items + + /// + /// Copies into a new array. + /// + /// The elements. + let ofArray (items : 'T array) = EquatableArray (ImmutableArray.Create<'T> items) + + /// + /// Wraps without copying it, as + /// does. + /// + /// The caller gives the array away: the result is immutable only as long as nothing writes to + /// any more. A later write changes the elements, the equality and the hash code of the + /// result, also where it already is a key of a dictionary or a member of a set. + /// + /// + /// The elements, in an array that is never written to again. + let unsafeOfArray (items : 'T array) = EquatableArray (ImmutableCollectionsMarshal.AsImmutableArray<'T> items) + + /// + /// Copies into a new array. + /// + /// The elements. + let ofSeq (items : 'T seq) = EquatableArray (ImmutableArray.CreateRange<'T> items) + + /// + /// An array that holds only. + /// + /// The element. + let singleton (item : 'T) = EquatableArray (ImmutableArray.Create<'T> item) + + /// + /// The elements of as an , + /// without copying them. + /// + /// The array. + let toImmutableArray (array : EquatableArray<'T>) = array.Items + + /// + /// Applies to every element of , in order. + /// + /// The function to apply. + /// The array. + let map (mapping : 'T -> 'U) (array : EquatableArray<'T>) = + let items = array.Items + let builder = ImmutableArray.CreateBuilder<'U> items.Length + + for item in items do + builder.Add (mapping item) + + EquatableArray (builder.MoveToImmutable ()) diff --git a/src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj b/src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj new file mode 100644 index 00000000..70e6c38b --- /dev/null +++ b/src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj @@ -0,0 +1,30 @@ + + + + net10.0 + $(AssemblyBaseName).Sql + true + true + + false + + + + true + true + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Cosmos.Sql/ImmutableArrayPatterns.fs b/src/Cosmos.Sql/ImmutableArrayPatterns.fs new file mode 100644 index 00000000..629bed70 --- /dev/null +++ b/src/Cosmos.Sql/ImmutableArrayPatterns.fs @@ -0,0 +1,74 @@ +/// +/// Struct partial active patterns that match an by its +/// length, so code over the children of a syntax node reads like a list pattern without converting to an F# list. +/// +/// Each pattern returns a value option made a struct by on its +/// return value, or a , and its payload is a struct tuple, so a match allocates nothing. +/// A multi-case active pattern over the lengths would return a heap +/// on every match, which is why every length is its own partial +/// pattern and lengths a pattern does not cover fall through to the next case. +/// +/// +/// +/// +/// match arguments with +/// | Arr0 -> "no argument" +/// | Arr1 single -> "one argument" +/// | Arr2 (first, second) -> "two arguments" +/// | Arr3 (first, second, third) -> "three arguments" +/// | ArrN 4 -> "four arguments" +/// | _ -> "more arguments" +/// +/// +[] +module FSharp.Azure.Cosmos.Sql.ImmutableArrayPatterns + +open System.Collections.Immutable + +/// +/// Matches an array without elements. A default (uninitialized) array counts as empty. +/// +/// The array to match. +let (|Arr0|_|) (items : ImmutableArray<'T>) = items.IsDefaultOrEmpty + +/// +/// Matches an array of exactly one element and returns that element. +/// +/// The array to match. +[] +let (|Arr1|_|) (items : ImmutableArray<'T>) = + if not items.IsDefault && items.Length = 1 then + ValueSome items[0] + else + ValueNone + +/// +/// Matches an array of exactly two elements and returns them as a struct tuple, which the pattern binds with or +/// without the keyword. +/// +/// The array to match. +[] +let (|Arr2|_|) (items : ImmutableArray<'T>) = + if not items.IsDefault && items.Length = 2 then + ValueSome (struct (items[0], items[1])) + else + ValueNone + +/// +/// Matches an array of exactly three elements and returns them as a struct tuple. +/// +/// The array to match. +[] +let (|Arr3|_|) (items : ImmutableArray<'T>) = + if not items.IsDefault && items.Length = 3 then + ValueSome (struct (items[0], items[1], items[2])) + else + ValueNone + +/// +/// Matches an array of exactly elements, for the lengths that have no pattern of their own; +/// index the array to read its elements. +/// +/// The number of elements to match, written as the argument of the pattern. +/// The array to match. +let (|ArrN|_|) (length : int) (items : ImmutableArray<'T>) = (if items.IsDefault then 0 else items.Length) = length diff --git a/tests/Cosmos.Sql.Tests/EquatableArrayModuleTests.fs b/tests/Cosmos.Sql.Tests/EquatableArrayModuleTests.fs new file mode 100644 index 00000000..a77daa40 --- /dev/null +++ b/tests/Cosmos.Sql.Tests/EquatableArrayModuleTests.fs @@ -0,0 +1,57 @@ +namespace FSharp.Azure.Cosmos.Sql.Tests + +open System.Collections.Immutable +open System.Runtime.InteropServices +open Microsoft.VisualStudio.TestTools.UnitTesting + +open FSharp.Azure.Cosmos.Sql + +/// +/// The functions of the module , which create and +/// transform values. +/// +[] +type EquatableArrayModuleTests () = + + [] + member _.``ofArray and ofSeq copy the elements in order`` () = + let source = [| 1; 2; 3 |] + let fromArray = EquatableArray.ofArray source + let fromSeq = EquatableArray.ofSeq (Seq.ofArray source) + + source[0] <- 9 + + CollectionAssert.AreEqual ([| 1; 2; 3 |], Seq.toArray fromArray, "ofArray should copy the array it is given.") + CollectionAssert.AreEqual ([| 1; 2; 3 |], Seq.toArray fromSeq, "ofSeq should copy the sequence it is given.") + + [] + member _.``unsafeOfArray wraps the array it is given without copying it`` () = + let source = [| 1; 2; 3 |] + let array = EquatableArray.unsafeOfArray source + + Assert.AreSame( + source, + ImmutableCollectionsMarshal.AsArray array.Items, + "unsafeOfArray should hold the very array it was given." + ) + + Assert.AreEqual (EquatableArray.unsafeOfArray [| 1; 2; 3 |], array, "The wrapped array should hold the elements.") + + [] + member _.``Map, singleton, empty and the conversion back keep the elements`` () = + let doubled = + EquatableArray.unsafeOfArray [| 1; 2; 3 |] + |> EquatableArray.map (fun item -> item * 2) + + Assert.AreEqual (EquatableArray.unsafeOfArray [| 2; 4; 6 |], doubled, "Map should apply the function in order.") + Assert.AreEqual (EquatableArray.unsafeOfArray [| 7 |], EquatableArray.singleton 7, "Singleton should hold one element.") + Assert.IsTrue (EquatableArray.empty.IsEmpty, "The empty array should have no elements.") + + let immutable = ImmutableArray.Create (4, 5) + + let unwrapped = + EquatableArray.ofImmutableArray immutable + |> EquatableArray.toImmutableArray + + // ImmutableArray equality compares the reference of the underlying array, so this asserts that nothing was copied + Assert.AreEqual>(immutable, unwrapped, "Wrapping and unwrapping should not copy the array.") diff --git a/tests/Cosmos.Sql.Tests/EquatableArrayTests.fs b/tests/Cosmos.Sql.Tests/EquatableArrayTests.fs new file mode 100644 index 00000000..6428055b --- /dev/null +++ b/tests/Cosmos.Sql.Tests/EquatableArrayTests.fs @@ -0,0 +1,161 @@ +namespace FSharp.Azure.Cosmos.Sql.Tests + +open System.Collections.Generic +open System.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +open FSharp.Azure.Cosmos.Sql + +/// An element that writes its number into the list it is given whenever it is asked for its hash code. +[] +type private HashProbe (number : int, asked : List) = + + /// Records the request and gives the number as the hash code. + override _.GetHashCode () = + asked.Add number + number + + /// Equal to itself only: the test needs no equal probes. + override this.Equals (other : objnull) = obj.ReferenceEquals (this, other) + +/// +/// The struct : its equality and hash code, its default +/// value, its views and its implicit conversions. +/// +[] +type EquatableArrayTests () = + + [] + member _.``Arrays with equal elements in the same order are equal and hash alike`` () = + let first = EquatableArray.unsafeOfArray [| 1; 2; 3 |] + let second = EquatableArray.unsafeOfArray [| 1; 2; 3 |] + + Assert.IsTrue (first.Equals second, "Arrays built separately from equal elements should be equal.") + Assert.AreEqual (first, second, "Object equality should agree with the typed equality.") + Assert.AreEqual (first.GetHashCode (), second.GetHashCode (), "Equal arrays should have equal hash codes.") + + [] + member _.``Arrays that differ in order, length or an element are not equal`` () = + let array = EquatableArray.unsafeOfArray [| 1; 2; 3 |] + + Assert.AreNotEqual (EquatableArray.unsafeOfArray [| 3; 2; 1 |], array, "The order of the elements should matter.") + Assert.AreNotEqual (EquatableArray.unsafeOfArray [| 1; 2 |], array, "The length should matter.") + Assert.AreNotEqual (EquatableArray.unsafeOfArray [| 1; 2; 4 |], array, "Every element should matter.") + Assert.IsFalse (array.Equals (box [| 1; 2; 3 |]), "An array of another type should not be equal.") + + [] + member _.``The hash code takes in every element once, first to last`` () = + let asked = List() + // Twenty elements, because the structural hash code of an ImmutableArray takes in only the last eight + let array = EquatableArray.unsafeOfArray (Array.init 20 (fun number -> HashProbe (number, asked))) + + array.GetHashCode () |> ignore + + CollectionAssert.AreEqual ( + Array.init 20 id, + asked.ToArray (), + "Every element should be asked for its hash code once, in the order of the array." + ) + + [] + member _.``The default value behaves as and equals an empty array`` () = + let defaultArray = Unchecked.defaultof> + + Assert.IsEmpty (defaultArray, "The default value should have no elements.") + Assert.IsTrue (defaultArray.IsEmpty, "The default value should be empty.") + Assert.IsTrue (defaultArray.Items.IsEmpty, "The default value should expose an empty, not a default, array.") + Assert.AreEqual (EquatableArray.empty, defaultArray, "The default value should equal the empty array.") + Assert.AreEqual ( + EquatableArray.empty.GetHashCode(), + defaultArray.GetHashCode (), + "The default value should hash like the empty array." + ) + + [] + member _.``Elements are compared with their own equality`` () = + let first = EquatableArray.unsafeOfArray [| "a"; "b" |] + let second = EquatableArray.unsafeOfArray [| "a"; System.String ([| 'b' |]) |] + + Assert.AreEqual (first, second, "Equal strings in different instances should make the arrays equal.") + + [] + member _.``Equality holds under every comparer, unlike the own equality of ImmutableArray`` () = + let first = ImmutableArray.Create (1, 2) + let second = ImmutableArray.Create (1, 2) + + // The reason the syntax tree does not hold ImmutableArray: its own equality compares array references + Assert.IsFalse ( + EqualityComparer>.Default.Equals(first, second), + "ImmutableArray's default equality should compare references." + ) + + Assert.IsFalse ( + (struct (1, first)).Equals(struct (1, second)), + "A struct tuple of ImmutableArrays should compare references." + ) + + Assert.IsTrue ( + EqualityComparer>.Default.Equals(EquatableArray first, EquatableArray second), + "EquatableArray's default equality should compare elements." + ) + + Assert.IsTrue ( + (struct (1, EquatableArray first)).Equals(struct (1, EquatableArray second)), + "A struct tuple of EquatableArrays should compare elements." + ) + + let set = HashSet>() + set.Add (EquatableArray first) |> ignore + + // Assert.Contains asks the set itself, so the lookup goes through the hash code and the equality of the array + Assert.Contains (EquatableArray second, set, "A hash set with the default comparer should find an equal array.") + + [] + member _.``The read-only list view and the enumerator yield the elements in order`` () = + let array = EquatableArray.unsafeOfArray [| 10; 20; 30 |] + let list = array :> IReadOnlyList + + Assert.HasCount (3, list, "The list view should report the length.") + Assert.AreEqual (20, list[1], "The list view should index the elements.") + CollectionAssert.AreEqual ([| 10; 20; 30 |], Seq.toArray list, "The list view should enumerate in order.") + + let mutable sum = 0 + + for item in array do + sum <- sum + item + + Assert.AreEqual (60, sum, "A for loop over the array should visit every element.") + + // F# applies an implicit conversion without warning FS3391 at method arguments only, so the conversions are + // exercised through methods that take the other type + static member private AsEquatable (array : EquatableArray) = array + static member private AsImmutable (items : ImmutableArray) = items + + [] + member _.``An ImmutableArray is accepted where an EquatableArray is expected, and the other way round`` () = + let immutable = ImmutableArray.Create (4, 5) + let equatable = EquatableArrayTests.AsEquatable immutable + + Assert.AreEqual (EquatableArray.unsafeOfArray [| 4; 5 |], equatable, "The conversion should keep the elements.") + + // ImmutableArray equality compares the reference of the underlying array, so these assert that nothing was copied + Assert.AreEqual>(immutable, equatable.Items, "The conversion should wrap, not copy, the array.") + + Assert.AreEqual>( + immutable, + EquatableArrayTests.AsImmutable equatable, + "The conversion back should hand out the wrapped array." + ) + + [] + member _.``The implicit conversions turn a default array into an empty one`` () = + let fromDefault = EquatableArrayTests.AsEquatable Unchecked.defaultof> + let toImmutable = EquatableArrayTests.AsImmutable Unchecked.defaultof> + + Assert.AreEqual (EquatableArray.empty, fromDefault, "A default ImmutableArray should convert to the empty array.") + Assert.IsFalse (toImmutable.IsDefault, "The default value should not convert to a default ImmutableArray.") + Assert.IsEmpty (toImmutable, "The default value should convert to an empty ImmutableArray.") + + [] + member _.``ToString shows the elements like an F# array`` () = + Assert.AreEqual ("[| 1; 2 |]", (EquatableArray.unsafeOfArray [| 1; 2 |]).ToString(), "ToString should list the elements.") diff --git a/tests/Cosmos.Sql.Tests/FSharp.Azure.Cosmos.Sql.Tests.fsproj b/tests/Cosmos.Sql.Tests/FSharp.Azure.Cosmos.Sql.Tests.fsproj new file mode 100644 index 00000000..e91fa921 --- /dev/null +++ b/tests/Cosmos.Sql.Tests/FSharp.Azure.Cosmos.Sql.Tests.fsproj @@ -0,0 +1,36 @@ + + + + + net10.0 + $(AssemblyBaseName).Sql.Tests + + Exe + true + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + diff --git a/tests/Cosmos.Sql.Tests/ImmutableArrayPatternsTests.fs b/tests/Cosmos.Sql.Tests/ImmutableArrayPatternsTests.fs new file mode 100644 index 00000000..e15455db --- /dev/null +++ b/tests/Cosmos.Sql.Tests/ImmutableArrayPatternsTests.fs @@ -0,0 +1,38 @@ +namespace FSharp.Azure.Cosmos.Sql.Tests + +open System.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +open FSharp.Azure.Cosmos.Sql + +/// +/// The active patterns that match an by its length. +/// +[] +type ImmutableArrayPatternsTests () = + + [] + member _.``The length patterns match exactly their length`` () = + let describe (items : ImmutableArray) = + match items with + | Arr0 -> "none" + | Arr1 single -> $"one %d{single}" + | Arr2 (first, second) -> $"two %d{first} %d{second}" + | Arr3 (first, second, third) -> $"three %d{first} %d{second} %d{third}" + | ArrN 4 -> "four" + | _ -> "more" + + Assert.AreEqual ("none", describe ImmutableArray.Empty, "Arr0 should match an empty array.") + Assert.AreEqual ("none", describe Unchecked.defaultof>, "Arr0 should match a default array.") + Assert.AreEqual ("one 1", describe (ImmutableArray.Create 1), "Arr1 should match one element.") + Assert.AreEqual ("two 1 2", describe (ImmutableArray.Create (1, 2)), "Arr2 should match two elements.") + Assert.AreEqual ("three 1 2 3", describe (ImmutableArray.Create (1, 2, 3)), "Arr3 should match three elements.") + Assert.AreEqual ("four", describe (ImmutableArray.Create (1, 2, 3, 4)), "ArrN 4 should match four elements.") + Assert.AreEqual ("more", describe (ImmutableArray.Create (1, 2, 3, 4, 5)), "Longer arrays should fall through.") + + [] + member _.``Arr2 binds a struct tuple pattern too`` () = + match ImmutableArray.Create ("a", "b") with + | Arr2 (struct (first, second)) -> + Assert.AreEqual ("ab", first + second, "The struct tuple pattern should bind both elements.") + | _ -> Assert.Fail "Arr2 should match an array of two elements." diff --git a/tests/Cosmos.Sql.Tests/TestCategories.fs b/tests/Cosmos.Sql.Tests/TestCategories.fs new file mode 100644 index 00000000..ecb87209 --- /dev/null +++ b/tests/Cosmos.Sql.Tests/TestCategories.fs @@ -0,0 +1,18 @@ +// The categories of the FSharp.Azure.Cosmos.Sql tests. Every test here is a fast, emulator-free unit test, so every +// category also carries "Unit", which --filter TestCategory=Unit selects. The attributes are sorted by name. +namespace FSharp.Azure.Cosmos.Sql.Tests + +open System.Collections.Generic +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// Categorizes a unit test of the collection types that the syntax tree is built from: +/// , +/// and +/// . +/// +type CollectionsUnitTestCategoryAttribute () = + inherit TestCategoryBaseAttribute () + + /// + override _.TestCategories = [| "Unit"; "Collections" |] :> IList diff --git a/tests/Cosmos.Sql.Tests/testconfig.json b/tests/Cosmos.Sql.Tests/testconfig.json new file mode 100644 index 00000000..b43d5de5 --- /dev/null +++ b/tests/Cosmos.Sql.Tests/testconfig.json @@ -0,0 +1,9 @@ +{ + "mstest": { + "parallelism": { + "enabled": true, + "scope": "method", + "workers": 0 + } + } +} \ No newline at end of file