Skip to content

Commit 44b1f73

Browse files
xperiandriclaude
andcommitted
test: contracts of IndexedList and IIndexedList, and the stable sorting contract
`StableSortContract` checks that `sort`, `sortBy`, `sortByDescending`, `sortDescending`, `sortWith` and `sortWithComparer` keep the order of equal elements, as the `List` functions do. The pairs of `Inputs.pairs` show it for the sorts by key; `sort` and `sortDescending` run on `KeyedItem`, whose comparison looks at a key only. The sequence-like, creation, index-based and stable sorting contracts run for `IndexedList`, and all of them but the creation contract for `IIndexedList`. Further tests cover null inputs, shared trees, `cons`, `uncons` and the patterns, `binarySearch`, `blit`, the order in which the functions of two lists call their callbacks before they throw, the complete messages of the `List` exceptions, the implementation that `IIndexedList` keeps, the builder functions, `indexedList { }` and the `Option` and `Tuple` namespaces. Two shared contracts now serve the modules that mirror `List` as well. `ofArray` of a null array is compared with `Array.toList`, because `List.ofArray` reads a null array without a check. `removeManyAt` of a count that is not positive is compared with what the mirrored function returns or throws, because `List.removeManyAt` removes nothing where `Array.removeManyAt` copies by the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b38b9eb commit 44b1f73

14 files changed

Lines changed: 3073 additions & 2 deletions

‎tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,48 @@ type IndexedContract<'C> (ops : IndexedOps<'C>) =
206206
member this.``removeManyAt throws for a range out of range as FSharp.Core does`` () =
207207
let struct (collection, elements) = several ()
208208

209-
for struct (index, count) in [ struct (-1, 1); struct (elements.Length - 1, 2); struct (0, -1) ] do
209+
for struct (index, count) in [ struct (-1, 1); struct (elements.Length - 1, 2) ] do
210210
this.ThrowsLikeFSharpCore (
211211
(fun () -> Array.removeManyAt index count (List.toArray elements)),
212212
(fun () -> List.removeManyAt index count elements),
213213
(fun () -> ops.RemoveManyAt index count collection),
214214
$"removeManyAt %d{index} %d{count} must throw"
215215
)
216216

217+
[<TestMethod; TestCategory(nameof FlatList.removeManyAt)>]
218+
member this.``removeManyAt of a count that is not positive does what FSharp.Core does`` () =
219+
let struct (collection, elements) = several ()
220+
221+
// List.removeManyAt removes nothing for such a count while index + count stays within the list, and throws
222+
// otherwise; Array.removeManyAt copies by the count and throws or returns what the copies give
223+
for struct (index, count) in [ struct (0, 0); struct (0, -1); struct (3, -2); struct (10, -5); struct (20, -5) ] do
224+
let arrayOracle () =
225+
Array.removeManyAt index count (List.toArray elements)
226+
|> List.ofArray
227+
228+
let listOracle () = List.removeManyAt index count elements
229+
230+
let outcome =
231+
try
232+
Ok (this.Mirrored (arrayOracle, listOracle))
233+
with ex ->
234+
Error ex
235+
236+
match outcome with
237+
| Ok expected ->
238+
Assert.AreEqual<int list>(
239+
expected,
240+
ops.ToList (ops.RemoveManyAt index count collection),
241+
$"%s{ops.Name}.removeManyAt %d{index} %d{count} must return what FSharp.Core returns"
242+
)
243+
| Error _ ->
244+
this.ThrowsLikeFSharpCore (
245+
arrayOracle,
246+
listOracle,
247+
(fun () -> ops.RemoveManyAt index count collection),
248+
$"removeManyAt %d{index} %d{count} must throw"
249+
)
250+
217251
[<TestMethod; TestCategory(nameof FlatList.updateAt)>]
218252
member _.``updateAt replaces the element at the index`` () =
219253
let struct (collection, elements) = several ()

‎tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ type SeqCreationContract<'C> (ops : SeqCreationOps<'C>) =
4848
member this.``ofArray throws for a null array as FSharp.Core does`` () =
4949
let source = Unchecked.defaultof<int array>
5050

51+
// List.ofArray reads a null array without a check and fails with NullReferenceException, while a null input throws
52+
// ArgumentNullException (ADR 0001, D6), as Array.toList does for the same conversion
5153
this.ThrowsLikeFSharpCore (
5254
(fun () -> Array.copy source),
53-
(fun () -> List.ofArray source),
55+
(fun () -> Array.toList source),
5456
(fun () -> ops.OfArray source),
5557
"ofArray of a null array must throw"
5658
)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
namespace FSharp.Collections.Immutable.Tests
2+
3+
open System
4+
open System.Collections.Generic
5+
open FSharp.Collections.Immutable
6+
open Microsoft.VisualStudio.TestTools.UnitTesting
7+
8+
/// <summary>An element whose equality and comparison look at its key only.</summary>
9+
/// <remarks>
10+
/// Elements with the same key and different tags compare equal, so a sort that compares whole elements, such as
11+
/// <c>sort</c>, shows on them whether it keeps the order of equal elements.
12+
/// </remarks>
13+
[<CustomEquality; CustomComparison>]
14+
type KeyedItem = {
15+
/// The key that the equality and the comparison look at.
16+
Key : int
17+
/// The tag that tells the elements with the same key apart.
18+
Tag : char
19+
} with
20+
21+
override this.Equals other =
22+
match other with
23+
| :? KeyedItem as other -> this.Key = other.Key
24+
| _ -> false
25+
26+
override this.GetHashCode () = this.Key.GetHashCode ()
27+
28+
// F# generic comparison calls the non-generic interface
29+
interface IComparable with
30+
member this.CompareTo other =
31+
match other with
32+
| :? KeyedItem as other -> compare this.Key other.Key
33+
| _ -> invalidArg (nameof other) "A keyed item compares with keyed items only"
34+
35+
/// <summary>The sorting functions of a module that sorts stably, for the contract tests.</summary>
36+
/// <remarks>
37+
/// The elements are the pairs of <see cref="P:FSharp.Collections.Immutable.Tests.Inputs.pairs"/>: the functions sort them
38+
/// by the key in the first component, and the tag in the second component shows the order of the elements with equal keys.
39+
/// <c>Sort</c> and <c>SortDescending</c> compare whole elements and pairs never compare equal, so they take
40+
/// <see cref="T:FSharp.Collections.Immutable.Tests.KeyedItem"/> elements instead, and the instance creates and reads the
41+
/// collection itself, as <c>SeqOps.Unzip</c> does for its pairs.
42+
/// </remarks>
43+
type StableSortOps<'C> = {
44+
/// The name of the module, for the messages of the assertions.
45+
Name : string
46+
/// Creates a collection that enumerates the given elements in their order.
47+
OfSeq : seq<struct (int * char)> -> 'C
48+
/// The elements in the order in which the collection enumerates them.
49+
ToList : 'C -> struct (int * char) list
50+
51+
/// Sorts a collection of the given keyed items.
52+
Sort : KeyedItem list -> KeyedItem list
53+
SortBy : (struct (int * char) -> int) -> 'C -> 'C
54+
SortByDescending : (struct (int * char) -> int) -> 'C -> 'C
55+
/// Sorts a collection of the given keyed items in descending order.
56+
SortDescending : KeyedItem list -> KeyedItem list
57+
SortWith : (struct (int * char) -> struct (int * char) -> int) -> 'C -> 'C
58+
SortWithComparer : IComparer<struct (int * char)> -> 'C -> 'C
59+
}
60+
61+
/// <summary>
62+
/// The contract of the modules that sort stably, as <see cref="T:Microsoft.FSharp.Collections.ListModule"/> does:
63+
/// <c>IndexedList</c>, <c>IIndexedList</c>, <c>Queue</c>, <c>IQueue</c>, <c>Stack</c> and <c>IStack</c> (ADR 0001, D5).
64+
/// </summary>
65+
/// <remarks>
66+
/// The expectations come from the stable sorts of <c>List</c> over the elements in the order in which the collection
67+
/// enumerates them. <c>FlatList</c> sorts unstably, as <c>Array</c> does, and has no such contract.
68+
/// </remarks>
69+
[<AbstractClass>]
70+
type StableSortContract<'C> (ops : StableSortOps<'C>) =
71+
72+
static let key (struct (key : int, _ : char)) = key
73+
74+
static let compareKeys (left : struct (int * char)) (right : struct (int * char)) = compare (key left) (key right)
75+
76+
/// The keyed items of the pairs of the input, in their order.
77+
static let keyedItems =
78+
Inputs.pairs
79+
|> List.map (fun struct (key, tag) -> { Key = key; Tag = tag })
80+
81+
/// The keys and the tags of keyed items, which tell apart the items that compare equal.
82+
static let keysAndTags (items : KeyedItem list) = items |> List.map (fun item -> struct (item.Key, item.Tag))
83+
84+
/// The collection of the input pairs and the elements that it enumerates.
85+
let pairs () =
86+
let collection = ops.OfSeq Inputs.pairs
87+
struct (collection, ops.ToList collection)
88+
89+
[<TestMethod; TestCategory(nameof IndexedList.sort)>]
90+
member _.``sort keeps the order of the elements that compare equal`` () =
91+
Assert.AreEqual<struct (int * char) list>(
92+
List.sort keyedItems |> keysAndTags,
93+
ops.Sort keyedItems |> keysAndTags,
94+
$"%s{ops.Name}.sort must keep the order of equal elements, as List.sort does"
95+
)
96+
97+
[<TestMethod; TestCategory(nameof IndexedList.sortBy)>]
98+
member _.``sortBy keeps the order of the elements with equal keys`` () =
99+
let struct (collection, elements) = pairs ()
100+
101+
Assert.AreEqual<struct (int * char) list>(
102+
List.sortBy key elements,
103+
ops.ToList (ops.SortBy key collection),
104+
$"%s{ops.Name}.sortBy must keep the order of equal keys, as List.sortBy does"
105+
)
106+
107+
[<TestMethod; TestCategory(nameof IndexedList.sortByDescending)>]
108+
member _.``sortByDescending keeps the order of the elements with equal keys`` () =
109+
let struct (collection, elements) = pairs ()
110+
111+
Assert.AreEqual<struct (int * char) list>(
112+
List.sortByDescending key elements,
113+
ops.ToList (ops.SortByDescending key collection),
114+
$"%s{ops.Name}.sortByDescending must keep the order of equal keys, as List.sortByDescending does"
115+
)
116+
117+
[<TestMethod; TestCategory(nameof IndexedList.sortDescending)>]
118+
member _.``sortDescending keeps the order of the elements that compare equal`` () =
119+
Assert.AreEqual<struct (int * char) list>(
120+
List.sortDescending keyedItems |> keysAndTags,
121+
ops.SortDescending keyedItems |> keysAndTags,
122+
$"%s{ops.Name}.sortDescending must keep the order of equal elements, as List.sortDescending does"
123+
)
124+
125+
[<TestMethod; TestCategory(nameof IndexedList.sortWith)>]
126+
member _.``sortWith keeps the order of the elements that compare equal`` () =
127+
let struct (collection, elements) = pairs ()
128+
129+
for struct (direction, comparer) in
130+
[
131+
struct ("ascending", compareKeys)
132+
struct ("descending", (fun left right -> compareKeys right left))
133+
] do
134+
Assert.AreEqual<struct (int * char) list>(
135+
List.sortWith comparer elements,
136+
ops.ToList (ops.SortWith comparer collection),
137+
$"%s{ops.Name}.sortWith %s{direction} must keep the order of equal elements, as List.sortWith does"
138+
)
139+
140+
[<TestMethod; TestCategory(nameof IndexedList.sortWithComparer)>]
141+
member _.``sortWithComparer keeps the order of the elements that compare equal`` () =
142+
let struct (collection, elements) = pairs ()
143+
144+
Assert.AreEqual<struct (int * char) list>(
145+
List.sortWith compareKeys elements,
146+
ops.ToList (
147+
ops.SortWithComparer (Comparer<struct (int * char)>.Create (fun left right -> compareKeys left right)) collection
148+
),
149+
$"%s{ops.Name}.sortWithComparer must keep the order of equal elements, as List.sortWith does"
150+
)

‎tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<Compile Include="Contracts\SeqLike\SeqLikeContract.fs" />
3434
<Compile Include="Contracts\SeqCreationContract.fs" />
3535
<Compile Include="Contracts\IndexedContract.fs" />
36+
<Compile Include="Contracts\StableSortContract.fs" />
3637
<Compile Include="TestCategoriesTests.fs" />
3738
<Compile Include="FlatList\FlatListOps.fs" />
3839
<Compile Include="FlatList\FlatListContractTests.fs" />
@@ -41,6 +42,16 @@
4142
<Compile Include="FlatList\FlatListComputationExpressionTests.fs" />
4243
<Compile Include="FlatList\FlatListOptionTests.fs" />
4344
<Compile Include="FlatList\FlatListTupleTests.fs" />
45+
<Compile Include="IndexedList\IndexedListOps.fs" />
46+
<Compile Include="IndexedList\IndexedListContractTests.fs" />
47+
<Compile Include="IndexedList\IndexedListTests.fs" />
48+
<Compile Include="IndexedList\IIndexedListTests.fs" />
49+
<Compile Include="IndexedList\IndexedListBuilderTests.fs" />
50+
<Compile Include="IndexedList\IndexedListComputationExpressionTests.fs" />
51+
<Compile Include="IndexedList\IndexedListOptionTests.fs" />
52+
<Compile Include="IndexedList\IndexedListTupleTests.fs" />
53+
<Compile Include="IndexedList\IIndexedListOptionTests.fs" />
54+
<Compile Include="IndexedList\IIndexedListTupleTests.fs" />
4455
</ItemGroup>
4556

4657
<ItemGroup>

0 commit comments

Comments
 (0)