Skip to content

Commit 41308a1

Browse files
xperiandriclaude
andcommitted
fixup! test: shared contracts of the set modules, run for HashSet and ISet
Tests of the gaps of the sets report (package A4), 2843 per target framework: - `SetMutationOps` gets `AddRange`, `RemoveRange` and `OfSeqByRemainder`, and `SetCreationOps` and `SetCustomComparerOps` get `OfReadOnlySpan` and `OfSpan`, one field for each function. `SortedSet` derives the tests of the mutation and creation contracts with these fields. - `SetMutationContract` runs `addRange` and `removeRange` with an array, a list and a lazy sequence over every pair of inputs, against `Set.union` and `Set.difference`, and with a comparer by remainder, where the elements of the set stay and the first of the other elements of each remainder is added (D14). The input itself must come back when the sequence adds nothing or removes nothing, under both comparers, and a null sequence throws `ArgumentNullException` with the name `elements`. - `SetCreationContract` runs `ofReadOnlySpan` and `ofSpan` of the module and of the struct on a copy of each input that the test changes after the call. `SetInputs` gets the row sources of the pairs that add or remove nothing, and `Remainders`, which `SetContract` uses too. - `HashSetTests` pins the first of the equal elements of a span through the table of the struct, `addRange` of an empty hash set (`Union` answers it with another instance), the comparer of an empty input that receives a hash set with another comparer, and `ofSeq` of the struct for an equal but distinct comparer instance. The tables of the comparer, the creation functions and the null checks list the new functions. `HashSetBuilderTests` covers `build`, the `build` of the struct and `update`, and `ISetTests` the implementation and the comparer kept by `addRange` and `removeRange`. Each new behavior was broken once in the code, 25 mutations, and a test failed each time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent f21fa68 commit 41308a1

10 files changed

Lines changed: 484 additions & 33 deletions

File tree

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,6 @@ type SetContract<'S> (ops : SetOps<'S>) =
6262
/// A folder from the end whose state records the order in which it saw the elements.
6363
static let recordBack (item : int) (visited : int array) = Array.append visited [| item |]
6464

65-
/// The remainders of the elements of an input in the division by 5, as a set that compares by remainders sees them.
66-
static let remainders (input : int array) = Set.ofArray (Array.map SetInputs.Remainder input)
67-
6865
/// <summary>
6966
/// The sets of a pair of inputs for the relations under the comparer of the first set.
7067
/// </summary>
@@ -263,7 +260,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
263260
let struct (set1, set2) = createByRemainder first second
264261

265262
Assert.AreEqual<bool>(
266-
Set.isProperSubset (remainders first) (remainders second),
263+
Set.isProperSubset (SetInputs.Remainders first) (SetInputs.Remainders second),
267264
ops.IsProperSubset set1 set2,
268265
$"%s{ops.Name}.isProperSubset must count the elements that the comparer of the first set finds equal once"
269266
)
@@ -273,7 +270,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
273270
let struct (set1, set2) = createByRemainder first second
274271

275272
Assert.AreEqual<bool>(
276-
Set.isProperSuperset (remainders first) (remainders second),
273+
Set.isProperSuperset (SetInputs.Remainders first) (SetInputs.Remainders second),
277274
ops.IsProperSuperset set1 set2,
278275
$"%s{ops.Name}.isProperSuperset must count the elements that the comparer of the first set finds equal once"
279276
)
@@ -283,7 +280,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
283280
let struct (set1, set2) = createByRemainder first second
284281

285282
Assert.AreEqual<bool>(
286-
Set.isSubset (remainders first) (remainders second),
283+
Set.isSubset (SetInputs.Remainders first) (SetInputs.Remainders second),
287284
ops.IsSubset set1 set2,
288285
$"%s{ops.Name}.isSubset must count the elements that the comparer of the first set finds equal once"
289286
)
@@ -293,7 +290,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
293290
let struct (set1, set2) = createByRemainder first second
294291

295292
Assert.AreEqual<bool>(
296-
Set.isSuperset (remainders first) (remainders second),
293+
Set.isSuperset (SetInputs.Remainders first) (SetInputs.Remainders second),
297294
ops.IsSuperset set1 set2,
298295
$"%s{ops.Name}.isSuperset must count the elements that the comparer of the first set finds equal once"
299296
)
@@ -303,7 +300,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
303300
let struct (set1, set2) = createByRemainder first second
304301

305302
Assert.AreEqual<bool>(
306-
not (Set.isEmpty (Set.intersect (remainders first) (remainders second))),
303+
not (Set.isEmpty (Set.intersect (SetInputs.Remainders first) (SetInputs.Remainders second))),
307304
ops.Overlaps set1 set2,
308305
$"%s{ops.Name}.overlaps must compare in the comparer of the first set"
309306
)
@@ -313,7 +310,7 @@ type SetContract<'S> (ops : SetOps<'S>) =
313310
let struct (set1, set2) = createByRemainder first second
314311

315312
Assert.AreEqual<bool>(
316-
(remainders first = remainders second),
313+
(SetInputs.Remainders first = SetInputs.Remainders second),
317314
ops.SetEquals set1 set2,
318315
$"%s{ops.Name}.setEquals must count the elements that the comparer of the first set finds equal once"
319316
)

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
3838
for element in elements do
3939
Assert.Contains (element, input, $"%s{message} must hold elements of the input")
4040

41+
/// Creates a set from a copy of an input and changes the copy afterwards, so that a set that shared the memory of its
42+
/// source would show the change.
43+
let createFromChangedCopy (create : int array -> 'S) (input : int array) =
44+
let source = Array.copy input
45+
let set = create source
46+
47+
if source.Length > 0 then
48+
source[0] <- 1000
49+
50+
set
51+
4152
// ---- Creation ----
4253

4354
[<TestMethod>]
@@ -46,16 +57,9 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
4657

4758
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
4859
member this.``ofArray copies the distinct elements`` (input : int array) =
49-
// The test changes the array after the call, so it changes a copy of its row
50-
let source = Array.copy input
51-
let set = ops.OfArray source
52-
53-
if source.Length > 0 then
54-
source[0] <- 1000
55-
5660
CollectionAssert.AreEqual (
5761
Set.toArray (Set.ofArray input),
58-
this.Sorted set,
62+
this.Sorted (createFromChangedCopy ops.OfArray input),
5963
$"%s{ops.Name}.ofArray must copy the distinct elements"
6064
)
6165

@@ -82,6 +86,14 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
8286
"ofList of a null list must throw"
8387
)
8488

89+
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
90+
member this.``ofReadOnlySpan copies the distinct elements`` (input : int array) =
91+
CollectionAssert.AreEqual (
92+
Set.toArray (Set.ofArray input),
93+
this.Sorted (createFromChangedCopy ops.OfReadOnlySpan input),
94+
$"%s{ops.Name}.ofReadOnlySpan must copy the distinct elements"
95+
)
96+
8597
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
8698
member this.``ofSeq keeps the distinct elements`` (input : int array) =
8799
let expected = Set.toArray (Set.ofArray input)
@@ -113,6 +125,14 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
113125
"ofSeq of a null sequence must throw"
114126
)
115127

128+
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
129+
member this.``ofSpan copies the distinct elements`` (input : int array) =
130+
CollectionAssert.AreEqual (
131+
Set.toArray (Set.ofArray input),
132+
this.Sorted (createFromChangedCopy ops.OfSpan input),
133+
$"%s{ops.Name}.ofSpan must copy the distinct elements"
134+
)
135+
116136
[<TestMethod>]
117137
member this.``singleton has one element`` () =
118138
CollectionAssert.AreEqual ([| 42 |], this.Sorted (ops.Singleton 42), $"%s{ops.Name}.singleton must have one element")
@@ -127,14 +147,7 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
127147

128148
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
129149
member _.``the struct copies one element of the array for each class of its comparer`` (input : int array) =
130-
// The test changes the array after the call, so it changes a copy of its row
131-
let source = Array.copy input
132-
let set = custom.OfArray source
133-
134-
if source.Length > 0 then
135-
source[0] <- 1000
136-
137-
holdsOnePerRemainder input set $"%s{customName}.ofArray"
150+
holdsOnePerRemainder input (createFromChangedCopy custom.OfArray input) $"%s{customName}.ofArray"
138151

139152
[<TestMethod>]
140153
member this.``the struct throws for a null array as FSharp.Core does`` () =
@@ -158,6 +171,10 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
158171
"forComparer ofList of a null list must throw"
159172
)
160173

174+
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
175+
member _.``the struct copies one element of the read-only span for each class of its comparer`` (input : int array) =
176+
holdsOnePerRemainder input (createFromChangedCopy custom.OfReadOnlySpan input) $"%s{customName}.ofReadOnlySpan"
177+
161178
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
162179
member _.``the struct keeps one element of the sequence for each class of its comparer`` (input : int array) =
163180
holdsOnePerRemainder input (custom.OfSeq (List.ofArray input)) $"%s{customName}.ofSeq of a list"
@@ -184,6 +201,10 @@ type SetCreationContract<'S when 'S :> seq<int>> (ops : SetCreationOps<'S>) =
184201
"forComparer ofSeq of a null sequence must throw"
185202
)
186203

204+
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
205+
member _.``the struct copies one element of the span for each class of its comparer`` (input : int array) =
206+
holdsOnePerRemainder input (createFromChangedCopy custom.OfSpan input) $"%s{customName}.ofSpan"
207+
187208
[<TestMethod>]
188209
member _.``the struct creates a set of one element with its comparer`` () =
189210
holdsOnePerRemainder [| 42 |] (custom.Singleton 42) $"%s{customName}.singleton"

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,37 @@ type SetInputs =
4949
struct (first, second)
5050
}
5151

52+
/// <summary>
53+
/// The pairs of inputs whose second input is a subset of the first one, as rows: the second input adds nothing to the
54+
/// first one, and empty and equal inputs are among them.
55+
/// </summary>
56+
static member SubsetPairs =
57+
SetInputs.Pairs
58+
|> Seq.filter (fun struct (first, second) -> Set.isSubset (Set.ofArray second) (Set.ofArray first))
59+
60+
/// <summary>
61+
/// The pairs of inputs whose remainders in the division by 5 are all remainders of the first input too, as rows: the
62+
/// second input adds nothing to a set that compares by remainders, although it may hold other numbers.
63+
/// </summary>
64+
static member SubsetPairsByRemainder =
65+
SetInputs.Pairs
66+
|> Seq.filter (fun struct (first, second) -> Set.isSubset (SetInputs.Remainders second) (SetInputs.Remainders first))
67+
68+
/// The pairs of inputs that have no number in common, as rows: the second input removes nothing from the first one.
69+
static member DisjointPairs =
70+
SetInputs.Pairs
71+
|> Seq.filter (fun struct (first, second) -> Set.isEmpty (Set.intersect (Set.ofArray first) (Set.ofArray second)))
72+
73+
/// <summary>
74+
/// The pairs of inputs that have no remainder in the division by 5 in common, as rows: the second input removes nothing
75+
/// from a set that compares by remainders, although it may hold numbers of the first input.
76+
/// </summary>
77+
static member DisjointPairsByRemainder =
78+
SetInputs.Pairs
79+
|> Seq.filter (fun struct (first, second) ->
80+
Set.isEmpty (Set.intersect (SetInputs.Remainders first) (SetInputs.Remainders second))
81+
)
82+
5283
/// The values to look up: in no input, in one input and in several inputs.
5384
static member Values = [| 42; 5; -3; 8; 1000 |]
5485

@@ -69,6 +100,10 @@ type SetInputs =
69100
/// </remarks>
70101
static member Remainder (item : int) = ((item % 5) + 5) % 5
71102

103+
/// The remainders of the numbers of an input in the division by 5, as a set: the classes of the numbers for a set that
104+
/// compares by remainders.
105+
static member Remainders (input : int array) = Set.ofArray (Array.map SetInputs.Remainder input)
106+
72107
/// The equality comparer of the structs that create hash and frozen sets: equal remainders make equal numbers.
73108
static member SameRemainder = sameRemainder
74109

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

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ open Microsoft.VisualStudio.TestTools.UnitTesting
1111
type SetMutationContract<'S> (ops : SetMutationOps<'S>) =
1212
inherit SetContractBase<'S> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray)
1313

14+
/// An input in the shapes that a sequence argument takes, each with its name for the message of the assertion: an
15+
/// array, a list and a lazy sequence.
16+
static let shapes (input : int array) = [|
17+
struct ("an array", input :> seq<int>)
18+
struct ("a list", List.ofArray input :> seq<int>)
19+
struct ("a lazy sequence", Seq.map id input)
20+
|]
21+
1422
[<TestMethod; DynamicData(nameof SetInputs.InputsAndValues, typeof<SetInputs>)>]
1523
member this.``add adds a value that the set does not contain`` (input : int array, value : int) =
1624
let struct (set, expected) = this.Create input
@@ -32,6 +40,78 @@ type SetMutationContract<'S> (ops : SetMutationOps<'S>) =
3240
$"%s{ops.Name}.add must not change its input"
3341
)
3442

43+
[<TestMethod; DynamicData(nameof SetInputs.Pairs, typeof<SetInputs>)>]
44+
member this.``addRange adds the elements that the set does not contain`` (first : int array, second : int array) =
45+
let struct (set, expected) = this.Create first
46+
let expected = Set.toArray (Set.union expected (Set.ofArray second))
47+
48+
for struct (shape, elements) in shapes second do
49+
CollectionAssert.AreEqual (
50+
expected,
51+
this.Sorted (ops.AddRange elements set),
52+
$"%s{ops.Name}.addRange of %s{shape} must add the elements that the set does not contain"
53+
)
54+
55+
// The elements of the set stay, and of the elements that are new to it the first one of each class is added (D14)
56+
[<TestMethod; DynamicData(nameof SetInputs.Pairs, typeof<SetInputs>)>]
57+
member this.``addRange keeps the elements of the set that the comparer finds equal`` (first : int array, second : int array) =
58+
let set = ops.OfSeqByRemainder first
59+
let held = ops.ToArray set
60+
let classes = SetInputs.Remainders held
61+
62+
let added =
63+
second
64+
|> Array.filter (fun item -> not (Set.contains (SetInputs.Remainder item) classes))
65+
|> Array.distinctBy SetInputs.Remainder
66+
67+
for struct (shape, elements) in shapes second do
68+
CollectionAssert.AreEqual (
69+
Array.sort (Array.append held added),
70+
this.Sorted (ops.AddRange elements set),
71+
$"%s{ops.Name}.addRange of %s{shape} must keep the elements of the set and add the first of the other elements of each remainder"
72+
)
73+
74+
[<TestMethod>]
75+
member this.``addRange leaves the input unchanged`` () =
76+
let set = ops.OfSeq Inputs.Several
77+
ops.AddRange Inputs.Other set |> ignore
78+
79+
CollectionAssert.AreEqual (
80+
Set.toArray (Set.ofArray Inputs.Several),
81+
this.Sorted set,
82+
$"%s{ops.Name}.addRange must not change its input"
83+
)
84+
85+
[<TestMethod; DynamicData(nameof SetInputs.SubsetPairs, typeof<SetInputs>)>]
86+
member _.``addRange returns the input when the sequence adds no element`` (first : int array, second : int array) =
87+
let set = ops.OfSeq first
88+
89+
Assert.AreSame (
90+
box set,
91+
box (ops.AddRange second set),
92+
$"%s{ops.Name}.addRange must return the input when the sequence adds no element"
93+
)
94+
95+
[<TestMethod; DynamicData(nameof SetInputs.SubsetPairsByRemainder, typeof<SetInputs>)>]
96+
member _.``addRange returns the input when the comparer finds every element of the sequence in the set``
97+
(first : int array, second : int array)
98+
=
99+
let set = ops.OfSeqByRemainder first
100+
101+
Assert.AreSame (
102+
box set,
103+
box (ops.AddRange second set),
104+
$"%s{ops.Name}.addRange must return the input when the comparer finds every element of the sequence in the set"
105+
)
106+
107+
[<TestMethod>]
108+
member this.``addRange throws ArgumentNullException for a null sequence`` () =
109+
this.ThrowsArgumentNull (
110+
"elements",
111+
(fun () -> ops.AddRange (Unchecked.defaultof<seq<int>>) (ops.OfSeq Inputs.Several)),
112+
"addRange of a null sequence must throw"
113+
)
114+
35115
[<TestMethod; DynamicData(nameof SetInputs.All, typeof<SetInputs>)>]
36116
member this.``clear removes every element`` (input : int array) =
37117
let struct (set, _) = this.Create input
@@ -57,3 +137,76 @@ type SetMutationContract<'S> (ops : SetMutationOps<'S>) =
57137
this.Sorted set,
58138
$"%s{ops.Name}.remove must not change its input"
59139
)
140+
141+
[<TestMethod; DynamicData(nameof SetInputs.Pairs, typeof<SetInputs>)>]
142+
member this.``removeRange removes the elements that the sequence contains`` (first : int array, second : int array) =
143+
let struct (set, expected) = this.Create first
144+
let expected = Set.toArray (Set.difference expected (Set.ofArray second))
145+
146+
for struct (shape, elements) in shapes second do
147+
CollectionAssert.AreEqual (
148+
expected,
149+
this.Sorted (ops.RemoveRange elements set),
150+
$"%s{ops.Name}.removeRange of %s{shape} must remove the elements that the sequence contains"
151+
)
152+
153+
// The comparer of the set decides which elements are equal to those of the sequence (D14)
154+
[<TestMethod; DynamicData(nameof SetInputs.Pairs, typeof<SetInputs>)>]
155+
member this.``removeRange removes the elements that the comparer finds equal`` (first : int array, second : int array) =
156+
let set = ops.OfSeqByRemainder first
157+
let classes = SetInputs.Remainders second
158+
159+
let remaining =
160+
ops.ToArray set
161+
|> Array.filter (fun item -> not (Set.contains (SetInputs.Remainder item) classes))
162+
|> Array.sort
163+
164+
for struct (shape, elements) in shapes second do
165+
CollectionAssert.AreEqual (
166+
remaining,
167+
this.Sorted (ops.RemoveRange elements set),
168+
$"%s{ops.Name}.removeRange of %s{shape} must remove the elements that the comparer finds equal to those of the sequence"
169+
)
170+
171+
[<TestMethod>]
172+
member this.``removeRange leaves the input unchanged`` () =
173+
let set = ops.OfSeq Inputs.Several
174+
ops.RemoveRange Inputs.Other set |> ignore
175+
176+
CollectionAssert.AreEqual (
177+
Set.toArray (Set.ofArray Inputs.Several),
178+
this.Sorted set,
179+
$"%s{ops.Name}.removeRange must not change its input"
180+
)
181+
182+
[<TestMethod; DynamicData(nameof SetInputs.DisjointPairs, typeof<SetInputs>)>]
183+
member _.``removeRange returns the input when the sequence has no element of the set``
184+
(first : int array, second : int array)
185+
=
186+
let set = ops.OfSeq first
187+
188+
Assert.AreSame (
189+
box set,
190+
box (ops.RemoveRange second set),
191+
$"%s{ops.Name}.removeRange must return the input when the sequence has no element of the set"
192+
)
193+
194+
[<TestMethod; DynamicData(nameof SetInputs.DisjointPairsByRemainder, typeof<SetInputs>)>]
195+
member _.``removeRange returns the input when the comparer finds no element of the sequence in the set``
196+
(first : int array, second : int array)
197+
=
198+
let set = ops.OfSeqByRemainder first
199+
200+
Assert.AreSame (
201+
box set,
202+
box (ops.RemoveRange second set),
203+
$"%s{ops.Name}.removeRange must return the input when the comparer finds no element of the sequence in the set"
204+
)
205+
206+
[<TestMethod>]
207+
member this.``removeRange throws ArgumentNullException for a null sequence`` () =
208+
this.ThrowsArgumentNull (
209+
"elements",
210+
(fun () -> ops.RemoveRange (Unchecked.defaultof<seq<int>>) (ops.OfSeq Inputs.Several)),
211+
"removeRange of a null sequence must throw"
212+
)

0 commit comments

Comments
 (0)