Skip to content

Commit 56b362b

Browse files
authored
Merge branch 'main' into repo-assist/docs-update-readme-0.6.0-2026-03-f0c7c614ff95add8
2 parents 88c3022 + bd4d8ec commit 56b362b

File tree

10 files changed

+484
-6
lines changed

10 files changed

+484
-6
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
212212
- [x] `forall` / `forallAsync` (see [#240])
213213
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
214214
- [x] `chunkBySize` / `windowed` (see [#258])
215-
- [ ] `compareWith`
216-
- [x] `distinct` / `distinctBy` (see [#305])
215+
- [x] `compareWith` / `compareWithAsync`
216+
- [x] `distinct`
217217
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
218218
- [x] `mapFold` (see [#306])
219219
- [x] `pairwise` (see [#293])
@@ -267,7 +267,7 @@ This is what has been implemented so far, is planned or skipped:
267267
| ✅ [#258][] | `chunkBySize` | `chunkBySize` | | |
268268
| ✅ [#11][] | `collect` | `collect` | `collectAsync` | |
269269
| ✅ [#11][] | | `collectSeq` | `collectSeqAsync` | |
270-
| | `compareWith` | `compareWith` | `compareWithAsync` | |
270+
| ✅ | `compareWith` | `compareWith` | `compareWithAsync` | |
271271
| ✅ [#69][] | `concat` | `concat` | | |
272272
| ✅ [#237][]| `concat` (list) | `concat` (list) | | |
273273
| ✅ [#237][]| `concat` (array) | `concat` (array) | | |

release-notes.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Release notes:
33

44
0.6.0
5+
- fixes: async { for item in taskSeq do ... } no longer wraps exceptions in AggregateException, #129
6+
- adds TaskSeq.compareWith and TaskSeq.compareWithAsync
57
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
68
- adds TaskSeq.pairwise, #289
79
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
5959
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
6060
<Compile Include="TaskSeq.Zip.Tests.fs" />
61+
<Compile Include="TaskSeq.CompareWith.Tests.fs" />
6162
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
6263
<Compile Include="TaskSeq.Windowed.Tests.fs" />
6364
<Compile Include="TaskSeq.Tests.CE.fs" />

src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module TaskSeq.Tests.AsyncExtensions
22

3+
open System
34
open Xunit
45
open FsUnit.Xunit
56

@@ -115,6 +116,44 @@ module SideEffects =
115116
sum |> should equal 465 // eq to: List.sum [1..30]
116117
}
117118

119+
module ExceptionPropagation =
120+
[<Fact>]
121+
let ``Async-for CE propagates exception without AggregateException wrapping`` () =
122+
// Verifies fix for https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129
123+
// Async.AwaitTask previously wrapped all task exceptions in AggregateException,
124+
// breaking try/catch blocks in async {} expressions that expect the original type.
125+
let run () = async {
126+
let values = taskSeq { yield 1 }
127+
128+
try
129+
for _ in values do
130+
raise (InvalidOperationException "test error")
131+
with :? InvalidOperationException ->
132+
()
133+
}
134+
135+
// Should complete without AggregateException escaping
136+
run () |> Async.RunSynchronously
137+
138+
[<Fact>]
139+
let ``Async-for CE try-catch catches original exception type, not AggregateException`` () =
140+
// Verifies that the original exception type is visible in catch blocks,
141+
// not wrapped in AggregateException as Async.AwaitTask used to do.
142+
let mutable caughtType: Type option = None
143+
144+
let run () = async {
145+
let values = taskSeq { yield 1 }
146+
147+
try
148+
for _ in values do
149+
raise (ArgumentException "test")
150+
with ex ->
151+
caughtType <- Some(ex.GetType())
152+
}
153+
154+
run () |> Async.RunSynchronously
155+
caughtType |> should equal (Some typeof<ArgumentException>)
156+
118157
module Other =
119158
[<Fact>]
120159
let ``Async-for CE must call dispose in empty taskSeq`` () = async {
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
module TaskSeq.Tests.CompareWith
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.compareWith
10+
// TaskSeq.compareWithAsync
11+
//
12+
13+
let inline sign x =
14+
if x < 0 then -1
15+
elif x > 0 then 1
16+
else 0
17+
18+
module EmptySeq =
19+
[<Fact>]
20+
let ``Null source1 is invalid`` () =
21+
assertNullArg
22+
<| fun () -> TaskSeq.compareWith compare null TaskSeq.empty<int>
23+
24+
assertNullArg
25+
<| fun () -> TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) null TaskSeq.empty<int>
26+
27+
[<Fact>]
28+
let ``Null source2 is invalid`` () =
29+
assertNullArg
30+
<| fun () -> TaskSeq.compareWith compare TaskSeq.empty<int> null
31+
32+
assertNullArg
33+
<| fun () -> TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) TaskSeq.empty<int> null
34+
35+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
36+
let ``TaskSeq-compareWith of two empty sequences is 0`` variant = task {
37+
let empty = Gen.getEmptyVariant variant
38+
let! result = TaskSeq.compareWith compare empty TaskSeq.empty<int>
39+
result |> should equal 0
40+
}
41+
42+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
43+
let ``TaskSeq-compareWithAsync of two empty sequences is 0`` variant = task {
44+
let empty = Gen.getEmptyVariant variant
45+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) empty TaskSeq.empty<int>
46+
result |> should equal 0
47+
}
48+
49+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
50+
let ``TaskSeq-compareWith: empty source1 is less than non-empty source2`` variant = task {
51+
let empty = Gen.getEmptyVariant variant
52+
let! result = TaskSeq.compareWith compare empty (TaskSeq.singleton 1)
53+
result |> should equal -1
54+
}
55+
56+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
57+
let ``TaskSeq-compareWith: non-empty source1 is greater than empty source2`` variant = task {
58+
let empty = Gen.getEmptyVariant variant
59+
let! result = TaskSeq.compareWith compare (TaskSeq.singleton 1) empty
60+
result |> should equal 1
61+
}
62+
63+
64+
module Immutable =
65+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
66+
let ``TaskSeq-compareWith: equal sequences return 0`` variant = task {
67+
let src1 = Gen.getSeqImmutable variant
68+
let src2 = Gen.getSeqImmutable variant
69+
let! result = TaskSeq.compareWith compare src1 src2
70+
result |> should equal 0
71+
}
72+
73+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
74+
let ``TaskSeq-compareWithAsync: equal sequences return 0`` variant = task {
75+
let src1 = Gen.getSeqImmutable variant
76+
let src2 = Gen.getSeqImmutable variant
77+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) src1 src2
78+
result |> should equal 0
79+
}
80+
81+
[<Fact>]
82+
let ``TaskSeq-compareWith: first element differs`` () = task {
83+
let src1 = taskSeq {
84+
1
85+
2
86+
3
87+
}
88+
89+
let src2 = taskSeq {
90+
2
91+
2
92+
3
93+
}
94+
95+
let! result = TaskSeq.compareWith compare src1 src2
96+
sign result |> should equal -1
97+
}
98+
99+
[<Fact>]
100+
let ``TaskSeq-compareWith: last element differs`` () = task {
101+
let src1 = taskSeq {
102+
1
103+
2
104+
4
105+
}
106+
107+
let src2 = taskSeq {
108+
1
109+
2
110+
3
111+
}
112+
113+
let! result = TaskSeq.compareWith compare src1 src2
114+
sign result |> should equal 1
115+
}
116+
117+
[<Fact>]
118+
let ``TaskSeq-compareWith: source1 shorter returns negative`` () = task {
119+
let src1 = taskSeq {
120+
1
121+
2
122+
}
123+
124+
let src2 = taskSeq {
125+
1
126+
2
127+
3
128+
}
129+
130+
let! result = TaskSeq.compareWith compare src1 src2
131+
result |> should equal -1
132+
}
133+
134+
[<Fact>]
135+
let ``TaskSeq-compareWith: source2 shorter returns positive`` () = task {
136+
let src1 = taskSeq {
137+
1
138+
2
139+
3
140+
}
141+
142+
let src2 = taskSeq {
143+
1
144+
2
145+
}
146+
147+
let! result = TaskSeq.compareWith compare src1 src2
148+
result |> should equal 1
149+
}
150+
151+
[<Fact>]
152+
let ``TaskSeq-compareWith: uses custom comparer result sign`` () = task {
153+
// comparer returns a large number, not just -1/0/1
154+
let bigCompare (a: int) (b: int) = (a - b) * 100
155+
156+
let src1 = taskSeq {
157+
1
158+
2
159+
3
160+
}
161+
162+
let src2 = taskSeq {
163+
1
164+
2
165+
5
166+
}
167+
168+
let! result = TaskSeq.compareWith bigCompare src1 src2
169+
// 3 compared to 5 gives (3-5)*100 = -200, which is negative
170+
result |> should be (lessThan 0)
171+
}
172+
173+
[<Fact>]
174+
let ``TaskSeq-compareWith: stops at first non-zero comparison`` () = task {
175+
let mutable callCount = 0
176+
177+
let countingCompare a b =
178+
callCount <- callCount + 1
179+
compare a b
180+
181+
let src1 = taskSeq {
182+
1
183+
99
184+
99
185+
99
186+
}
187+
188+
let src2 = taskSeq {
189+
2
190+
99
191+
99
192+
99
193+
}
194+
195+
let! result = TaskSeq.compareWith countingCompare src1 src2
196+
sign result |> should equal -1
197+
// Should stop after first comparison
198+
callCount |> should equal 1
199+
}
200+
201+
[<Fact>]
202+
let ``TaskSeq-compareWithAsync: async comparer works`` () = task {
203+
let src1 = taskSeq {
204+
1
205+
2
206+
3
207+
}
208+
209+
let src2 = taskSeq {
210+
1
211+
2
212+
4
213+
}
214+
215+
let! result =
216+
TaskSeq.compareWithAsync
217+
(fun a b -> task {
218+
// simulate async work with a yield point
219+
return compare a b
220+
})
221+
src1
222+
src2
223+
224+
sign result |> should equal -1
225+
}
226+
227+
[<Fact>]
228+
let ``TaskSeq-compareWithAsync: async comparer works correctly`` () = task {
229+
let src1 = taskSeq {
230+
10
231+
20
232+
30
233+
}
234+
235+
let src2 = taskSeq {
236+
10
237+
20
238+
30
239+
}
240+
241+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) src1 src2
242+
result |> should equal 0
243+
}

0 commit comments

Comments
 (0)