Skip to content

Commit 60ad1e1

Browse files
xperiandriclaude
andcommitted
fixup! test: shared contracts of the set modules, run for HashSet and ISet
One data-row test for each group of functions that take a sequence, with a row for every input that they reject: `null`, a default flat list given as a sequence, and a default flat list of strings given as a sequence of objects (a default flat list of hash sets given as a sequence of interface sets for `ISet`), which `checkNotNullOrDefaultSeq` recognizes through reflection. - `HashSetTests`: the functions of the module and the member `ofSeq` of the struct, 15 rows. - `ISetTests`: the functions of the interface module, 12 rows. - `HashSetComputationExpressionTests`: `yield!` for a sequence, an array and a hash set, and `for`, 8 rows. Each row asserts the exception, its type and the name of the parameter through `FlatListAssert.RejectsNull`, and the rows reuse `FlatListOracles.defaultStringsAsObjects` of the foundation. Three tests check that a flat list that is not default passes as a sequence of objects and of interface sets. Reverting each of the 13 fixes once failed a test, and so did switching off the covariant case of `checkNotNullOrDefaultSeq` in `Helper.fs`, which was restored. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 8edc13f commit 60ad1e1

3 files changed

Lines changed: 264 additions & 0 deletions

File tree

‎tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetComputationExpressionTests.fs‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,65 @@ type HashSetComputationExpressionTests () =
1212
/// The elements of a hash set, sorted in the order of F# generic comparison.
1313
static let sorted (set : HashSet<'T>) = HashSet.toArray set |> Array.sort
1414

15+
/// <summary>
16+
/// The expressions that reject their input, by name, with the parameter that names the input and the expression.
17+
/// </summary>
18+
/// <remarks>
19+
/// A default flat list converts to a sequence without a check and fails when the sequence is enumerated, and a null
20+
/// input reaches <c>UnionWith</c>, which names its own parameter (ADR 0001, D6). Each expression rejects its input
21+
/// when the buffer is created, not when the expression runs.
22+
/// </remarks>
23+
static let rejections : Map<string, struct (string * (unit -> unit))> =
24+
let nullElements = Unchecked.defaultof<seq<int>>
25+
let defaultElements = Unchecked.defaultof<FlatList<int>> :> seq<int>
26+
27+
// The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see
28+
let widenedElements = FlatListOracles.defaultStringsAsObjects
29+
30+
Map.ofArray [|
31+
"yield! (null sequence)", struct ("items", (fun () -> hashSet { yield! nullElements } |> ignore))
32+
"yield! (default flat list)", struct ("items", (fun () -> hashSet { yield! defaultElements } |> ignore))
33+
"yield! (default flat list of strings as objects)",
34+
struct ("items", (fun () -> hashSet { yield! widenedElements } |> ignore))
35+
"yield! (null array)", struct ("items", (fun () -> hashSet { yield! Unchecked.defaultof<int array> } |> ignore))
36+
"yield! (null hash set)",
37+
struct ("items",
38+
(fun () ->
39+
hashSet { yield! Unchecked.defaultof<HashSet<int>> }
40+
|> ignore
41+
))
42+
"for (null sequence)",
43+
struct ("source",
44+
(fun () ->
45+
hashSet {
46+
for item in nullElements do
47+
yield item
48+
}
49+
|> ignore
50+
))
51+
"for (default flat list)",
52+
struct ("source",
53+
(fun () ->
54+
hashSet {
55+
for item in defaultElements do
56+
yield item
57+
}
58+
|> ignore
59+
))
60+
"for (default flat list of strings as objects)",
61+
struct ("source",
62+
(fun () ->
63+
hashSet {
64+
for item in widenedElements do
65+
yield item
66+
}
67+
|> ignore
68+
))
69+
|]
70+
71+
/// The names of the expressions that reject their input, as rows.
72+
static member Rejections = rejections.Keys
73+
1574
[<TestMethod>]
1675
member _.``an expression without elements is empty`` () =
1776
let set : HashSet<int> = hashSet { () }
@@ -51,6 +110,28 @@ type HashSetComputationExpressionTests () =
51110

52111
CollectionAssert.AreEqual ([| 1..7 |], sorted set, "hashSet must add the elements of yield!")
53112

113+
[<TestMethod; DynamicData(nameof HashSetComputationExpressionTests.Rejections)>]
114+
member _.``yield! and for reject a null input and a default flat list given as a sequence`` (name : string) =
115+
let struct (paramName, actual) = rejections[name]
116+
FlatListAssert.RejectsNull ($"hashSet {{ %s{name} }}", paramName, actual)
117+
118+
[<TestMethod>]
119+
member _.``yield! and for take a flat list of strings that is not default as a sequence of objects`` () =
120+
let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |])
121+
122+
let set = hashSet {
123+
yield! widened
124+
125+
for item in widened do
126+
yield item
127+
}
128+
129+
CollectionAssert.AreEquivalent (
130+
[| ("a" :> obj); ("b" :> obj) |],
131+
HashSet.toArray set,
132+
"hashSet must take the elements of a flat list of strings as objects in yield! and for"
133+
)
134+
54135
[<TestMethod>]
55136
member _.``for adds the elements of the body for each item`` () =
56137
let set = hashSet {

‎tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTests.fs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,57 @@ type HashSetTests () =
187187
nameof HashSet.update, struct ("set", (fun () -> HashSet.update ignore nullSet |> ignore))
188188
|]
189189

190+
/// <summary>
191+
/// The functions that take a sequence, by name and by the input that they reject, with the parameter that names the
192+
/// input and the function on it.
193+
/// </summary>
194+
/// <remarks>
195+
/// A default flat list converts to a sequence without a check and fails when the sequence is enumerated, so the other
196+
/// inputs are valid and the function would enumerate the sequence (ADR 0001, D6).
197+
/// </remarks>
198+
static let sequenceRejections : Map<string, struct (string * (unit -> unit))> =
199+
let set = HashSet.ofArray Inputs.Several
200+
let ints = HashSet.forComparer (EqualityComparer<int>.Default :> IEqualityComparer<int>)
201+
let nullElements = Unchecked.defaultof<seq<int>>
202+
let defaultElements = Unchecked.defaultof<FlatList<int>> :> seq<int>
203+
let nullSets = Unchecked.defaultof<seq<HashSet<int>>>
204+
let defaultSets = Unchecked.defaultof<FlatList<HashSet<int>>> :> seq<HashSet<int>>
205+
206+
// The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see
207+
let widenedElements = FlatListOracles.defaultStringsAsObjects
208+
let objects = HashSet.ofArray [| ("a" :> obj) |]
209+
210+
Map.ofArray [|
211+
$"%s{nameof HashSet.addRange} (null elements)",
212+
struct ("elements", (fun () -> HashSet.addRange nullElements set |> ignore))
213+
$"%s{nameof HashSet.addRange} (default flat list)",
214+
struct ("elements", (fun () -> HashSet.addRange defaultElements set |> ignore))
215+
$"%s{nameof HashSet.addRange} (default flat list of strings as objects)",
216+
struct ("elements", (fun () -> HashSet.addRange widenedElements objects |> ignore))
217+
$"%s{nameof HashSet.intersectMany} (null sets)", struct ("sets", (fun () -> HashSet.intersectMany nullSets |> ignore))
218+
$"%s{nameof HashSet.intersectMany} (default flat list)",
219+
struct ("sets", (fun () -> HashSet.intersectMany defaultSets |> ignore))
220+
$"%s{nameof HashSet.ofSeq} (null elements)", struct ("elements", (fun () -> HashSet.ofSeq nullElements |> ignore))
221+
$"%s{nameof HashSet.ofSeq} (default flat list)",
222+
struct ("elements", (fun () -> HashSet.ofSeq defaultElements |> ignore))
223+
$"%s{nameof HashSet.ofSeq} (default flat list of strings as objects)",
224+
struct ("elements", (fun () -> HashSet.ofSeq widenedElements |> ignore))
225+
$"%s{nameof HashSet.removeRange} (null elements)",
226+
struct ("elements", (fun () -> HashSet.removeRange nullElements set |> ignore))
227+
$"%s{nameof HashSet.removeRange} (default flat list)",
228+
struct ("elements", (fun () -> HashSet.removeRange defaultElements set |> ignore))
229+
$"%s{nameof HashSet.removeRange} (default flat list of strings as objects)",
230+
struct ("elements", (fun () -> HashSet.removeRange widenedElements objects |> ignore))
231+
$"%s{nameof HashSet.unionMany} (null sets)", struct ("sets", (fun () -> HashSet.unionMany nullSets |> ignore))
232+
$"%s{nameof HashSet.unionMany} (default flat list)",
233+
struct ("sets", (fun () -> HashSet.unionMany defaultSets |> ignore))
234+
// The struct of forComparer
235+
$"forComparer %s{nameof ints.ofSeq} (null elements)",
236+
struct ("elements", (fun () -> ints.ofSeq nullElements |> ignore))
237+
$"forComparer %s{nameof ints.ofSeq} (default flat list)",
238+
struct ("elements", (fun () -> ints.ofSeq defaultElements |> ignore))
239+
|]
240+
190241
/// The names of the creation functions without a comparer, as rows.
191242
static member DefaultCreations = defaultCreations.Keys
192243

@@ -208,6 +259,9 @@ type HashSetTests () =
208259
/// The names of the functions that take a hash set, with the parameter where a function takes two, as rows.
209260
static member NullRejections = nullRejections.Keys
210261

262+
/// The names of the functions that take a sequence, with the input that they reject, as rows.
263+
static member SequenceRejections = sequenceRejections.Keys
264+
211265
// ---- Comparers ----
212266

213267
[<TestMethod; DynamicData(nameof HashSetTests.DefaultCreations)>]
@@ -512,6 +566,42 @@ type HashSetTests () =
512566

513567
Assert.AreEqual<string | null>(paramName, ex.ParamName, $"HashSet.%s{name} must name the parameter of the hash set")
514568

569+
[<TestMethod; DynamicData(nameof HashSetTests.SequenceRejections)>]
570+
member _.``functions that take a sequence reject a null sequence and a default flat list given as the sequence``
571+
(name : string)
572+
=
573+
let struct (paramName, actual) = sequenceRejections[name]
574+
FlatListAssert.RejectsNull ($"HashSet.%s{name}", paramName, actual)
575+
576+
[<TestMethod>]
577+
member _.``a flat list of strings that is not default passes as a sequence of objects`` () =
578+
let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |])
579+
let objects = HashSet.ofArray [| ("b" :> obj); ("c" :> obj) |]
580+
581+
CollectionAssert.AreEquivalent (
582+
[| ("a" :> obj); ("b" :> obj) |],
583+
HashSet.toArray (HashSet.ofSeq widened),
584+
"HashSet.ofSeq must take the elements of a flat list of strings as objects"
585+
)
586+
587+
CollectionAssert.AreEquivalent (
588+
[| ("a" :> obj); ("b" :> obj) |],
589+
HashSet.toArray ((HashSet.forComparer (EqualityComparer<obj>.Default :> IEqualityComparer<obj>)).ofSeq widened),
590+
"CustomComparerHashSet.ofSeq must take the elements of a flat list of strings as objects"
591+
)
592+
593+
CollectionAssert.AreEquivalent (
594+
[| ("a" :> obj); ("b" :> obj); ("c" :> obj) |],
595+
HashSet.toArray (HashSet.addRange widened objects),
596+
"HashSet.addRange must take the elements of a flat list of strings as objects"
597+
)
598+
599+
CollectionAssert.AreEquivalent (
600+
[| ("c" :> obj) |],
601+
HashSet.toArray (HashSet.removeRange widened objects),
602+
"HashSet.removeRange must take the elements of a flat list of strings as objects"
603+
)
604+
515605
[<TestMethod>]
516606
member _.``ofBuilder rejects a null builder`` () =
517607
let ex =

‎tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,57 @@ type ISetTests () =
157157
nameof ISet.unionMany, struct ("sets", (fun () -> ISet.unionMany [| nullSet; set |] |> ignore))
158158
|]
159159

160+
/// A flat list of hash sets as a sequence of interface sets. A flat list of hash sets is also a sequence of interface
161+
/// sets, by the covariance of seq: the element type of the sequence is not the one of the flat list, and a type test for
162+
/// the flat list of interface sets does not see it.
163+
static let asInterfaceSets (sets : FlatList<HashSet<int>>) : seq<IImmutableSet<int>> =
164+
match box sets with
165+
| :? seq<IImmutableSet<int>> as widened -> widened
166+
| _ -> invalidOp "A flat list of hash sets must be a sequence of interface sets"
167+
168+
/// <summary>
169+
/// The functions that take a sequence, by name and by the input that they reject, with the parameter that names the
170+
/// input and the function on it.
171+
/// </summary>
172+
/// <remarks>
173+
/// A default flat list converts to a sequence without a check and fails when the sequence is enumerated, so the other
174+
/// inputs are valid and the function would enumerate the sequence (ADR 0001, D6).
175+
/// </remarks>
176+
static let sequenceRejections : Map<string, struct (string * (unit -> unit))> =
177+
let set = HashSet.ofArray Inputs.Several :> IImmutableSet<int>
178+
let nullElements = Unchecked.defaultof<seq<int>>
179+
let defaultElements = Unchecked.defaultof<FlatList<int>> :> seq<int>
180+
let nullSets = Unchecked.defaultof<seq<IImmutableSet<int>>>
181+
let defaultSets = Unchecked.defaultof<FlatList<IImmutableSet<int>>> :> seq<IImmutableSet<int>>
182+
183+
// The flat lists of strings and of hash sets are sequences of objects and of interface sets
184+
let widenedElements = FlatListOracles.defaultStringsAsObjects
185+
let objects = HashSet.ofArray [| ("a" :> obj) |] :> IImmutableSet<obj>
186+
let widenedSets = asInterfaceSets Unchecked.defaultof<FlatList<HashSet<int>>>
187+
188+
Map.ofArray [|
189+
$"%s{nameof ISet.addRange} (null elements)", struct ("elements", (fun () -> ISet.addRange nullElements set |> ignore))
190+
$"%s{nameof ISet.addRange} (default flat list)",
191+
struct ("elements", (fun () -> ISet.addRange defaultElements set |> ignore))
192+
$"%s{nameof ISet.addRange} (default flat list of strings as objects)",
193+
struct ("elements", (fun () -> ISet.addRange widenedElements objects |> ignore))
194+
$"%s{nameof ISet.intersectMany} (null sets)", struct ("sets", (fun () -> ISet.intersectMany nullSets |> ignore))
195+
$"%s{nameof ISet.intersectMany} (default flat list)",
196+
struct ("sets", (fun () -> ISet.intersectMany defaultSets |> ignore))
197+
$"%s{nameof ISet.intersectMany} (default flat list of hash sets as interface sets)",
198+
struct ("sets", (fun () -> ISet.intersectMany widenedSets |> ignore))
199+
$"%s{nameof ISet.removeRange} (null elements)",
200+
struct ("elements", (fun () -> ISet.removeRange nullElements set |> ignore))
201+
$"%s{nameof ISet.removeRange} (default flat list)",
202+
struct ("elements", (fun () -> ISet.removeRange defaultElements set |> ignore))
203+
$"%s{nameof ISet.removeRange} (default flat list of strings as objects)",
204+
struct ("elements", (fun () -> ISet.removeRange widenedElements objects |> ignore))
205+
$"%s{nameof ISet.unionMany} (null sets)", struct ("sets", (fun () -> ISet.unionMany nullSets |> ignore))
206+
$"%s{nameof ISet.unionMany} (default flat list)", struct ("sets", (fun () -> ISet.unionMany defaultSets |> ignore))
207+
$"%s{nameof ISet.unionMany} (default flat list of hash sets as interface sets)",
208+
struct ("sets", (fun () -> ISet.unionMany widenedSets |> ignore))
209+
|]
210+
160211
/// The names of the transformations that keep the element type, as rows.
161212
static member SameTypeTransformations = sameTypeTransformations.Keys
162213

@@ -169,6 +220,9 @@ type ISetTests () =
169220
/// The names of the functions that take a set, with the parameter where a function takes two, as rows.
170221
static member NullRejections = nullRejections.Keys
171222

223+
/// The names of the functions that take a sequence, with the input that they reject, as rows.
224+
static member SequenceRejections = sequenceRejections.Keys
225+
172226
[<TestMethod; DynamicData(nameof ISetTests.SameTypeTransformations)>]
173227
member _.``the transformations that keep the element type keep the implementation`` (name : string) =
174228
let result = sameTypeTransformations[name] (sortedSet Inputs.Several) (sortedSet Inputs.Other)
@@ -286,3 +340,42 @@ type ISetTests () =
286340
Assert.ThrowsExactly<ArgumentNullException>(Action actual, $"ISet.%s{name} must reject a null set")
287341

288342
Assert.AreEqual<string | null>(paramName, ex.ParamName, $"ISet.%s{name} must name the parameter of the set")
343+
344+
[<TestMethod; DynamicData(nameof ISetTests.SequenceRejections)>]
345+
member _.``functions that take a sequence reject a null sequence and a default flat list given as the sequence``
346+
(name : string)
347+
=
348+
let struct (paramName, actual) = sequenceRejections[name]
349+
FlatListAssert.RejectsNull ($"ISet.%s{name}", paramName, actual)
350+
351+
[<TestMethod>]
352+
member _.``a flat list that is not default passes as a sequence of objects and as a sequence of interface sets`` () =
353+
let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |])
354+
let objects = HashSet.ofArray [| ("b" :> obj); ("c" :> obj) |] :> IImmutableSet<obj>
355+
356+
let sets =
357+
asInterfaceSets (FlatList.ofArray [| HashSet.ofArray [| 1; 2 |]; HashSet.ofArray [| 2; 3 |] |])
358+
359+
CollectionAssert.AreEquivalent (
360+
[| ("a" :> obj); ("b" :> obj); ("c" :> obj) |],
361+
ISet.toArray (ISet.addRange widened objects),
362+
"ISet.addRange must take the elements of a flat list of strings as objects"
363+
)
364+
365+
CollectionAssert.AreEquivalent (
366+
[| ("c" :> obj) |],
367+
ISet.toArray (ISet.removeRange widened objects),
368+
"ISet.removeRange must take the elements of a flat list of strings as objects"
369+
)
370+
371+
CollectionAssert.AreEquivalent (
372+
[| 1; 2; 3 |],
373+
ISet.toArray (ISet.unionMany sets),
374+
"ISet.unionMany must take the sets of a flat list of hash sets as interface sets"
375+
)
376+
377+
CollectionAssert.AreEquivalent (
378+
[| 2 |],
379+
ISet.toArray (ISet.intersectMany sets),
380+
"ISet.intersectMany must take the sets of a flat list of hash sets as interface sets"
381+
)

0 commit comments

Comments
 (0)