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