Skip to content

Commit 7acdb0e

Browse files
authored
Merge branch 'main' into add-foldUntil
2 parents e1257b5 + 41538fc commit 7acdb0e

13 files changed

Lines changed: 587 additions & 133 deletions

.github/aw/actions-lock.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
{
22
"entries": {
3+
"actions/github-script@v9": {
4+
"repo": "actions/github-script",
5+
"version": "v9",
6+
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
7+
},
38
"actions/github-script@v9.0.0": {
49
"repo": "actions/github-script",
510
"version": "v9.0.0",
611
"sha": "d746ffe35508b1917358783b479e04febd2b8f71"
712
},
8-
"github/gh-aw-actions/setup@v0.68.3": {
13+
"github/gh-aw-actions/setup@v0.71.3": {
914
"repo": "github/gh-aw-actions/setup",
10-
"version": "v0.68.3",
11-
"sha": "ba90f2186d7ad780ec640f364005fa24e797b360"
15+
"version": "v0.71.3",
16+
"sha": "07c7335cd76c4d4d9f00dd7874f85ff55ed71f24"
1217
},
1318
"github/gh-aw/actions/setup@v0.68.7": {
1419
"repo": "github/gh-aw/actions/setup",

.github/workflows/main.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,35 @@ jobs:
6565
# this path glob pattern requires forward slashes!
6666
path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx
6767
reporter: dotnet-trx
68+
69+
test-release-linux:
70+
name: Test Release Build (Linux)
71+
runs-on: ubuntu-latest
72+
steps:
73+
- name: checkout-code
74+
uses: actions/checkout@v6
75+
with:
76+
fetch-depth: 0
77+
78+
- name: setup-dotnet
79+
uses: actions/setup-dotnet@v4
80+
81+
- name: Cache NuGet packages
82+
uses: actions/cache@v5
83+
with:
84+
path: ~/.nuget/packages
85+
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }}
86+
restore-keys: nuget-${{ runner.os }}-
87+
88+
# build.cmd is Windows-only; run dotnet test directly on Linux
89+
- name: Run dotnet test - release (Linux)
90+
run: dotnet test src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj -c Release --blame-hang-timeout 60000ms --logger "console;verbosity=detailed" --logger "trx;LogFileName=test-results-release-linux.trx"
91+
92+
- name: Publish test results - release (Linux)
93+
uses: dorny/test-reporter@v2
94+
if: always()
95+
with:
96+
name: Report release tests (Linux)
97+
# this path glob pattern requires forward slashes!
98+
path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release-linux.trx
99+
reporter: dotnet-trx

.github/workflows/repo-assist.lock.yml

Lines changed: 280 additions & 104 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/repo-assist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: |
1414
Always polite, constructive, and mindful of the project's goals.
1515
1616
on:
17-
schedule: every 48 hours
17+
schedule: weekly
1818
workflow_dispatch:
1919
slash_command:
2020
name: repo-assist

release-notes.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ Release notes:
33

44
Unreleased
55
- adds TaskSeq.foldWhile and TaskSeq.foldWhileAsync: fold with early termination via a (state, element) -> bool predicate (takeWhile-style, exclusive). When the predicate returns false, iteration halts without folding that element; no further elements are enumerated.
6+
- feat: add `TaskSeq.toChannelAsync` and `TaskSeq.ofChannel` for bidirectional `System.Threading.Channels` integration, closing #415
7+
- eng: update PackageValidationBaselineVersion from 0.4.0 to 1.1.1 to enforce binary compatibility checks against the current stable release
68
- test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync
9+
- test: add SideEffects module to TaskSeq.WithCancellation.Tests.fs, verifying re-iteration semantics are preserved when wrapping with a CancellationToken
710
- fixes: `Async.bind` signature corrected from `(Async<'T> -> Async<'U>)` to `('T -> Async<'U>)` to match standard monadic bind semantics (same as `Task.bind`); the previous signature made the function effectively equivalent to direct application
811
- refactor: simplify splitAt 'rest' taskSeq to use while!, removing redundant go2 mutable and manual MoveNextAsync pre-advance
912

src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
highest version available.
2020
This ensures that, if we have a forwards compat issue, we will get an error.
2121
-->
22-
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0" />
23-
<PackageReference Include="FsToolkit.ErrorHandling.TaskResult" Version="4.15.1" />
22+
<PackageReference Include="FSharp.Control.TaskSeq" Version="1.1.1" />
23+
<PackageReference Include="FsToolkit.ErrorHandling.TaskResult" Version="4.18.0" />
2424
<PackageReference Include="FsUnit.xUnit" Version="6.0.1" />
2525
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
2626
<PackageReference Include="xunit" Version="2.9.3" />

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,81 @@ module Immutable =
9191
let! result = TaskSeq.singleton 42 |> TaskSeq.lastOrDefault 0
9292
result |> should equal 42
9393
}
94+
95+
96+
module SideEffects =
97+
[<Fact>]
98+
let ``TaskSeq-firstOrDefault __special-case__ prove it does not read beyond first yield`` () = task {
99+
let mutable x = 42
100+
101+
let ts = taskSeq {
102+
yield x
103+
x <- x + 1 // we never get here
104+
}
105+
106+
let! fortyTwo = ts |> TaskSeq.firstOrDefault 0
107+
let! stillFortyTwo = ts |> TaskSeq.firstOrDefault 0 // the statement after 'yield' will never be reached
108+
109+
fortyTwo |> should equal 42
110+
stillFortyTwo |> should equal 42
111+
}
112+
113+
[<Fact>]
114+
let ``TaskSeq-firstOrDefault __special-case__ prove early side effect is executed`` () = task {
115+
let mutable x = 42
116+
117+
let ts = taskSeq {
118+
x <- x + 1
119+
x <- x + 1
120+
yield 42
121+
x <- x + 200 // we won't get here!
122+
}
123+
124+
let! result = ts |> TaskSeq.firstOrDefault 0
125+
result |> should equal 42
126+
x |> should equal 44
127+
128+
let! result = ts |> TaskSeq.firstOrDefault 0
129+
result |> should equal 42
130+
x |> should equal 46
131+
}
132+
133+
[<Fact>]
134+
let ``TaskSeq-lastOrDefault __special-case__ prove it reads the entire sequence`` () = task {
135+
let mutable x = 42
136+
137+
let ts = taskSeq {
138+
yield x
139+
x <- x + 1 // will be executed
140+
yield x
141+
x <- x + 1 // will be executed
142+
}
143+
144+
let! result = ts |> TaskSeq.lastOrDefault -1
145+
result |> should equal 43
146+
x |> should equal 44
147+
}
148+
149+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
150+
let ``TaskSeq-firstOrDefault returns first item in a side-effect sequence`` variant = task {
151+
let ts = Gen.getSeqWithSideEffect variant
152+
153+
let! first = ts |> TaskSeq.firstOrDefault 0
154+
first |> should equal 1
155+
156+
// side effect: re-enumerating changes the first item
157+
let! secondFirst = ts |> TaskSeq.firstOrDefault 0
158+
secondFirst |> should not' (equal 1)
159+
}
160+
161+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
162+
let ``TaskSeq-lastOrDefault returns last item and exhausts the sequence`` variant = task {
163+
let ts = Gen.getSeqWithSideEffect variant
164+
165+
let! last = ts |> TaskSeq.lastOrDefault 0
166+
last |> should equal 10
167+
168+
// side effect: re-enumerating continues from mutated state
169+
let! secondLast = ts |> TaskSeq.lastOrDefault 0
170+
secondLast |> should equal 20
171+
}

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module TaskSeq.Tests.``Conversion-To``
22

33
open System.Collections.Generic
4+
open System.Threading.Channels
45

56
open Xunit
67
open FsUnit.Xunit
@@ -186,3 +187,72 @@ module SideEffects =
186187
let (results2: seq<_>) = tq |> TaskSeq.toSeq
187188
results1 |> Seq.toArray |> should equal [| 1..10 |]
188189
results2 |> Seq.toArray |> should equal [| 11..20 |]
190+
191+
module Channel =
192+
193+
[<Fact>]
194+
let ``TaskSeq-toChannelAsync with null writer raises`` () =
195+
assertNullArg
196+
<| fun () ->
197+
TaskSeq.toChannelAsync null (TaskSeq.ofArray [| 1 |])
198+
|> ignore
199+
200+
[<Fact>]
201+
let ``TaskSeq-toChannelAsync with null source raises`` () =
202+
let ch = Channel.CreateUnbounded<int>()
203+
204+
assertNullArg
205+
<| fun () -> TaskSeq.toChannelAsync ch.Writer null |> ignore
206+
207+
[<Fact>]
208+
let ``TaskSeq-ofChannel with null reader raises`` () =
209+
assertNullArg
210+
<| fun () -> TaskSeq.ofChannel<int> null |> ignore
211+
212+
[<Fact>]
213+
let ``TaskSeq-toChannelAsync with empty source completes the channel`` () = task {
214+
let ch = Channel.CreateUnbounded<int>()
215+
do! TaskSeq.toChannelAsync ch.Writer TaskSeq.empty
216+
ch.Reader.Completion.IsCompleted |> should be True
217+
let! results = TaskSeq.ofChannel ch.Reader |> TaskSeq.toArrayAsync
218+
results |> should be Empty
219+
}
220+
221+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
222+
let ``TaskSeq-toChannelAsync writes all elements and completes the channel`` variant = task {
223+
let tq = Gen.getSeqImmutable variant
224+
let ch = Channel.CreateUnbounded<int>()
225+
do! TaskSeq.toChannelAsync ch.Writer tq
226+
let! results = TaskSeq.ofChannel ch.Reader |> TaskSeq.toArrayAsync
227+
results |> should equal [| 1..10 |]
228+
// Completion resolves once the channel is marked done and the buffer is drained
229+
do! ch.Reader.Completion
230+
}
231+
232+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
233+
let ``TaskSeq-ofChannel yields all elements written to the channel`` variant = task {
234+
let tq = Gen.getSeqImmutable variant
235+
let ch = Channel.CreateUnbounded<int>()
236+
do! TaskSeq.toChannelAsync ch.Writer tq
237+
let! results = TaskSeq.ofChannel ch.Reader |> TaskSeq.toArrayAsync
238+
results |> should equal [| 1..10 |]
239+
}
240+
241+
[<Fact>]
242+
let ``TaskSeq-ofChannel ends when channel is completed and drained`` () = task {
243+
let ch = Channel.CreateUnbounded<int>()
244+
do! ch.Writer.WriteAsync 42
245+
do! ch.Writer.WriteAsync 99
246+
ch.Writer.Complete()
247+
let! results = TaskSeq.ofChannel ch.Reader |> TaskSeq.toArrayAsync
248+
results |> should equal [| 42; 99 |]
249+
}
250+
251+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
252+
let ``TaskSeq-toChannelAsync executes side effects`` variant = task {
253+
let tq = Gen.getSeqWithSideEffect variant
254+
let ch = Channel.CreateUnbounded<int>()
255+
do! TaskSeq.toChannelAsync ch.Writer tq
256+
let! results = TaskSeq.ofChannel ch.Reader |> TaskSeq.toArrayAsync
257+
results |> should equal [| 1..10 |]
258+
}

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,57 @@ module ``Sequence contents`` =
170170

171171
collected |> Seq.toArray |> should equal [| 1..5 |]
172172
}
173+
174+
module SideEffects =
175+
176+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
177+
let ``TaskSeq-withCancellation applied multiple times`` variant = task {
178+
let ts = Gen.getSeqWithSideEffect variant
179+
let wrapped = TaskSeq.withCancellation CancellationToken.None ts
180+
181+
let! first = wrapped |> TaskSeq.toArrayAsync
182+
let! second = wrapped |> TaskSeq.toArrayAsync
183+
let! third = wrapped |> TaskSeq.toArrayAsync
184+
185+
first |> should equal [| 1..10 |]
186+
second |> should equal [| 11..20 |]
187+
third |> should equal [| 21..30 |]
188+
}
189+
190+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
191+
let ``TaskSeq-withCancellation with active CancellationToken applied multiple times`` variant = task {
192+
use cts = new CancellationTokenSource()
193+
let ts = Gen.getSeqWithSideEffect variant
194+
let wrapped = TaskSeq.withCancellation cts.Token ts
195+
196+
let! first = wrapped |> TaskSeq.toArrayAsync
197+
let! second = wrapped |> TaskSeq.toArrayAsync
198+
199+
first |> should equal [| 1..10 |]
200+
second |> should equal [| 11..20 |]
201+
}
202+
203+
[<Fact>]
204+
let ``TaskSeq-withCancellation evaluates each source element exactly once per iteration`` () = task {
205+
let mutable count = 0
206+
207+
let ts = taskSeq {
208+
for i in 1..5 do
209+
count <- count + 1
210+
yield i
211+
}
212+
213+
let! _ =
214+
ts
215+
|> TaskSeq.withCancellation CancellationToken.None
216+
|> TaskSeq.toArrayAsync
217+
218+
count |> should equal 5
219+
220+
let! _ =
221+
ts
222+
|> TaskSeq.withCancellation CancellationToken.None
223+
|> TaskSeq.toArrayAsync
224+
225+
count |> should equal 10
226+
}

src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,4 @@
88
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
99
<IsBaselineSuppression>true</IsBaselineSuppression>
1010
</Suppression>
11-
<Suppression>
12-
<DiagnosticId>CP0002</DiagnosticId>
13-
<Target>M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind``5(FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}})</Target>
14-
<Left>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Left>
15-
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
16-
<IsBaselineSuppression>true</IsBaselineSuppression>
17-
</Suppression>
18-
<Suppression>
19-
<DiagnosticId>CP0002</DiagnosticId>
20-
<Target>M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind$W``5(Microsoft.FSharp.Core.FSharpFunc{``0,``3},Microsoft.FSharp.Core.FSharpFunc{``3,``1},Microsoft.FSharp.Core.FSharpFunc{``3,System.Boolean},FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}})</Target>
21-
<Left>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Left>
22-
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
23-
<IsBaselineSuppression>true</IsBaselineSuppression>
24-
</Suppression>
25-
<Suppression>
26-
<DiagnosticId>CP0002</DiagnosticId>
27-
<Target>M:FSharp.Control.TaskExtensions.TaskBuilder#For``2(Microsoft.FSharp.Control.TaskBuilder,System.Collections.Generic.IAsyncEnumerable{``0},Microsoft.FSharp.Core.FSharpFunc{``0,Microsoft.FSharp.Core.CompilerServices.ResumableCode{Microsoft.FSharp.Control.TaskStateMachineData{``1},Microsoft.FSharp.Core.Unit}})</Target>
28-
<Left>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Left>
29-
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
30-
<IsBaselineSuppression>true</IsBaselineSuppression>
31-
</Suppression>
3211
</Suppressions>

0 commit comments

Comments
 (0)