├── .config
└── dotnet-tools.json
├── .github
├── dependabot.yml
└── workflows
│ ├── build.yaml
│ ├── main.yaml
│ ├── publish.yaml
│ ├── test-report.yaml
│ └── test.yaml
├── .gitignore
├── Directory.Build.props
├── LICENSE
├── README.md
├── Version.props
├── assets
├── nuget-package-readme.md
└── taskseq-icon.png
├── backdate-tags.cmd
├── build.cmd
├── release-notes.txt
└── src
├── .config
└── dotnet-tools.json
├── .editorconfig
├── FSharp.Control.TaskSeq.SmokeTests
├── FSharp.Control.TaskSeq.SmokeTests.fsproj
├── SmokeTests.fs
├── TaskSeq.PocTests.fs
└── TestUtils.fs
├── FSharp.Control.TaskSeq.Test
├── AssemblyInfo.fs
├── FSharp.Control.TaskSeq.Test.fsproj
├── TaskSeq.Append.Tests.fs
├── TaskSeq.AsyncExtensions.Tests.fs
├── TaskSeq.Cast.Tests.fs
├── TaskSeq.Choose.Tests.fs
├── TaskSeq.Collect.Tests.fs
├── TaskSeq.Concat.Tests.fs
├── TaskSeq.Contains.Tests.fs
├── TaskSeq.Delay.Tests.fs
├── TaskSeq.Do.Tests.fs
├── TaskSeq.Empty.Tests.fs
├── TaskSeq.ExactlyOne.Tests.fs
├── TaskSeq.Except.Tests.fs
├── TaskSeq.Exists.Tests.fs
├── TaskSeq.Filter.Tests.fs
├── TaskSeq.Find.Tests.fs
├── TaskSeq.FindIndex.Tests.fs
├── TaskSeq.Fold.Tests.fs
├── TaskSeq.Forall.Tests.fs
├── TaskSeq.Head.Tests.fs
├── TaskSeq.Indexed.Tests.fs
├── TaskSeq.Init.Tests.fs
├── TaskSeq.InsertAt.Tests.fs
├── TaskSeq.IsEmpty.fs
├── TaskSeq.Item.Tests.fs
├── TaskSeq.Iter.Tests.fs
├── TaskSeq.Last.Tests.fs
├── TaskSeq.Length.Tests.fs
├── TaskSeq.Let.Tests.fs
├── TaskSeq.Map.Tests.fs
├── TaskSeq.MaxMin.Tests.fs
├── TaskSeq.OfXXX.Tests.fs
├── TaskSeq.Pick.Tests.fs
├── TaskSeq.Realworld.fs
├── TaskSeq.RemoveAt.Tests.fs
├── TaskSeq.Singleton.Tests.fs
├── TaskSeq.Skip.Tests.fs
├── TaskSeq.SkipWhile.Tests.fs
├── TaskSeq.StateTransitionBug-delayed.Tests.CE.fs
├── TaskSeq.StateTransitionBug.Tests.CE.fs
├── TaskSeq.Tail.Tests.fs
├── TaskSeq.Take.Tests.fs
├── TaskSeq.TakeWhile.Tests.fs
├── TaskSeq.TaskExtensions.Tests.fs
├── TaskSeq.Tests.CE.fs
├── TaskSeq.ToXXX.Tests.fs
├── TaskSeq.UpdateAt.Tests.fs
├── TaskSeq.Using.Tests.fs
├── TaskSeq.Zip.Tests.fs
├── TestUtils.fs
├── Traces
│ ├── TRACE_FAIL 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt
│ └── TRACE_SUCCESS 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt
├── Xunit.Extensions.fs
├── fail-trace.txt
└── success-trace.txt
├── FSharp.Control.TaskSeq.sln
├── FSharp.Control.TaskSeq.sln.DotSettings
├── FSharp.Control.TaskSeq.v3.ncrunchsolution
└── FSharp.Control.TaskSeq
├── AssemblyInfo.fs
├── AsyncExtensions.fs
├── AsyncExtensions.fsi
├── DebugUtils.fs
├── FSharp.Control.TaskSeq.fsproj
├── TaskExtensions.fs
├── TaskExtensions.fsi
├── TaskSeq.fs
├── TaskSeq.fsi
├── TaskSeqBuilder.fs
├── TaskSeqBuilder.fsi
├── TaskSeqInternal.fs
├── Utils.fs
└── Utils.fsi
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "fantomas": {
6 | "version": "6.3.0-alpha-004",
7 | "commands": [
8 | "fantomas"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | ignore:
6 | # ignore all patch and pre-release updates
7 | - dependency-name: "*"
8 | update-types: ["version-update:semver-patch"]
9 | schedule:
10 | interval: daily
11 | open-pull-requests-limit: 10
12 |
13 | - package-ecosystem: nuget
14 | directory: "/"
15 | ignore:
16 | # ignore all patch and pre-release updates
17 | - dependency-name: "*"
18 | update-types: ["version-update:semver-patch"]
19 | schedule:
20 | interval: daily
21 | open-pull-requests-limit: 10
22 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: ci-build
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | verify_formatting:
7 | runs-on: ubuntu-latest
8 | name: Verify code formatting
9 |
10 | steps:
11 | - name: checkout-code
12 | uses: actions/checkout@v4
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: setup-dotnet
17 | uses: actions/setup-dotnet@v4
18 |
19 | - name: tool restore
20 | run: dotnet tool restore
21 |
22 | - name: validate formatting
23 | run: dotnet fantomas . --check
24 |
25 | build:
26 | name: Build
27 | runs-on: windows-latest
28 | steps:
29 | # checkout the code
30 | - name: checkout-code
31 | uses: actions/checkout@v4
32 | with:
33 | fetch-depth: 0
34 |
35 | # setup dotnet based on global.json
36 | - name: setup-dotnet
37 | uses: actions/setup-dotnet@v4
38 |
39 | # build it, test it, pack it
40 | - name: Run dotnet build (release)
41 | # see issue #105
42 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
43 | shell: cmd
44 | run: ./build.cmd
45 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: Build main (release)
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | name: Build
11 | runs-on: windows-latest
12 | steps:
13 | # checkout the code
14 | - name: checkout-code
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 | # setup dotnet based on global.json
19 | - name: setup-dotnet
20 | uses: actions/setup-dotnet@v4
21 | # build it, test it, pack it
22 | - name: Run dotnet build (release)
23 | # see issue #105
24 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
25 | shell: cmd
26 | run: ./build.cmd
27 |
28 | test-release:
29 | name: Test Release Build
30 | runs-on: windows-latest
31 | steps:
32 | # checkout the code
33 | - name: checkout-code
34 | uses: actions/checkout@v4
35 | with:
36 | fetch-depth: 0
37 | # setup dotnet based on global.json
38 | - name: setup-dotnet
39 | uses: actions/setup-dotnet@v4
40 | # build it, test it, pack it
41 | - name: Run dotnet test - release
42 | # see issue #105
43 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
44 | shell: cmd
45 | run: ./build.cmd ci -release
46 | - name: Publish test results - release
47 | uses: dorny/test-reporter@v1
48 | if: always()
49 | with:
50 | name: Report release tests
51 | # this path glob pattern requires forward slashes!
52 | path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx
53 | reporter: dotnet-trx
54 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Pack & Publish Nuget
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | publish:
10 | name: Publish nuget (if new version)
11 | runs-on: windows-latest
12 | steps:
13 | # checkout the code
14 | - name: checkout-code
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 | # setup dotnet based on global.json
19 | - name: setup-dotnet
20 | uses: actions/setup-dotnet@v4
21 | # build it, test it, pack it, publish it
22 | - name: Run dotnet build (release, for nuget)
23 | # see issue #105 and #243
24 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
25 | shell: cmd
26 | run: ./build.cmd
27 | - name: Nuget publish
28 | # skip-duplicate ensures that the 409 error received when the package was already published,
29 | # will just issue a warning and won't have the GH action fail.
30 | # NUGET_PUBLISH_TOKEN_TASKSEQ is valid until approx. 11 Dec 2024 and will need to be updated by then:
31 | # - log in to Nuget.org using 'abelbraaksma' admin account and then refresh the token in Nuget
32 | # - copy the token
33 | # - go to https://github.com/fsprojects/FSharp.Control.TaskSeq/settings/secrets/actions
34 | # - select button "Add repository secret" or update the existing one under "Repository secrets"
35 | # - rerun the job
36 | run: dotnet nuget push packages\FSharp.Control.TaskSeq.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PUBLISH_TOKEN_TASKSEQ }} --skip-duplicate
37 |
--------------------------------------------------------------------------------
/.github/workflows/test-report.yaml:
--------------------------------------------------------------------------------
1 | name: ci-report
2 |
3 | # See Dorny instructions for why we need a separate yaml for creating a test report
4 | # for public repositories that accept forks:
5 | # https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
6 |
7 | on:
8 | workflow_run:
9 | workflows: ['ci-test'] # runs after CI workflow
10 | types:
11 | - completed
12 | jobs:
13 | test-report-release:
14 | runs-on: windows-latest
15 | steps:
16 | - uses: dorny/test-reporter@v1
17 | with:
18 | artifact: test-results-release # artifact name
19 | name: Report release tests # Name of the check run which will be created
20 | path: '*.trx' # Path to test results (inside artifact .zip)
21 | reporter: dotnet-trx # Format of test results
22 |
23 | test-report-debug:
24 | runs-on: windows-latest
25 | steps:
26 | - uses: dorny/test-reporter@v1
27 | with:
28 | artifact: test-results-debug # artifact name
29 | name: Report debug tests # Name of the check run which will be created
30 | path: '*.trx' # Path to test results (inside artifact .zip)
31 | reporter: dotnet-trx # Format of test results
32 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: ci-test
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | test-release:
7 | name: Test Release Build
8 | runs-on: windows-latest
9 | steps:
10 | # checkout the code
11 | - name: checkout-code
12 | uses: actions/checkout@v4
13 | with:
14 | fetch-depth: 0
15 |
16 | # setup dotnet based on global.json
17 | - name: setup-dotnet
18 | uses: actions/setup-dotnet@v4
19 |
20 | # build it, test it
21 | - name: Run dotnet test - release
22 | # see issue #105
23 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
24 | shell: cmd
25 | run: ./build.cmd ci -release
26 |
27 | # upload test results
28 | - uses: actions/upload-artifact@v3
29 | if: success() || failure()
30 | with:
31 | name: test-results-release
32 | # this path glob pattern requires forward slashes!
33 | path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx
34 |
35 |
36 | test-debug:
37 | name: Test Debug Build
38 | runs-on: windows-latest
39 | steps:
40 | # checkout the code
41 | - name: checkout-code
42 | uses: actions/checkout@v4
43 | with:
44 | fetch-depth: 0
45 |
46 | # setup dotnet based on global.json
47 | - name: setup-dotnet
48 | uses: actions/setup-dotnet@v4
49 |
50 | # build it, test it
51 | - name: Run dotnet test - debug
52 | # see issue #105
53 | # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble
54 | shell: cmd
55 | run: ./build.cmd ci -debug
56 |
57 | # upload test results
58 | - uses: actions/upload-artifact@v3
59 | if: success() || failure()
60 | with:
61 | name: test-results-debug
62 | # this path glob pattern requires forward slashes!
63 | path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-debug.trx
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # Rider / JetBrains IDEs
150 | .idea/
151 |
152 | # Web workbench (sass)
153 | .sass-cache/
154 |
155 | # Installshield output folder
156 | [Ee]xpress/
157 |
158 | # DocProject is a documentation generator add-in
159 | DocProject/buildhelp/
160 | DocProject/Help/*.HxT
161 | DocProject/Help/*.HxC
162 | DocProject/Help/*.hhc
163 | DocProject/Help/*.hhk
164 | DocProject/Help/*.hhp
165 | DocProject/Help/Html2
166 | DocProject/Help/html
167 |
168 | # Click-Once directory
169 | publish/
170 |
171 | # Publish Web Output
172 | *.[Pp]ublish.xml
173 | *.azurePubxml
174 | # Note: Comment the next line if you want to checkin your web deploy settings,
175 | # but database connection strings (with potential passwords) will be unencrypted
176 | *.pubxml
177 | *.publishproj
178 |
179 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
180 | # checkin your Azure Web App publish settings, but sensitive information contained
181 | # in these scripts will be unencrypted
182 | PublishScripts/
183 |
184 | # NuGet Packages
185 | *.nupkg
186 | # NuGet Symbol Packages
187 | *.snupkg
188 | # The packages folder can be ignored because of Package Restore
189 | **/[Pp]ackages/*
190 | # except build/, which is used as an MSBuild target.
191 | !**/[Pp]ackages/build/
192 | # Uncomment if necessary however generally it will be regenerated when needed
193 | #!**/[Pp]ackages/repositories.config
194 | # NuGet v3's project.json files produces more ignorable files
195 | *.nuget.props
196 | *.nuget.targets
197 |
198 | # Microsoft Azure Build Output
199 | csx/
200 | *.build.csdef
201 |
202 | # Microsoft Azure Emulator
203 | ecf/
204 | rcf/
205 |
206 | # Windows Store app package directories and files
207 | AppPackages/
208 | BundleArtifacts/
209 | Package.StoreAssociation.xml
210 | _pkginfo.txt
211 | *.appx
212 | *.appxbundle
213 | *.appxupload
214 |
215 | # Visual Studio cache files
216 | # files ending in .cache can be ignored
217 | *.[Cc]ache
218 | # but keep track of directories ending in .cache
219 | !?*.[Cc]ache/
220 |
221 | # Others
222 | ClientBin/
223 | ~$*
224 | *~
225 | *.dbmdl
226 | *.dbproj.schemaview
227 | *.jfm
228 | *.pfx
229 | *.publishsettings
230 | orleans.codegen.cs
231 |
232 | # Including strong name files can present a security risk
233 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
234 | #*.snk
235 |
236 | # Since there are multiple workflows, uncomment next line to ignore bower_components
237 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
238 | #bower_components/
239 |
240 | # RIA/Silverlight projects
241 | Generated_Code/
242 |
243 | # Backup & report files from converting an old project file
244 | # to a newer Visual Studio version. Backup files are not needed,
245 | # because we have git ;-)
246 | _UpgradeReport_Files/
247 | Backup*/
248 | UpgradeLog*.XML
249 | UpgradeLog*.htm
250 | ServiceFabricBackup/
251 | *.rptproj.bak
252 |
253 | # SQL Server files
254 | *.mdf
255 | *.ldf
256 | *.ndf
257 |
258 | # Business Intelligence projects
259 | *.rdl.data
260 | *.bim.layout
261 | *.bim_*.settings
262 | *.rptproj.rsuser
263 | *- [Bb]ackup.rdl
264 | *- [Bb]ackup ([0-9]).rdl
265 | *- [Bb]ackup ([0-9][0-9]).rdl
266 |
267 | # Microsoft Fakes
268 | FakesAssemblies/
269 |
270 | # GhostDoc plugin setting file
271 | *.GhostDoc.xml
272 |
273 | # Node.js Tools for Visual Studio
274 | .ntvs_analysis.dat
275 | node_modules/
276 |
277 | # Visual Studio 6 build log
278 | *.plg
279 |
280 | # Visual Studio 6 workspace options file
281 | *.opt
282 |
283 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
284 | *.vbw
285 |
286 | # Visual Studio LightSwitch build output
287 | **/*.HTMLClient/GeneratedArtifacts
288 | **/*.DesktopClient/GeneratedArtifacts
289 | **/*.DesktopClient/ModelManifest.xml
290 | **/*.Server/GeneratedArtifacts
291 | **/*.Server/ModelManifest.xml
292 | _Pvt_Extensions
293 |
294 | # Paket dependency manager
295 | .paket/paket.exe
296 | paket-files/
297 |
298 | # FAKE - F# Make
299 | .fake/
300 |
301 | # CodeRush personal settings
302 | .cr/personal
303 |
304 | # Python Tools for Visual Studio (PTVS)
305 | __pycache__/
306 | *.pyc
307 |
308 | # Cake - Uncomment if you are using it
309 | # tools/**
310 | # !tools/packages.config
311 |
312 | # Tabs Studio
313 | *.tss
314 |
315 | # Telerik's JustMock configuration file
316 | *.jmconfig
317 |
318 | # BizTalk build output
319 | *.btp.cs
320 | *.btm.cs
321 | *.odx.cs
322 | *.xsd.cs
323 |
324 | # OpenCover UI analysis results
325 | OpenCover/
326 |
327 | # Azure Stream Analytics local run output
328 | ASALocalRun/
329 |
330 | # MSBuild Binary and Structured Log
331 | *.binlog
332 |
333 | # NVidia Nsight GPU debugger configuration file
334 | *.nvuser
335 |
336 | # MFractors (Xamarin productivity tool) working folder
337 | .mfractor/
338 |
339 | # Local History for Visual Studio
340 | .localhistory/
341 |
342 | # BeatPulse healthcheck temp database
343 | healthchecksdb
344 |
345 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
346 | MigrationBackup/
347 |
348 | # Ionide (cross platform F# VS Code tools) working folder
349 | .ionide/
350 | *.ncrunchproject
351 | nuget-api-key.txt
352 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Abel Braaksma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Version.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0.4.0
5 |
6 |
--------------------------------------------------------------------------------
/assets/taskseq-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/FSharp.Control.TaskSeq/d2713a19eeb1606152e26167b1bac43a2f857a7c/assets/taskseq-icon.png
--------------------------------------------------------------------------------
/backdate-tags.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Batch file to override the date and/or message of existing tag, or create a new
4 | REM tag that takes the same date/time of an existing commit.
5 | REM
6 | REM Usage:
7 | REM > backdate-tags.cmd v0.1.1 "New message"
8 | REM
9 | REM How it works:
10 | REM * checkout the commit at the moment of the tag
11 | REM * get the date/time of that commit and store in GIT_COMMITER_DATE env var
12 | REM * recreate the tag (it will now take the date of its commit)
13 | REM * push tags changes to remove (with --force)
14 | REM * return to HEAD
15 | REM
16 | REM PS:
17 | REM * these escape codes are for underlining the headers so they stand out between all GIT's output garbage
18 | REM * the back-dating trick is taken from here: https://stackoverflow.com/questions/21738647/change-date-of-git-tag-or-github-release-based-on-it
19 |
20 | ECHO.
21 | ECHO [4;97mList existing tags:[0m
22 | git tag -n
23 |
24 | ECHO.
25 | ECHO [4;97mCheckout to tag:[0m
26 | git checkout tags/%1
27 |
28 | REM Output the first string, containing the date of commit, and put it in a file
29 | REM then set the contents of that file to env var GIT_COMMITTER_DATE (which in turn is needed to enable back-dating)
30 | REM then delete the temp file
31 | ECHO.
32 | ECHO [4;97mRetrieve original commit date[0m
33 |
34 | git show --format=%%aD | findstr "^[MTWFS][a-z][a-z],.*" > _date.tmp
35 | < _date.tmp (set /p GIT_COMMITTER_DATE=)
36 | del _date.tmp
37 |
38 | ECHO Committer date for tag: %GIT_COMMITTER_DATE%
39 | ECHO Overriding tag '%1' with text: %2
40 | ECHO.
41 | REM Override (with -af) the tag, if it exists (no quotes around %2)
42 | git tag -af %1 -m %2
43 |
44 | ECHO.
45 | ECHO [4;97mUpdated tag:[0m
46 | git tag --points-at HEAD -n
47 | ECHO.
48 |
49 | REM Push to remove and override (with --force)
50 | ECHO [4;97mPush changes to remote[0m
51 | git push --tags --force
52 |
53 | REM Go back to original HEAD
54 | ECHO.
55 | ECHO [4;97mBack to original HEAD[0m
56 | git checkout -
57 |
58 | ECHO.
59 | ECHO [4;97mList of all tags[0m
60 | git tag -n
61 |
--------------------------------------------------------------------------------
/build.cmd:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Make environment variables local to the batch script
4 | SETLOCAL
5 |
6 | REM Default local parameters (BUILD_MODE must remain empty, otherwise, 'help' doesn't work)
7 | SET BUILD_CONFIG=Release
8 | SET BUILD_MODE=
9 |
10 | SET DOTNET_TEST_ARGS=
11 | SET DOTNET_TEST_PROJECT_LOCATION=
12 |
13 | SET DOTNET_CI_ARGS=--blame-hang-timeout 60000ms --logger "console;verbosity=detailed"
14 | SET DOTNET_TEST_ARGS=--logger "console;verbosity=detailed"
15 | SET DOTNET_TEST_PROJECT_LOCATION=".\src\FSharp.Control.TaskSeq.Test\FSharp.Control.TaskSeq.Test.fsproj"
16 |
17 | REM This is used to get a 'rest of arguments' list, which allows passing
18 | REM other arguments to the dotnet build and test commands
19 | SET REST_ARGS=%*
20 |
21 | :parseArgs
22 | IF "%~1"=="build" (
23 | SET BUILD_MODE=build
24 | REM Drop 'build' from the remaining args
25 | CALL :shiftArg %REST_ARGS%
26 |
27 | ) ELSE IF "%~1"=="test" (
28 | SET BUILD_MODE=test
29 | REM Drop 'test' from the remaining args
30 | CALL :shiftArg %REST_ARGS%
31 |
32 | ) ELSE IF "%~1"=="ci" (
33 | SET BUILD_MODE=ci
34 | REM Drop 'ci' from the remaining args
35 | CALL :shiftArg %REST_ARGS%
36 |
37 | ) ELSE IF "%~1"=="help" (
38 | GOTO :showHelp
39 |
40 | ) ELSE IF "%~1"=="/help" (
41 | GOTO :showHelp
42 |
43 | ) ELSE IF "%~1"=="-help" (
44 | GOTO :showHelp
45 |
46 | ) ELSE IF "%~1"=="" (
47 | REM No args, default: build
48 | SET BUILD_MODE=build
49 | SET BUILD_CONFIG=release
50 | )
51 |
52 | CALL :tryBuildConfig %REST_ARGS%
53 | ECHO Additional arguments: %REST_ARGS%
54 |
55 | REM Main branching starts here
56 | IF "%BUILD_MODE%"=="build" GOTO :runBuild
57 | IF "%BUILD_MODE%"=="test" GOTO :runTest
58 | IF "%BUILD_MODE%"=="ci" GOTO :runCi
59 |
60 |
61 | REM Something wrong, we don't recognize the given arguments
62 | REM Display help:
63 |
64 | ECHO Argument not recognized
65 |
66 | :showHelp
67 | ECHO.
68 | ECHO Available options are:
69 | ECHO.
70 | ECHO build Run 'dotnet build' (default if omitted)
71 | ECHO test Run 'dotnet test' with default configuration and no CI logging.
72 | ECHO ci Run 'dotnet test' with CI configuration and TRX logging.
73 | ECHO.
74 | ECHO Optionally combined with:
75 | ECHO.
76 | ECHO release Build release configuration (default).
77 | ECHO debug Build debug configuration.
78 | ECHO.
79 | ECHO Any arguments that follow the special arguments will be passed on to 'dotnet test' or 'dotnet build'
80 | ECHO Such user-supplied arguments can only be given when one of the above specific commands is used.
81 | ECHO
82 | ECHO Optional arguments may be given with a leading '/' or '-', if so preferred.
83 | ECHO.
84 | ECHO Examples:
85 | ECHO.
86 | ECHO Run default build (release):
87 | ECHO build
88 | ECHO.
89 | ECHO Run debug build:
90 | ECHO build debug
91 | ECHO.
92 | ECHO Run debug build with detailed verbosity:
93 | ECHO build debug --verbosity detailed
94 | ECHO.
95 | ECHO Run the tests in default CI configuration
96 | ECHO build ci
97 | ECHO.
98 | ECHO Run the tests as in CI, but with the Debug configuration
99 | ECHO build ci -debug
100 | ECHO.
101 | ECHO Run the tests without TRX logging
102 | ECHO build test -release
103 | ECHO.
104 | GOTO :EOF
105 |
106 | REM Normal building
107 | :runBuild
108 | SET BUILD_COMMAND=dotnet build src/FSharp.Control.TaskSeq.sln -c %BUILD_CONFIG% %REST_ARGS%
109 | ECHO Building for %BUILD_CONFIG% configuration...
110 | ECHO.
111 | ECHO Executing:
112 | ECHO %BUILD_COMMAND%
113 | ECHO.
114 | ECHO Restoring dotnet tools...
115 | dotnet tool restore
116 | %BUILD_COMMAND%
117 | GOTO :EOF
118 |
119 | REM Testing
120 | :runTest
121 | SET TEST_COMMAND=dotnet test -c %BUILD_CONFIG% %DOTNET_TEST_ARGS% %DOTNET_TEST_PROJECT_LOCATION% %REST_ARGS%
122 | ECHO.
123 | ECHO Testing: %BUILD_CONFIG% configuration...
124 | ECHO.
125 | ECHO Restoring dotnet tools...
126 | dotnet tool restore
127 |
128 | ECHO Executing:
129 | ECHO %TEST_COMMAND%
130 | %TEST_COMMAND%
131 | GOTO :EOF
132 |
133 | REM Continuous integration
134 | :runCi
135 | SET TRX_LOGGER=--logger "trx;LogFileName=test-results-%BUILD_CONFIG%.trx"
136 | SET CI_COMMAND=dotnet test -c %BUILD_CONFIG% %DOTNET_CI_ARGS% %DOTNET_TEST_PROJECT_LOCATION% %TRX_LOGGER% %REST_ARGS%
137 | ECHO.
138 | ECHO Continuous integration: %BUILD_CONFIG% configuration...
139 | ECHO.
140 | ECHO Restoring dotnet tools...
141 | dotnet tool restore
142 |
143 | ECHO Executing:
144 | ECHO %CI_COMMAND%
145 | %CI_COMMAND%
146 | GOTO :EOF
147 |
148 |
149 | REM Callable label, will resume after 'CALL' line
150 | :tryBuildConfig
151 | IF "%~1"=="release" (
152 | SET BUILD_CONFIG=release
153 | CALL :shiftArg %REST_ARGS%
154 | )
155 | IF "%~1"=="-release" (
156 | SET BUILD_CONFIG=release
157 | CALL :shiftArg %REST_ARGS%
158 | )
159 | IF "%~1"=="/release" (
160 | SET BUILD_CONFIG=release
161 | CALL :shiftArg %REST_ARGS%
162 | )
163 | IF "%~1"=="debug" (
164 | SET BUILD_CONFIG=debug
165 | CALL :shiftArg %REST_ARGS%
166 | )
167 | IF "%~1"=="-debug" (
168 | SET BUILD_CONFIG=debug
169 | CALL :shiftArg %REST_ARGS%
170 | )
171 | IF "%~1"=="/debug" (
172 | SET BUILD_CONFIG=debug
173 | CALL :shiftArg %REST_ARGS%
174 | )
175 | GOTO :EOF
176 |
177 | REM Callable label, will resume after 'CALL' line
178 | :shiftArg
179 | REM WARNING!!!
180 | REM If called from inside an IF-statement, it will NOT keep the resulting
181 | REM variable %REST_ARGS%, until execution gets OUTSIDE of the IF-block
182 |
183 | REM Do not call 'SHIFT' here, as we do it manually
184 | REM Here, '%*' means the arguments given in the CALL command to this label
185 | SET REST_ARGS=%*
186 |
187 | REM Shift by stripping until and including the first argument
188 | IF NOT "%REST_ARGS%"=="" CALL SET REST_ARGS=%%REST_ARGS:*%1=%%
189 | GOTO :EOF
190 |
--------------------------------------------------------------------------------
/release-notes.txt:
--------------------------------------------------------------------------------
1 |
2 | Release notes:
3 | 0.4.0
4 | - overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234
5 | - new surface area functions, fixes #208:
6 | * TaskSeq.take, skip, #209
7 | * TaskSeq.truncate, drop, #209
8 | * TaskSeq.where, whereAsync, #217
9 | * TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219
10 | * TaskSeq.max, min, maxBy, minBy, maxByAsync, minByAsync, #221
11 | * TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt, #236
12 | * TaskSeq.forall, forallAsync, #240
13 | * TaskSeq.concat (overloads: seq, array, resizearray, list), #237
14 |
15 | - Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135
16 | - Performance: several inline and allocation improvements
17 | - BINARY INCOMPATIBILITY: 'TaskSeq' module replaced by static members on 'TaskSeq<_>', fixes #184
18 | - DEPRECATIONS (warning FS0044):
19 | - type 'taskSeq<_>' is renamed to 'TaskSeq<_>', fixes #193
20 | - function 'ValueTask.ofIValueTaskSource` renamed to `ValueTask.ofSource`, fixes #193
21 | - function `ValueTask.FromResult` is renamed to `ValueTask.fromResult`, fixes #193
22 |
23 | 0.4.0-alpha.1
24 | - bugfix: not calling Dispose for 'use!', 'use', or `finally` blocks #157 (by @bartelink)
25 | - BREAKING CHANGE: null args now raise ArgumentNullException instead of NullReferenceException, #127
26 | - adds `let!` and `do!` support for F#'s Async<'T>, #79, #114
27 | - adds TaskSeq.takeWhile, takeWhileAsync, takeWhileInclusive, takeWhileInclusiveAsync, #126 (by @bartelink)
28 | - adds AsyncSeq vs TaskSeq comparison chart, #131
29 | - bugfix: removes release-notes.txt from file dependencies, but keep in the package, #138
30 |
31 | 0.3.0
32 | - improved xml doc comments, signature files for exposing types, fixes #112.
33 | - adds support for static TaskLike, allowing the same let! and do! overloads that F# task supports, fixes #110.
34 | - implements 'do!' for non-generic Task like with Task.Delay, fixes #43.
35 | - task and async CEs extended with support for 'for .. in ..do' with TaskSeq, #75, #93, #99 (in part by @theangrybyrd).
36 | - adds TaskSeq.singleton, #90 (by @gusty).
37 | - bugfix: fixes overload resolution bug with 'use' and 'use!', #97 (thanks @peterfaria).
38 | - improves TaskSeq.empty by not relying on resumable state, #89 (by @gusty).
39 | - bugfix: does not throw exceptions anymore for unequal lengths in TaskSeq.zip, fixes #32.
40 | - BACKWARD INCOMPATIBILITY: several internal-only types now hidden
41 |
42 | 0.2.2
43 | - removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead.
44 | - renames TaskSeq.toSeqCached to TaskSeq.toSeq, which was its actual operational behavior.
45 |
46 | 0.2.1
47 | - fixes an issue with ValueTask on completed iterations.
48 | - adds `TaskSeq.except` and `TaskSeq.exceptOfSeq` async set operations.
49 |
50 | 0.2
51 | - moved from NET 6.0, to NetStandard 2.1 for greater compatibility, no functional changes.
52 | - move to minimally necessary FSharp.Core version: 6.0.2.
53 | - updated readme with progress overview, corrected meta info, added release notes.
54 |
55 | 0.1.1
56 | - updated meta info in nuget package and added readme.
57 |
58 | 0.1
59 | - initial release
60 | - implements taskSeq CE using resumable state machines
61 | - with support for: yield, yield!, let, let!, while, for, try-with, try-finally, use, use!
62 | - and: tasks and valuetasks
63 | - adds toXXX / ofXXX functions
64 | - adds map/mapi/fold/iter/iteri/collect etc with async variants
65 | - adds find/pick/choose/filter etc with async variants and 'try' variants
66 | - adds cast/concat/append/prepend/delay/exactlyOne
67 | - adds empty/isEmpty
68 | - adds findIndex/indexed/init/initInfinite
69 | - adds head/last/tryHead/tryLast/tail/tryTail
70 | - adds zip/length
71 |
--------------------------------------------------------------------------------
/src/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "fantomas": {
6 | "version": "6.3.0-alpha-004",
7 | "commands": [
8 | "fantomas"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | True
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | runtime; build; native; contentfiles; analyzers; buildtransitive
33 | all
34 |
35 |
36 | runtime; build; native; contentfiles; analyzers; buildtransitive
37 | all
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.SmokeTests/SmokeTests.fs:
--------------------------------------------------------------------------------
1 | module Tests
2 |
3 | open System
4 | open System.Threading.Tasks
5 | open Xunit
6 | open FSharp.Control
7 | open FsUnit.Xunit
8 |
9 | //
10 | // this file can be used to hand-test NuGet deploys
11 | // esp. when there are changes in the surface area
12 | //
13 | // This project gets compiled in CI, but is not part
14 | // of the structured test reports currently.
15 | // However, a compile error will fail the CI pipeline.
16 | //
17 |
18 |
19 | type private MultiDispose(disposed: int ref) =
20 | member _.Get1() = 1
21 |
22 | interface IDisposable with
23 | member _.Dispose() = disposed.Value <- 1
24 |
25 | interface IAsyncDisposable with
26 | member _.DisposeAsync() = ValueTask(task { do disposed.Value <- -1 })
27 |
28 | []
29 | let ``Use and execute a minimal taskSeq from nuget`` () =
30 | taskSeq { yield 10 }
31 | |> TaskSeq.toArray
32 | |> fun x -> Assert.Equal(x, [| 10 |])
33 |
34 | []
35 | let ``Use taskSeq from nuget with multiple keywords v0.2.2`` () =
36 | taskSeq {
37 | do! task { do! Task.Delay 10 }
38 | let! x = task { return 1 }
39 | yield x
40 | let! vt = ValueTask(task { return 2 })
41 | yield vt
42 | yield 10
43 | }
44 | |> TaskSeq.toArray
45 | |> fun x -> Assert.Equal(x, [| 1; 2; 10 |])
46 |
47 | // from version 0.3.0:
48 |
49 | []
50 | let ``Use taskSeq from nuget with multiple keywords v0.3.0`` () =
51 | taskSeq {
52 | do! task { do! Task.Delay 10 }
53 | do! Task.Delay 10 // only in 0.3
54 | let! x = task { return 1 } :> Task // only in 0.3
55 | yield 1
56 | let! vt = ValueTask(task { return 2 })
57 | yield vt
58 | let! vt = ValueTask(task { return 2 }) // only in 0.3
59 | do! ValueTask(task { return 2 }) // only in 0.3
60 | yield 3
61 | yield 10
62 | }
63 | |> TaskSeq.toArray
64 | |> fun x -> Assert.Equal(x, [| 1; 2; 3; 10 |])
65 |
66 | []
67 | let ``Use taskSeq when type implements IDisposable and IAsyncDisposable`` () =
68 | let disposed = ref 0
69 |
70 | let ts = taskSeq {
71 | use! x = task { return new MultiDispose(disposed) } // Used to fail to compile (see #97, fixed in v0.3.0)
72 | yield x.Get1()
73 | }
74 |
75 | ts
76 | |> TaskSeq.length
77 | |> Task.map (should equal 1)
78 | |> Task.map (fun _ -> disposed.Value |> should equal -1) // must favor IAsyncDisposable, not IDisposable
79 |
80 | []
81 | let ``Use taskSeq as part of an F# task CE`` () = task {
82 | let ts = taskSeq { yield! [ 0..99 ] }
83 | let ra = ResizeArray()
84 |
85 | // loop through a taskSeq, support added in v0.3.0
86 | for v in ts do
87 | ra.Add v
88 |
89 | ra.ToArray() |> should equal [| 0..99 |]
90 | }
91 |
92 | []
93 | let ``New surface area functions availability tests v0.3.0`` () = task {
94 | let ts = TaskSeq.singleton 10 // added in v0.3.0
95 | let! ls = TaskSeq.toListAsync ts
96 | List.exactlyOne ls |> should equal 10
97 | }
98 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.SmokeTests/TaskSeq.PocTests.fs:
--------------------------------------------------------------------------------
1 | namespace TaskSeq.Tests
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 | open FsToolkit.ErrorHandling
6 |
7 | open FSharp.Control
8 |
9 | /////////////////////////////////////////////////////////////////////////////
10 | /// ///
11 | /// This file contains bunch of tests that exemplify how hard it can be ///
12 | /// to use IAsyncEnumarable "by hand", and how mistakes can be made ///
13 | /// that can lead to occasional failings ///
14 | /// ///
15 | /////////////////////////////////////////////////////////////////////////////
16 |
17 |
18 | module ``PoC's for seq of tasks`` =
19 |
20 | []
21 | let ``Good: Show joining tasks with continuation is good`` () = task {
22 | // acts like a fold
23 | let! results = Gen.createAndJoinMultipleTasks 10 Gen.joinWithContinuation
24 | results |> should equal 10
25 | }
26 |
27 | []
28 | let ``Good: Show that joining tasks with 'bind' in task CE is good`` () = task {
29 | let! tasks = Gen.createAndJoinMultipleTasks 10 Gen.joinIdentityDelayed
30 |
31 | let tasks = tasks |> Array.ofList
32 | let len = Array.length tasks
33 | let results = Array.zeroCreate len
34 |
35 | for i in 0 .. len - 1 do
36 | // this uses Task.bind under the hood, which ensures order-of-execution and wait-for-previous
37 | let! item = tasks[i]() // only now are we delay-executing the task in the array
38 | results[i] <- item
39 |
40 | results |> should equal <| Array.init len ((+) 1)
41 | }
42 |
43 | []
44 | let ``Good: Show that joining tasks with 'taskSeq' is good`` () = task {
45 | let! tasks = Gen.createAndJoinMultipleTasks 10 Gen.joinIdentityDelayed
46 |
47 | let asAsyncSeq = taskSeq {
48 | for task in tasks do
49 | // cannot use `yield!` here, as `taskSeq` expects it to return a seq
50 | let! x = task ()
51 | yield x
52 | }
53 |
54 | let! results = asAsyncSeq |> TaskSeq.toArrayAsync
55 |
56 | results |> should equal
57 | <| Array.init (Array.length results) ((+) 1)
58 | }
59 |
60 | []
61 | let ``Bad: Show that joining tasks with 'traverseTaskResult' can be bad`` () = task {
62 | let! taskList = Gen.createAndJoinMultipleTasks 10 Gen.joinIdentityHotStarted
63 |
64 | // since tasks are hot-started, by this time they are already *all* running
65 | let! results =
66 | taskList
67 | |> List.map (Task.map Result.Ok)
68 | |> List.traverseTaskResultA id
69 |
70 | match results with
71 | | Ok results ->
72 | // BAD!! As you can see, results are unequal to expected output
73 | results |> should not'
74 | <| equal (List.init (List.length results) ((+) 1))
75 | | Error err -> failwith $"Impossible: {err}"
76 | }
77 |
78 | []
79 | let ``Bad: Show that joining tasks as a list of tasks can be bad`` () = task {
80 | let! taskList = Gen.createAndJoinMultipleTasks 10 Gen.joinIdentityHotStarted
81 |
82 | // since tasks are hot-started, by this time they are already *all* running
83 | let tasks = taskList |> Array.ofList
84 | let results = Array.zeroCreate 10
85 |
86 | for i in 0..9 do
87 | let! item = tasks[i]
88 | results[i] <- item
89 |
90 | // BAD!! As you can see, results are unequal to expected output
91 | results |> should not'
92 | <| equal (Array.init (Array.length results) ((+) 1))
93 | }
94 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.SmokeTests/TestUtils.fs:
--------------------------------------------------------------------------------
1 | namespace TaskSeq.Tests
2 |
3 | open System
4 | open System.Threading
5 | open System.Threading.Tasks
6 | open System.Diagnostics
7 | open System.Collections.Generic
8 |
9 | open Xunit
10 | open Xunit.Abstractions
11 | open FsUnit.Xunit
12 |
13 | open FSharp.Control
14 |
15 | /// Milliseconds
16 | []
17 | type ms
18 |
19 | /// Microseconds
20 | []
21 | type µs
22 |
23 | /// Helpers for short waits, as Task.Delay has about 15ms precision.
24 | /// Inspired by IoT code: https://github.com/dotnet/iot/pull/235/files
25 | module DelayHelper =
26 |
27 | let private rnd = Random()
28 |
29 | ///
30 | /// Delay for at least the specified .
31 | ///
32 | /// The number of microseconds to delay.
33 | ///
34 | /// True to allow yielding the thread. If this is set to false, on single-proc systems
35 | /// this will prevent all other code from running.
36 | ///
37 | let spinWaitDelay (microseconds: int64<µs>) (allowThreadYield: bool) =
38 | let start = Stopwatch.GetTimestamp()
39 | let minimumTicks = int64 microseconds * Stopwatch.Frequency / 1_000_000L
40 |
41 | // FIXME: though this is part of official IoT code, the `allowThreadYield` version is extremely slow
42 | // slower than would be expected from a simple SpinOnce. Though this may be caused by scenarios with
43 | // many tasks at once. Have to investigate. See perf smoke tests.
44 | if allowThreadYield then
45 | let spinWait = SpinWait()
46 |
47 | while Stopwatch.GetTimestamp() - start < minimumTicks do
48 | spinWait.SpinOnce(1)
49 |
50 | else
51 | while Stopwatch.GetTimestamp() - start < minimumTicks do
52 | Thread.SpinWait(1)
53 |
54 | let delayTask (µsecMin: int64<µs>) (µsecMax: int64<µs>) f = task {
55 | let rnd () = rnd.NextInt64(int64 µsecMin, int64 µsecMax) * 1L<µs>
56 |
57 | // ensure unequal running lengths and points-in-time for assigning the variable
58 | // DO NOT use Thead.Sleep(), it's blocking!
59 | // WARNING: Task.Delay only has a 15ms timer resolution!!!
60 |
61 | // TODO: check this! The following comment may not be correct
62 | // this creates a resume state, which seems more efficient than SpinWait.SpinOnce, see DelayHelper.
63 | let! _ = Task.Delay 0
64 | let delay = rnd ()
65 |
66 | // typical minimum accuracy of Task.Delay is 15.6ms
67 | // for delay-cases shorter than that, we use SpinWait
68 | if delay < 15_000L<µs> then
69 | do spinWaitDelay (rnd ()) false
70 | else
71 | do! Task.Delay(int <| float delay / 1_000.0)
72 |
73 | return f ()
74 | }
75 |
76 | ///
77 | /// Creates dummy backgroundTasks with a randomized delay and a mutable state,
78 | /// to ensure we properly test whether processing is done ordered or not.
79 | /// Default for and
80 | /// are 10,000µs and 30,000µs respectively (or 10ms and 30ms).
81 | ///
82 | type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) =
83 | let mutable x = 0
84 |
85 | ///
86 | /// Creates dummy tasks with a randomized delay and a mutable state,
87 | /// to ensure we properly test whether processing is done ordered or not.
88 | /// Uses the defaults for and
89 | /// with 10,000µs and 30,000µs respectively (or 10ms and 30ms).
90 | ///
91 | new() = new DummyTaskFactory(10_000L<µs>, 30_000L<µs>)
92 |
93 |
94 | /// Bunch of delayed tasks that randomly have a yielding delay of 10-30ms, therefore having overlapping execution times.
95 | member _.CreateDelayedTasks_SideEffect total = [
96 | for i in 0 .. total - 1 do
97 | fun () -> DelayHelper.delayTask µsecMin µsecMax (fun _ -> Interlocked.Increment &x)
98 | ]
99 |
100 | /// Just some dummy task generators, copied over from the base test project, with artificial delays,
101 | /// mostly to ensure sequential async operation of side effects.
102 | module Gen =
103 | /// Joins two tasks using merely BCL methods. This approach is what you can use to
104 | /// properly, sequentially execute a chain of tasks in a non-blocking, non-overlapping way.
105 | let joinWithContinuation tasks =
106 | let simple (t: unit -> Task<_>) (source: unit -> Task<_>) : unit -> Task<_> =
107 | fun () ->
108 | source()
109 | .ContinueWith((fun (_: Task) -> t ()), TaskContinuationOptions.OnlyOnRanToCompletion)
110 | .Unwrap()
111 | :?> Task<_>
112 |
113 | let rec combine acc (tasks: (unit -> Task<_>) list) =
114 | match tasks with
115 | | [] -> acc
116 | | t :: tail -> combine (simple t acc) tail
117 |
118 | match tasks with
119 | | first :: rest -> combine first rest
120 | | [] -> failwith "oh oh, no tasks given!"
121 |
122 | let joinIdentityHotStarted tasks () = task { return tasks |> List.map (fun t -> t ()) }
123 |
124 | let joinIdentityDelayed tasks () = task { return tasks }
125 |
126 | let createAndJoinMultipleTasks total joiner : Task<_> =
127 | // the actual creation of tasks
128 | let tasks = DummyTaskFactory().CreateDelayedTasks_SideEffect total
129 | let combinedTask = joiner tasks
130 | // start the combined tasks
131 | combinedTask ()
132 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/AssemblyInfo.fs:
--------------------------------------------------------------------------------
1 | namespace TaskSeq.Tests
2 |
3 | open System.Runtime.CompilerServices
4 |
5 | // this prevents an XUnit bug to break over itself on CI
6 | // tests themselves can be run in parallel just fine.
7 | []
8 |
9 | do ()
10 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | True
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
68 |
69 |
70 |
71 |
72 |
73 | runtime; build; native; contentfiles; analyzers; buildtransitive
74 | all
75 |
76 |
77 | runtime; build; native; contentfiles; analyzers; buildtransitive
78 | all
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Append
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.append
10 | // TaskSeq.appendSeq
11 | // TaskSeq.prependSeq
12 | //
13 |
14 | let validateSequence ts =
15 | ts
16 | |> TaskSeq.toListAsync
17 | |> Task.map (List.map string)
18 | |> Task.map (String.concat "")
19 | |> Task.map (should equal "1234567891012345678910")
20 |
21 |
22 | module EmptySeq =
23 | []
24 | let ``Null source is invalid`` () =
25 | assertNullArg
26 | <| fun () -> TaskSeq.empty |> TaskSeq.append null
27 |
28 | assertNullArg
29 | <| fun () -> null |> TaskSeq.append TaskSeq.empty
30 |
31 | assertNullArg <| fun () -> null |> TaskSeq.append null
32 |
33 | [)>]
34 | let ``TaskSeq-append both args empty`` variant =
35 | Gen.getEmptyVariant variant
36 | |> TaskSeq.append (Gen.getEmptyVariant variant)
37 | |> verifyEmpty
38 |
39 | [)>]
40 | let ``TaskSeq-appendSeq both args empty`` variant =
41 | Seq.empty
42 | |> TaskSeq.appendSeq (Gen.getEmptyVariant variant)
43 | |> verifyEmpty
44 |
45 | [)>]
46 | let ``TaskSeq-prependSeq both args empty`` variant =
47 | Gen.getEmptyVariant variant
48 | |> TaskSeq.prependSeq Seq.empty
49 | |> verifyEmpty
50 |
51 | module Immutable =
52 | [)>]
53 | let ``TaskSeq-append`` variant =
54 | Gen.getSeqImmutable variant
55 | |> TaskSeq.append (Gen.getSeqImmutable variant)
56 | |> validateSequence
57 |
58 | [)>]
59 | let ``TaskSeq-appendSeq with a list`` variant =
60 | [ 1..10 ]
61 | |> TaskSeq.appendSeq (Gen.getSeqImmutable variant)
62 | |> validateSequence
63 |
64 | [)>]
65 | let ``TaskSeq-appendSeq with an array`` variant =
66 | [| 1..10 |]
67 | |> TaskSeq.appendSeq (Gen.getSeqImmutable variant)
68 | |> validateSequence
69 |
70 | [)>]
71 | let ``TaskSeq-prependSeq with a list`` variant =
72 | Gen.getSeqImmutable variant
73 | |> TaskSeq.prependSeq [ 1..10 ]
74 | |> validateSequence
75 |
76 | [)>]
77 | let ``TaskSeq-prependSeq with an array`` variant =
78 | Gen.getSeqImmutable variant
79 | |> TaskSeq.prependSeq [| 1..10 |]
80 | |> validateSequence
81 |
82 | module SideEffects =
83 | [)>]
84 | let ``TaskSeq-append consumes whole sequence once incl after-effects`` variant =
85 | let mutable i = 0
86 |
87 | taskSeq {
88 | i <- i + 1
89 | yield! [ 1..10 ]
90 | i <- i + 1
91 | }
92 | |> TaskSeq.append (Gen.getSeqImmutable variant)
93 | |> validateSequence
94 | |> Task.map (fun () -> i |> should equal 2)
95 |
96 | []
97 | let ``TaskSeq-appendSeq consumes whole sequence once incl after-effects`` () =
98 | let mutable i = 0
99 |
100 | let ts = taskSeq {
101 | i <- i + 1
102 | yield! [ 1..10 ]
103 | i <- i + 1
104 | }
105 |
106 | [| 1..10 |]
107 | |> TaskSeq.appendSeq ts
108 | |> validateSequence
109 | |> Task.map (fun () -> i |> should equal 2)
110 |
111 | []
112 | let ``TaskSeq-prependSeq consumes whole sequence once incl after-effects`` () =
113 | let mutable i = 0
114 |
115 | taskSeq {
116 | i <- i + 1
117 | yield! [ 1..10 ]
118 | i <- i + 1
119 | }
120 | |> TaskSeq.prependSeq [ 1..10 ]
121 | |> validateSequence
122 | |> Task.map (fun () -> i |> should equal 2)
123 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.AsyncExtensions
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // Async extensions
10 | //
11 |
12 | module EmptySeq =
13 | [)>]
14 | let ``Async-for CE with empty taskSeq`` variant = async {
15 | let values = Gen.getEmptyVariant variant
16 |
17 | let mutable sum = 42
18 |
19 | for x in values do
20 | sum <- sum + x
21 |
22 | sum |> should equal 42
23 | }
24 |
25 | []
26 | let ``Async-for CE must execute side effect in empty taskSeq`` () = async {
27 | let mutable data = 0
28 | let values = taskSeq { do data <- 42 }
29 |
30 | for _ in values do
31 | ()
32 |
33 | data |> should equal 42
34 | }
35 |
36 |
37 | module Immutable =
38 | [)>]
39 | let ``Async-for CE with taskSeq`` variant = async {
40 | let values = Gen.getSeqImmutable variant
41 |
42 | let mutable sum = 0
43 |
44 | for x in values do
45 | sum <- sum + x
46 |
47 | sum |> should equal 55
48 | }
49 |
50 | [)>]
51 | let ``Async-for CE with taskSeq multiple iterations`` variant = async {
52 | let values = Gen.getSeqImmutable variant
53 |
54 | let mutable sum = 0
55 |
56 | for x in values do
57 | sum <- sum + x
58 |
59 | // each following iteration should start at the beginning
60 | for x in values do
61 | sum <- sum + x
62 |
63 | for x in values do
64 | sum <- sum + x
65 |
66 | sum |> should equal 165
67 | }
68 |
69 | []
70 | let ``Async-for mixing both types of for loops`` () = async {
71 | // this test ensures overload resolution is correct
72 | let ts = TaskSeq.singleton 20
73 | let sq = Seq.singleton 20
74 | let mutable sum = 2
75 |
76 | for x in ts do
77 | sum <- sum + x
78 |
79 | for x in sq do
80 | sum <- sum + x
81 |
82 | sum |> should equal 42
83 | }
84 |
85 | module SideEffects =
86 | [)>]
87 | let ``Async-for CE with taskSeq`` variant = async {
88 | let values = Gen.getSeqWithSideEffect variant
89 |
90 | let mutable sum = 0
91 |
92 | for x in values do
93 | sum <- sum + x
94 |
95 | sum |> should equal 55
96 | }
97 |
98 | [)>]
99 | let ``Async-for CE with taskSeq multiple iterations`` variant = async {
100 | let values = Gen.getSeqWithSideEffect variant
101 |
102 | let mutable sum = 0
103 |
104 | for x in values do
105 | sum <- sum + x
106 |
107 | // each following iteration should start at the beginning
108 | // with the "side effect" tests, the mutable state updates
109 | for x in values do
110 | sum <- sum + x // starts at 11
111 |
112 | for x in values do
113 | sum <- sum + x // starts at 21
114 |
115 | sum |> should equal 465 // eq to: List.sum [1..30]
116 | }
117 |
118 | module Other =
119 | []
120 | let ``Async-for CE must call dispose in empty taskSeq`` () = async {
121 | let disposed = ref 0
122 | let values = Gen.getEmptyDisposableTaskSeq disposed
123 |
124 | for _ in values do
125 | ()
126 |
127 | // the DisposeAsync should be called by now
128 | disposed.Value |> should equal 1
129 | }
130 |
131 | []
132 | let ``Async-for CE must call dispose on singleton`` () = async {
133 | let disposed = ref 0
134 | let mutable sum = 0
135 | let values = Gen.getSingletonDisposableTaskSeq disposed
136 |
137 | for x in values do
138 | sum <- x
139 |
140 | // the DisposeAsync should be called by now
141 | disposed.Value |> should equal 1
142 | sum |> should equal 42
143 | }
144 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Cast.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Cast
2 |
3 | open System
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.box
12 | // TaskSeq.unbox
13 | // TaskSeq.cast
14 | //
15 |
16 | /// Asserts that a sequence contains the char values 'A'..'J'.
17 | let validateSequence ts =
18 | ts
19 | |> TaskSeq.toListAsync
20 | |> Task.map (List.map string)
21 | |> Task.map (String.concat "")
22 | |> Task.map (should equal "12345678910")
23 |
24 | module EmptySeq =
25 | []
26 | let ``Null source is invalid`` () =
27 | assertNullArg <| fun () -> TaskSeq.box null
28 | assertNullArg <| fun () -> TaskSeq.unbox null
29 | assertNullArg <| fun () -> TaskSeq.cast null
30 |
31 | [)>]
32 | let ``TaskSeq-box empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.box |> verifyEmpty
33 |
34 | [)>]
35 | let ``TaskSeq-unbox empty`` variant =
36 | Gen.getEmptyVariant variant
37 | |> TaskSeq.box
38 | |> TaskSeq.unbox
39 | |> verifyEmpty
40 |
41 | [)>]
42 | let ``TaskSeq-cast empty`` variant =
43 | Gen.getEmptyVariant variant
44 | |> TaskSeq.box
45 | |> TaskSeq.cast
46 | |> verifyEmpty
47 |
48 | [)>]
49 | let ``TaskSeq-unbox empty to invalid type should not fail`` variant =
50 | Gen.getEmptyVariant variant
51 | |> TaskSeq.box
52 | |> TaskSeq.unbox // cannot cast to int, but for empty sequences, the exception won't be thrown
53 | |> verifyEmpty
54 |
55 | [)>]
56 | let ``TaskSeq-cast empty to invalid type should not fail`` variant =
57 | Gen.getEmptyVariant variant
58 | |> TaskSeq.box
59 | |> TaskSeq.cast // cannot cast to int, but for empty sequences, the exception won't be thrown
60 | |> verifyEmpty
61 |
62 | module Immutable =
63 | [)>]
64 | let ``TaskSeq-box`` variant =
65 | Gen.getSeqImmutable variant
66 | |> TaskSeq.box
67 | |> validateSequence
68 |
69 | [)>]
70 | let ``TaskSeq-unbox`` variant =
71 | Gen.getSeqImmutable variant
72 | |> TaskSeq.box
73 | |> TaskSeq.unbox
74 | |> validateSequence
75 |
76 | [)>]
77 | let ``TaskSeq-cast`` variant =
78 | Gen.getSeqImmutable variant
79 | |> TaskSeq.box
80 | |> TaskSeq.cast
81 | |> validateSequence
82 |
83 | [)>]
84 | let ``TaskSeq-unbox invalid type should throw`` variant =
85 | fun () ->
86 | Gen.getSeqImmutable variant
87 | |> TaskSeq.box
88 | |> TaskSeq.unbox // cannot unbox from int to uint, even though types have the same size
89 | |> TaskSeq.toArrayAsync
90 | |> Task.ignore
91 |
92 | |> should throwAsyncExact typeof
93 |
94 | [)>]
95 | let ``TaskSeq-cast invalid type should throw`` variant =
96 | fun () ->
97 | Gen.getSeqImmutable variant
98 | |> TaskSeq.box
99 | |> TaskSeq.cast
100 | |> TaskSeq.toArrayAsync
101 | |> Task.ignore
102 |
103 | |> should throwAsyncExact typeof
104 |
105 | [)>]
106 | let ``TaskSeq-unbox invalid type should NOT throw before sequence is iterated`` variant =
107 | fun () ->
108 | Gen.getSeqImmutable variant
109 | |> TaskSeq.box
110 | |> TaskSeq.unbox // no iteration done
111 | |> ignore
112 |
113 | |> should not' (throw typeof)
114 |
115 | [)>]
116 | let ``TaskSeq-cast invalid type should NOT throw before sequence is iterated`` variant =
117 | fun () ->
118 | Gen.getSeqImmutable variant
119 | |> TaskSeq.box
120 | |> TaskSeq.cast // no iteration done
121 | |> ignore
122 |
123 | |> should not' (throw typeof)
124 |
125 | module SideEffects =
126 | []
127 | let ``TaskSeq-box prove that it has no effect until executed`` () =
128 | let mutable i = 0
129 |
130 | let ts = taskSeq {
131 | i <- i + 1 // we should not get here
132 | i <- i + 1
133 | yield 42
134 | i <- i + 1
135 | }
136 |
137 | // point of this test: just calling 'box' won't execute anything of the sequence!
138 | let boxed = ts |> TaskSeq.box |> TaskSeq.box |> TaskSeq.box
139 |
140 | // no side effect until iterated
141 | i |> should equal 0
142 |
143 | boxed
144 | |> TaskSeq.last
145 | |> Task.map (should equal 42)
146 | |> Task.map (fun () -> i = 9)
147 |
148 | []
149 | let ``TaskSeq-unbox prove that it has no effect until executed`` () =
150 | let mutable i = 0
151 |
152 | let ts = taskSeq {
153 | i <- i + 1 // we should not get here
154 | i <- i + 1
155 | yield box 42
156 | i <- i + 1
157 | }
158 |
159 | // point of this test: just calling 'unbox' won't execute anything of the sequence!
160 | let unboxed = ts |> TaskSeq.unbox
161 |
162 | // no side effect until iterated
163 | i |> should equal 0
164 |
165 | unboxed
166 | |> TaskSeq.last
167 | |> Task.map (should equal 42)
168 | |> Task.map (fun () -> i = 3)
169 |
170 | []
171 | let ``TaskSeq-cast prove that it has no effect until executed`` () =
172 | let mutable i = 0
173 |
174 | let ts = taskSeq {
175 | i <- i + 1 // we should not get here
176 | i <- i + 1
177 | yield box 42
178 | i <- i + 1
179 | }
180 |
181 | // point of this test: just calling 'cast' won't execute anything of the sequence!
182 | let cast = ts |> TaskSeq.cast
183 | i |> should equal 0 // no side effect until iterated
184 |
185 | cast
186 | |> TaskSeq.last
187 | |> Task.map (should equal 42)
188 | |> Task.map (fun () -> i = 3)
189 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Choose.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Choose
2 |
3 | open System
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.choose
12 | // TaskSeq.chooseAsync
13 | //
14 |
15 | module EmptySeq =
16 | []
17 | let ``Null source is invalid`` () =
18 | assertNullArg
19 | <| fun () -> TaskSeq.choose (fun _ -> None) null
20 |
21 | assertNullArg
22 | <| fun () -> TaskSeq.chooseAsync (fun _ -> Task.fromResult None) null
23 |
24 | [)>]
25 | let ``TaskSeq-choose`` variant = task {
26 | let! empty =
27 | Gen.getEmptyVariant variant
28 | |> TaskSeq.choose (fun _ -> Some 42)
29 | |> TaskSeq.toListAsync
30 |
31 | List.isEmpty empty |> should be True
32 | }
33 |
34 | [)>]
35 | let ``TaskSeq-chooseAsync`` variant = task {
36 | let! empty =
37 | Gen.getEmptyVariant variant
38 | |> TaskSeq.chooseAsync (fun _ -> task { return Some 42 })
39 | |> TaskSeq.toListAsync
40 |
41 | List.isEmpty empty |> should be True
42 | }
43 |
44 | module Immutable =
45 | [)>]
46 | let ``TaskSeq-choose can convert and filter`` variant = task {
47 | let chooser number = if number <= 5 then Some(char number + '@') else None
48 | let ts = Gen.getSeqImmutable variant
49 |
50 | let! letters1 = TaskSeq.choose chooser ts |> TaskSeq.toArrayAsync
51 | let! letters2 = TaskSeq.choose chooser ts |> TaskSeq.toArrayAsync
52 |
53 | String letters1 |> should equal "ABCDE"
54 | String letters2 |> should equal "ABCDE"
55 | }
56 |
57 | [)>]
58 | let ``TaskSeq-chooseAsync can convert and filter`` variant = task {
59 | let chooser number = task { return if number <= 5 then Some(char number + '@') else None }
60 | let ts = Gen.getSeqImmutable variant
61 |
62 | let! letters1 = TaskSeq.chooseAsync chooser ts |> TaskSeq.toArrayAsync
63 | let! letters2 = TaskSeq.chooseAsync chooser ts |> TaskSeq.toArrayAsync
64 |
65 | String letters1 |> should equal "ABCDE"
66 | String letters2 |> should equal "ABCDE"
67 | }
68 |
69 | module SideEffects =
70 | [)>]
71 | let ``TaskSeq-choose applied multiple times`` variant = task {
72 | let ts = Gen.getSeqWithSideEffect variant
73 | let chooser x number = if number <= x then Some(char number + '@') else None
74 |
75 | let! lettersA = ts |> TaskSeq.choose (chooser 5) |> TaskSeq.toArrayAsync
76 | let! lettersK = ts |> TaskSeq.choose (chooser 15) |> TaskSeq.toArrayAsync
77 | let! lettersU = ts |> TaskSeq.choose (chooser 25) |> TaskSeq.toArrayAsync
78 |
79 | String lettersA |> should equal "ABCDE"
80 | String lettersK |> should equal "KLMNO"
81 | String lettersU |> should equal "UVWXY"
82 | }
83 |
84 | [)>]
85 | let ``TaskSeq-chooseAsync applied multiple times`` variant = task {
86 | let ts = Gen.getSeqWithSideEffect variant
87 | let chooser x number = task { return if number <= x then Some(char number + '@') else None }
88 |
89 | let! lettersA = TaskSeq.chooseAsync (chooser 5) ts |> TaskSeq.toArrayAsync
90 | let! lettersK = TaskSeq.chooseAsync (chooser 15) ts |> TaskSeq.toArrayAsync
91 | let! lettersU = TaskSeq.chooseAsync (chooser 25) ts |> TaskSeq.toArrayAsync
92 |
93 | String lettersA |> should equal "ABCDE"
94 | String lettersK |> should equal "KLMNO"
95 | String lettersU |> should equal "UVWXY"
96 | }
97 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Contains.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Contains
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.contains
10 | //
11 |
12 | module EmptySeq =
13 | []
14 | let ``Null source is invalid`` () = assertNullArg <| fun () -> TaskSeq.contains 42 null
15 |
16 | [)>]
17 | let ``TaskSeq-contains returns false`` variant =
18 | Gen.getEmptyVariant variant
19 | |> TaskSeq.contains 12
20 | |> Task.map (should be False)
21 |
22 | module Immutable =
23 | [)>]
24 | let ``TaskSeq-contains sad path returns false`` variant =
25 | Gen.getSeqImmutable variant
26 | |> TaskSeq.contains 0
27 | |> Task.map (should be False)
28 |
29 | [)>]
30 | let ``TaskSeq-contains happy path middle of seq`` variant =
31 | Gen.getSeqImmutable variant
32 | |> TaskSeq.contains 5
33 | |> Task.map (should be True)
34 |
35 | [)>]
36 | let ``TaskSeq-contains happy path first item of seq`` variant =
37 | Gen.getSeqImmutable variant
38 | |> TaskSeq.contains 1
39 | |> Task.map (should be True)
40 |
41 | [)>]
42 | let ``TaskSeq-contains happy path last item of seq`` variant =
43 | Gen.getSeqImmutable variant
44 | |> TaskSeq.contains 10
45 | |> Task.map (should be True)
46 |
47 | module SideEffects =
48 | [)>]
49 | let ``TaskSeq-contains KeyNotFoundException only sometimes for mutated state`` variant = task {
50 | let ts = Gen.getSeqWithSideEffect variant
51 |
52 | // first: false
53 | let! found = TaskSeq.contains 11 ts
54 | found |> should be False
55 |
56 | // find again: found now, because of side effects
57 | let! found = TaskSeq.contains 11 ts
58 | found |> should be True
59 |
60 | // find once more: false
61 | let! found = TaskSeq.contains 11 ts
62 | found |> should be False
63 | }
64 |
65 | []
66 | let ``TaskSeq-contains _specialcase_ prove we don't read past the found item`` () = task {
67 | let mutable i = 0
68 |
69 | let ts = taskSeq {
70 | for _ in 0..9 do
71 | i <- i + 1
72 | yield i
73 | }
74 |
75 | let! found = ts |> TaskSeq.contains 3
76 | found |> should be True
77 | i |> should equal 3 // only partial evaluation!
78 |
79 | // find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
80 | let! found = ts |> TaskSeq.contains 4
81 | found |> should be True
82 | i |> should equal 4 // only partial evaluation!
83 | }
84 |
85 | []
86 | let ``TaskSeq-contains _specialcase_ prove we don't read past the found item v2`` () = task {
87 | let mutable i = 0
88 |
89 | let ts = taskSeq {
90 | yield 42
91 | i <- i + 1
92 | i <- i + 1
93 | }
94 |
95 | let! found = ts |> TaskSeq.contains 42
96 | found |> should be True
97 | i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
98 | }
99 |
100 | []
101 | let ``TaskSeq-contains _specialcase_ prove statement after yield is not evaluated`` () = task {
102 | let mutable i = 0
103 |
104 | let ts = taskSeq {
105 | for _ in 0..9 do
106 | yield i
107 | i <- i + 1
108 | }
109 |
110 | let! found = ts |> TaskSeq.contains 0
111 | found |> should be True
112 | i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
113 |
114 | // find some next item. We do get a new iterator, but mutable state is now starting at '1'
115 | let! found = ts |> TaskSeq.contains 4
116 | found |> should be True
117 | i |> should equal 4 // only partial evaluation!
118 | }
119 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Delay.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Delay
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.delay
10 | //
11 |
12 | let validateSequence ts =
13 | ts
14 | |> TaskSeq.toListAsync
15 | |> Task.map (List.map string)
16 | |> Task.map (String.concat "")
17 | |> Task.map (should equal "12345678910")
18 |
19 | module EmptySeq =
20 | [)>]
21 | let ``TaskSeq-delay with empty sequences`` variant =
22 | fun () -> Gen.getEmptyVariant variant
23 | |> TaskSeq.delay
24 | |> verifyEmpty
25 |
26 | module Immutable =
27 | [)>]
28 | let ``TaskSeq-delay`` variant =
29 | fun () -> Gen.getSeqImmutable variant
30 | |> TaskSeq.delay
31 | |> validateSequence
32 |
33 | module SideEffect =
34 | []
35 | let ``TaskSeq-delay executes side effects`` () = task {
36 | let mutable i = 0
37 |
38 | let ts =
39 | fun () -> taskSeq {
40 | yield! [ 1..10 ]
41 | i <- i + 1
42 | }
43 | |> TaskSeq.delay
44 |
45 | do! ts |> validateSequence
46 | i |> should equal 1
47 | let! len = TaskSeq.length ts
48 | i |> should equal 2 // re-eval of the sequence executes side effect again
49 | len |> should equal 10
50 | }
51 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Do
2 |
3 | open System.Threading.Tasks
4 |
5 | open FsUnit
6 | open Xunit
7 |
8 | open FSharp.Control
9 |
10 | []
11 | let ``CE taskSeq: use 'do'`` () =
12 | let mutable value = 0
13 |
14 | taskSeq { do value <- value + 1 } |> verifyEmpty
15 |
16 | []
17 | let ``CE taskSeq: use 'do!' with a task`` () =
18 | let mutable value = 0
19 |
20 | taskSeq { do! task { do value <- value + 1 } }
21 | |> verifyEmpty
22 | |> Task.map (fun _ -> value |> should equal 1)
23 |
24 | []
25 | let ``CE taskSeq: use 'do!' with a ValueTask`` () =
26 | let mutable value = 0
27 |
28 | taskSeq { do! ValueTask.ofTask (task { do value <- value + 1 }) }
29 | |> verifyEmpty
30 | |> Task.map (fun _ -> value |> should equal 1)
31 |
32 | []
33 | let ``CE taskSeq: use 'do!' with a non-generic ValueTask`` () =
34 | let mutable value = 0
35 |
36 | taskSeq { do! ValueTask(task { do value <- value + 1 }) }
37 | |> verifyEmpty
38 | |> Task.map (fun _ -> value |> should equal 1)
39 |
40 | []
41 | let ``CE taskSeq: use 'do!' with a non-generic task`` () =
42 | let mutable value = 0
43 |
44 | taskSeq { do! task { do value <- value + 1 } |> Task.ignore }
45 | |> verifyEmpty
46 | |> Task.map (fun _ -> value |> should equal 1)
47 |
48 | []
49 | let ``CE taskSeq: use 'do!' with a task-delay`` () =
50 | let mutable value = 0
51 |
52 | taskSeq {
53 | do value <- value + 1
54 | do! Task.Delay 50
55 | do value <- value + 1
56 | }
57 | |> verifyEmpty
58 | |> Task.map (fun _ -> value |> should equal 2)
59 |
60 | []
61 | let ``CE taskSeq: use 'do!' with Async`` () =
62 | let mutable value = 0
63 |
64 | taskSeq {
65 | do value <- value + 1
66 | do! Async.Sleep 50
67 | do value <- value + 1
68 | }
69 | |> verifyEmpty
70 | |> Task.map (fun _ -> value |> should equal 2)
71 |
72 | []
73 | let ``CE taskSeq: use 'do!' with Async - mutables`` () =
74 | let mutable value = 0
75 |
76 | taskSeq {
77 | do! async { value <- value + 1 }
78 | do! Async.Sleep 50
79 | do! async { value <- value + 1 }
80 | }
81 | |> verifyEmpty
82 | |> Task.map (fun _ -> value |> should equal 2)
83 |
84 | []
85 | let ``CE taskSeq: use 'do!' with all kinds of overloads at once`` () =
86 | let mutable value = 0
87 |
88 | // this test should be expanded in case any new overload is added
89 | // that is supported by `do!`, to ensure the high/low priority
90 | // overloads still work properly
91 | taskSeq {
92 | do! task { do value <- value + 1 } |> Task.ignore
93 | do! ValueTask <| task { do value <- value + 1 }
94 | do! ValueTask.ofTask (task { do value <- value + 1 })
95 | do! ValueTask<_>(()) // unit ValueTask that completes immediately
96 | do! Task.fromResult (()) // unit Task that completes immediately
97 | do! Task.Delay 0
98 | do! Async.Sleep 0
99 | do! async { value <- value + 1 } // eq 4
100 | }
101 | |> verifyEmpty
102 | |> Task.map (fun _ -> value |> should equal 4)
103 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Empty.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Empty
2 |
3 | open System.Threading.Tasks
4 | open Xunit
5 | open FsUnit.Xunit
6 |
7 | open FSharp.Control
8 |
9 |
10 | []
11 | let ``TaskSeq-empty returns an empty sequence`` () = task {
12 | let! sq = TaskSeq.empty |> TaskSeq.toListAsync
13 | Seq.isEmpty sq |> should be True
14 | Seq.length sq |> should equal 0
15 | }
16 |
17 | []
18 | let ``TaskSeq-empty returns an empty sequence - variant`` () = task {
19 | let! isEmpty = TaskSeq.empty |> TaskSeq.isEmpty
20 | isEmpty |> should be True
21 | }
22 |
23 | []
24 | let ``TaskSeq-empty in a taskSeq context`` () = task {
25 | let! sq =
26 | taskSeq { yield! TaskSeq.empty }
27 | |> TaskSeq.toArrayAsync
28 |
29 | Array.isEmpty sq |> should be True
30 | }
31 |
32 | []
33 | let ``TaskSeq-empty of unit in a taskSeq context`` () = task {
34 | let! sq =
35 | taskSeq { yield! TaskSeq.empty }
36 | |> TaskSeq.toArrayAsync
37 |
38 | Array.isEmpty sq |> should be True
39 | }
40 |
41 | []
42 | let ``TaskSeq-empty of more complex type in a taskSeq context`` () = task {
43 | let! sq =
44 | taskSeq { yield! TaskSeq.empty, int>> } // not a TaskResult, but a ResultTask lol
45 | |> TaskSeq.toArrayAsync
46 |
47 | Array.isEmpty sq |> should be True
48 | }
49 |
50 | []
51 | let ``TaskSeq-empty multiple times in a taskSeq context`` () = task {
52 | let! sq =
53 | taskSeq {
54 | yield! TaskSeq.empty
55 | yield! TaskSeq.empty
56 | yield! TaskSeq.empty
57 | yield! TaskSeq.empty
58 | yield! TaskSeq.empty
59 | }
60 | |> TaskSeq.toArrayAsync
61 |
62 | Array.isEmpty sq |> should be True
63 | }
64 |
65 | []
66 | let ``TaskSeq-empty multiple times with side effects`` () = task {
67 | let mutable x = 0
68 |
69 | let sq = taskSeq {
70 | yield! TaskSeq.empty
71 | yield! TaskSeq.empty
72 | x <- x + 1
73 | yield! TaskSeq.empty
74 | x <- x + 1
75 | yield! TaskSeq.empty
76 | x <- x + 1
77 | yield! TaskSeq.empty
78 | x <- x + 1
79 | x <- x + 1
80 | }
81 |
82 | // executing side effects once
83 | (TaskSeq.toArray >> Array.isEmpty) sq |> should be True
84 | x |> should equal 5
85 |
86 | // twice
87 | (TaskSeq.toArray >> Array.isEmpty) sq |> should be True
88 | x |> should equal 10
89 | }
90 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Except.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Except
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.except
10 | // TaskSeq.exceptOfSeq
11 | //
12 |
13 |
14 | module EmptySeq =
15 | []
16 | let ``Null source is invalid`` () =
17 | assertNullArg <| fun () -> TaskSeq.except null TaskSeq.empty
18 | assertNullArg <| fun () -> TaskSeq.except TaskSeq.empty null
19 | assertNullArg <| fun () -> TaskSeq.except null null
20 |
21 | assertNullArg
22 | <| fun () -> TaskSeq.exceptOfSeq null TaskSeq.empty
23 |
24 | assertNullArg
25 | <| fun () -> TaskSeq.exceptOfSeq Seq.empty null
26 |
27 | assertNullArg <| fun () -> TaskSeq.exceptOfSeq null null
28 |
29 | [)>]
30 | let ``TaskSeq-except`` variant =
31 | Gen.getEmptyVariant variant
32 | |> TaskSeq.except (Gen.getEmptyVariant variant)
33 | |> verifyEmpty
34 |
35 | [)>]
36 | let ``TaskSeq-exceptOfSeq`` variant =
37 | Gen.getEmptyVariant variant
38 | |> TaskSeq.exceptOfSeq Seq.empty
39 | |> verifyEmpty
40 |
41 | [)>]
42 | let ``TaskSeq-except v2`` variant =
43 | Gen.getEmptyVariant variant
44 | |> TaskSeq.except TaskSeq.empty
45 | |> verifyEmpty
46 |
47 | [)>]
48 | let ``TaskSeq-except v3`` variant =
49 | TaskSeq.empty
50 | |> TaskSeq.except (Gen.getEmptyVariant variant)
51 | |> verifyEmpty
52 |
53 | [)>]
54 | let ``TaskSeq-except no side effect in exclude seq if source seq is empty`` variant =
55 | let mutable i = 0
56 |
57 | // The `exclude` argument of TaskSeq.except is only iterated after the first item
58 | // from the input. With empty input, this is not evaluated
59 | let exclude = taskSeq {
60 | i <- i + 1 // we test that we never get here
61 | yield 12
62 | }
63 |
64 | Gen.getEmptyVariant variant
65 | |> TaskSeq.except exclude
66 | |> verifyEmpty
67 | |> Task.map (fun () -> i |> should equal 0) // exclude seq is only enumerated after first item in source
68 |
69 | module Immutable =
70 | [)>]
71 | let ``TaskSeq-except removes duplicates`` variant =
72 | TaskSeq.ofList [ 1; 1; 2; 3; 4; 12; 12; 12; 13; 13; 13; 13; 13; 99 ]
73 | |> TaskSeq.except (Gen.getSeqImmutable variant)
74 | |> TaskSeq.toArrayAsync
75 | |> Task.map (should equal [| 12; 13; 99 |])
76 |
77 | []
78 | let ``TaskSeq-except removes duplicates with empty itemsToExcept`` () =
79 | TaskSeq.ofList [ 1; 1; 2; 3; 4; 12; 12; 12; 13; 13; 13; 13; 13; 99 ]
80 | |> TaskSeq.except TaskSeq.empty
81 | |> TaskSeq.toArrayAsync
82 | |> Task.map (should equal [| 1; 2; 3; 4; 12; 13; 99 |])
83 |
84 | [)>]
85 | let ``TaskSeq-except removes everything`` variant =
86 | Gen.getSeqImmutable variant
87 | |> TaskSeq.except (Gen.getSeqImmutable variant)
88 | |> verifyEmpty
89 |
90 | [)>]
91 | let ``TaskSeq-except removes everything with duplicates`` variant =
92 | taskSeq {
93 | yield! Gen.getSeqImmutable variant
94 | yield! Gen.getSeqImmutable variant
95 | yield! Gen.getSeqImmutable variant
96 | yield! Gen.getSeqImmutable variant
97 | }
98 | |> TaskSeq.except (Gen.getSeqImmutable variant)
99 | |> verifyEmpty
100 |
101 | []
102 | let ``TaskSeq-exceptOfSeq removes duplicates`` () =
103 | TaskSeq.ofList [ 1; 1; 2; 3; 4; 12; 12; 12; 13; 13; 13; 13; 13; 99 ]
104 | |> TaskSeq.exceptOfSeq [ 1..10 ]
105 | |> TaskSeq.toArrayAsync
106 | |> Task.map (should equal [| 12; 13; 99 |])
107 |
108 | []
109 | let ``TaskSeq-exceptOfSeq removes duplicates with empty itemsToExcept`` () =
110 | TaskSeq.ofList [ 1; 1; 2; 3; 4; 12; 12; 12; 13; 13; 13; 13; 13; 99 ]
111 | |> TaskSeq.exceptOfSeq Seq.empty
112 | |> TaskSeq.toArrayAsync
113 | |> Task.map (should equal [| 1; 2; 3; 4; 12; 13; 99 |])
114 |
115 | [)>]
116 | let ``TaskSeq-exceptOfSeq removes everything`` variant =
117 | Gen.getSeqImmutable variant
118 | |> TaskSeq.exceptOfSeq [ 1..10 ]
119 | |> verifyEmpty
120 |
121 | [)>]
122 | let ``TaskSeq-exceptOfSeq removes everything with duplicates`` variant =
123 | taskSeq {
124 | yield! Gen.getSeqImmutable variant
125 | yield! Gen.getSeqImmutable variant
126 | yield! Gen.getSeqImmutable variant
127 | yield! Gen.getSeqImmutable variant
128 | }
129 | |> TaskSeq.exceptOfSeq [ 1..10 ]
130 | |> verifyEmpty
131 |
132 | module SideEffects =
133 | [)>]
134 | let ``TaskSeq-except removes duplicates`` variant =
135 | TaskSeq.ofList [ 1; 1; 2; 3; 4; 12; 12; 12; 13; 13; 13; 13; 13; 99 ]
136 | |> TaskSeq.except (Gen.getSeqWithSideEffect variant)
137 | |> TaskSeq.toArrayAsync
138 | |> Task.map (should equal [| 12; 13; 99 |])
139 |
140 | [)>]
141 | let ``TaskSeq-except removes everything`` variant =
142 | Gen.getSeqWithSideEffect variant
143 | |> TaskSeq.except (Gen.getSeqWithSideEffect variant)
144 | |> verifyEmpty
145 |
146 | [)>]
147 | let ``TaskSeq-except removes everything with duplicates`` variant =
148 | taskSeq {
149 | yield! Gen.getSeqWithSideEffect variant
150 | yield! Gen.getSeqWithSideEffect variant
151 | yield! Gen.getSeqWithSideEffect variant
152 | yield! Gen.getSeqWithSideEffect variant
153 | }
154 | |> TaskSeq.except (Gen.getSeqWithSideEffect variant)
155 | |> verifyEmpty
156 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Exists
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.exists
10 | // TaskSeq.existsAsyncc
11 | //
12 |
13 | module EmptySeq =
14 | []
15 | let ``Null source is invalid`` () =
16 | assertNullArg
17 | <| fun () -> TaskSeq.exists (fun _ -> false) null
18 |
19 | assertNullArg
20 | <| fun () -> TaskSeq.existsAsync (fun _ -> Task.fromResult false) null
21 |
22 | [)>]
23 | let ``TaskSeq-exists returns false`` variant =
24 | Gen.getEmptyVariant variant
25 | |> TaskSeq.exists ((=) 12)
26 | |> Task.map (should be False)
27 |
28 | [)>]
29 | let ``TaskSeq-existsAsync returns false`` variant =
30 | Gen.getEmptyVariant variant
31 | |> TaskSeq.existsAsync (fun x -> task { return x = 12 })
32 | |> Task.map (should be False)
33 |
34 | module Immutable =
35 | [)>]
36 | let ``TaskSeq-exists sad path returns false`` variant =
37 | Gen.getSeqImmutable variant
38 | |> TaskSeq.exists ((=) 0)
39 | |> Task.map (should be False)
40 |
41 | [)>]
42 | let ``TaskSeq-existsAsync sad path return false`` variant =
43 | Gen.getSeqImmutable variant
44 | |> TaskSeq.existsAsync (fun x -> task { return x = 0 })
45 | |> Task.map (should be False)
46 |
47 | [)>]
48 | let ``TaskSeq-exists happy path middle of seq`` variant =
49 | Gen.getSeqImmutable variant
50 | |> TaskSeq.exists (fun x -> x < 6 && x > 4)
51 | |> Task.map (should be True)
52 |
53 | [)>]
54 | let ``TaskSeq-existsAsync happy path middle of seq`` variant =
55 | Gen.getSeqImmutable variant
56 | |> TaskSeq.existsAsync (fun x -> task { return x < 6 && x > 4 })
57 | |> Task.map (should be True)
58 |
59 | [)>]
60 | let ``TaskSeq-exists happy path first item of seq`` variant =
61 | Gen.getSeqImmutable variant
62 | |> TaskSeq.exists ((=) 1)
63 | |> Task.map (should be True)
64 |
65 | [)>]
66 | let ``TaskSeq-existsAsync happy path first item of seq`` variant =
67 | Gen.getSeqImmutable variant
68 | |> TaskSeq.existsAsync (fun x -> task { return x = 1 })
69 | |> Task.map (should be True)
70 |
71 | [)>]
72 | let ``TaskSeq-exists happy path last item of seq`` variant =
73 | Gen.getSeqImmutable variant
74 | |> TaskSeq.exists ((=) 10)
75 | |> Task.map (should be True)
76 |
77 | [)>]
78 | let ``TaskSeq-existsAsync happy path last item of seq`` variant =
79 | Gen.getSeqImmutable variant
80 | |> TaskSeq.existsAsync (fun x -> task { return x = 10 })
81 | |> Task.map (should be True)
82 |
83 | module SideEffects =
84 | [)>]
85 | let ``TaskSeq-exists success only sometimes for mutated state`` variant = task {
86 | let ts = Gen.getSeqWithSideEffect variant
87 | let finder = (=) 11
88 |
89 | // first: false
90 | let! found = TaskSeq.exists finder ts
91 | found |> should be False
92 |
93 | // find again: found now, because of side effects
94 | let! found = TaskSeq.exists finder ts
95 | found |> should be True
96 |
97 | // find once more: false
98 | let! found = TaskSeq.exists finder ts
99 | found |> should be False
100 | }
101 |
102 | [)>]
103 | let ``TaskSeq-existsAsync success only sometimes for mutated state`` variant = task {
104 | let ts = Gen.getSeqWithSideEffect variant
105 | let finder x = task { return x = 11 }
106 |
107 | // first: false
108 | let! found = TaskSeq.existsAsync finder ts
109 | found |> should be False
110 |
111 | // find again: found now, because of side effects
112 | let! found = TaskSeq.existsAsync finder ts
113 | found |> should be True
114 |
115 | // find once more: false
116 | let! found = TaskSeq.existsAsync finder ts
117 | found |> should be False
118 | }
119 |
120 | []
121 | let ``TaskSeq-exists _specialcase_ prove we don't read past the found item`` () = task {
122 | let mutable i = 0
123 |
124 | let ts = taskSeq {
125 | for _ in 0..9 do
126 | i <- i + 1
127 | yield i
128 | }
129 |
130 | let! found = ts |> TaskSeq.exists ((=) 3)
131 | found |> should be True
132 | i |> should equal 3 // only partial evaluation!
133 |
134 | // find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
135 | let! found = ts |> TaskSeq.exists ((=) 4)
136 | found |> should be True
137 | i |> should equal 4 // only partial evaluation!
138 | }
139 |
140 | []
141 | let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item`` () = task {
142 | let mutable i = 0
143 |
144 | let ts = taskSeq {
145 | for _ in 0..9 do
146 | i <- i + 1
147 | yield i
148 | }
149 |
150 | let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 3 })
151 | found |> should be True
152 | i |> should equal 3 // only partial evaluation!
153 |
154 | // find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
155 | let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
156 | found |> should be True
157 | i |> should equal 4
158 | }
159 |
160 | []
161 | let ``TaskSeq-exists _specialcase_ prove we don't read past the found item v2`` () = task {
162 | let mutable i = 0
163 |
164 | let ts = taskSeq {
165 | yield 42
166 | i <- i + 1
167 | i <- i + 1
168 | }
169 |
170 | let! found = ts |> TaskSeq.exists ((=) 42)
171 | found |> should be True
172 | i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
173 | }
174 |
175 | []
176 | let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item v2`` () = task {
177 | let mutable i = 0
178 |
179 | let ts = taskSeq {
180 | yield 42
181 | i <- i + 1
182 | i <- i + 1
183 | }
184 |
185 | let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 42 })
186 | found |> should be True
187 | i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
188 | }
189 |
190 | []
191 | let ``TaskSeq-exists _specialcase_ prove statement after yield is not evaluated`` () = task {
192 | let mutable i = 0
193 |
194 | let ts = taskSeq {
195 | for _ in 0..9 do
196 | yield i
197 | i <- i + 1
198 | }
199 |
200 | let! found = ts |> TaskSeq.exists ((=) 0)
201 | found |> should be True
202 | i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
203 |
204 | // find some next item. We do get a new iterator, but mutable state is now still starting at '0'
205 | let! found = ts |> TaskSeq.exists ((=) 4)
206 | found |> should be True
207 | i |> should equal 4 // only partial evaluation!
208 | }
209 |
210 | []
211 | let ``TaskSeq-existsAsync _specialcase_ prove statement after yield is not evaluated`` () = task {
212 | let mutable i = 0
213 |
214 | let ts = taskSeq {
215 | for _ in 0..9 do
216 | yield i
217 | i <- i + 1
218 | }
219 |
220 | let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 0 })
221 | found |> should be True
222 | i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
223 |
224 | // find some next item. We do get a new iterator, but mutable state is now still starting at '0'
225 | let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
226 | found |> should be True
227 | i |> should equal 4 // only partial evaluation!
228 | }
229 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Filter
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.filter
10 | // TaskSeq.filterAsync
11 | // TaskSeq.where
12 | // TaskSeq.whereAsync
13 | //
14 |
15 |
16 | module EmptySeq =
17 | []
18 | let ``TaskSeq-filter or where with null source raises`` () =
19 | assertNullArg
20 | <| fun () -> TaskSeq.filter (fun _ -> false) null
21 |
22 | assertNullArg
23 | <| fun () -> TaskSeq.filterAsync (fun _ -> Task.fromResult false) null
24 |
25 | assertNullArg
26 | <| fun () -> TaskSeq.where (fun _ -> false) null
27 |
28 | assertNullArg
29 | <| fun () -> TaskSeq.whereAsync (fun _ -> Task.fromResult false) null
30 |
31 |
32 | [)>]
33 | let ``TaskSeq-filter or where has no effect`` variant = task {
34 | do!
35 | Gen.getEmptyVariant variant
36 | |> TaskSeq.filter ((=) 12)
37 | |> TaskSeq.toListAsync
38 | |> Task.map (List.isEmpty >> should be True)
39 |
40 | do!
41 | Gen.getEmptyVariant variant
42 | |> TaskSeq.where ((=) 12)
43 | |> TaskSeq.toListAsync
44 | |> Task.map (List.isEmpty >> should be True)
45 | }
46 |
47 | [)>]
48 | let ``TaskSeq-filterAsync or whereAsync has no effect`` variant = task {
49 | do!
50 | Gen.getEmptyVariant variant
51 | |> TaskSeq.filterAsync (fun x -> task { return x = 12 })
52 | |> TaskSeq.toListAsync
53 | |> Task.map (List.isEmpty >> should be True)
54 |
55 | do!
56 | Gen.getEmptyVariant variant
57 | |> TaskSeq.whereAsync (fun x -> task { return x = 12 })
58 | |> TaskSeq.toListAsync
59 | |> Task.map (List.isEmpty >> should be True)
60 | }
61 |
62 | module Immutable =
63 | [)>]
64 | let ``TaskSeq-filter or where filters correctly`` variant = task {
65 | do!
66 | Gen.getSeqImmutable variant
67 | |> TaskSeq.filter ((<=) 5) // greater than
68 | |> verifyDigitsAsString "EFGHIJ"
69 |
70 | do!
71 | Gen.getSeqImmutable variant
72 | |> TaskSeq.where ((>) 5) // greater than
73 | |> verifyDigitsAsString "ABCD"
74 | }
75 |
76 | [)>]
77 | let ``TaskSeq-filterAsync or whereAsync filters correctly`` variant = task {
78 | do!
79 | Gen.getSeqImmutable variant
80 | |> TaskSeq.filterAsync (fun x -> task { return x <= 5 })
81 | |> verifyDigitsAsString "ABCDE"
82 |
83 | do!
84 | Gen.getSeqImmutable variant
85 | |> TaskSeq.whereAsync (fun x -> task { return x > 5 })
86 | |> verifyDigitsAsString "FGHIJ"
87 |
88 | }
89 |
90 | module SideEffects =
91 | [)>]
92 | let ``TaskSeq-filter filters correctly`` variant = task {
93 | do!
94 | Gen.getSeqWithSideEffect variant
95 | |> TaskSeq.filter ((<=) 5) // greater than or equal
96 | |> verifyDigitsAsString "EFGHIJ"
97 |
98 | do!
99 | Gen.getSeqWithSideEffect variant
100 | |> TaskSeq.where ((>) 5) // less than
101 | |> verifyDigitsAsString "ABCD"
102 | }
103 |
104 | [)>]
105 | let ``TaskSeq-filterAsync filters correctly`` variant = task {
106 | do!
107 | Gen.getSeqWithSideEffect variant
108 | |> TaskSeq.filterAsync (fun x -> task { return x <= 5 })
109 | |> verifyDigitsAsString "ABCDE"
110 |
111 | do!
112 | Gen.getSeqWithSideEffect variant
113 | |> TaskSeq.whereAsync (fun x -> task { return x > 5 && x < 9 })
114 | |> verifyDigitsAsString "FGH"
115 | }
116 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Fold.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Fold
2 |
3 | open System.Text
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.fold
12 | // TaskSeq.foldAsync
13 | //
14 |
15 | module EmptySeq =
16 | []
17 | let ``Null source is invalid`` () =
18 | assertNullArg
19 | <| fun () -> TaskSeq.fold (fun _ _ -> 42) 0 null
20 |
21 | assertNullArg
22 | <| fun () -> TaskSeq.foldAsync (fun _ _ -> Task.fromResult 42) 0 null
23 |
24 | [)>]
25 | let ``TaskSeq-fold takes state when empty`` variant = task {
26 | let! empty =
27 | Gen.getEmptyVariant variant
28 | |> TaskSeq.fold (fun _ item -> char (item + 64)) '_'
29 |
30 | empty |> should equal '_'
31 | }
32 |
33 | [)>]
34 | let ``TaskSeq-foldAsync takes state when empty`` variant = task {
35 | let! alphabet =
36 | Gen.getEmptyVariant variant
37 | |> TaskSeq.foldAsync (fun _ item -> task { return char (item + 64) }) '_'
38 |
39 | alphabet |> should equal '_'
40 | }
41 |
42 | module Immutable =
43 | [)>]
44 | let ``TaskSeq-fold folds with every item`` variant = task {
45 | let! letters =
46 | (StringBuilder(), Gen.getSeqImmutable variant)
47 | ||> TaskSeq.fold (fun state item -> state.Append(char item + '@'))
48 |
49 | letters.ToString() |> should equal "ABCDEFGHIJ"
50 | }
51 |
52 | [)>]
53 | let ``TaskSeq-foldAsync folds with every item`` variant = task {
54 | let! letters =
55 | (StringBuilder(), Gen.getSeqImmutable variant)
56 | ||> TaskSeq.foldAsync (fun state item -> task { return state.Append(char item + '@') })
57 |
58 |
59 | letters.ToString() |> should equal "ABCDEFGHIJ"
60 | }
61 |
62 | module SideEffects =
63 | [)>]
64 | let ``TaskSeq-fold folds with every item, next fold has different state`` variant = task {
65 | let ts = Gen.getSeqWithSideEffect variant
66 |
67 | let! letters =
68 | (StringBuilder(), ts)
69 | ||> TaskSeq.fold (fun state item -> state.Append(char item + '@'))
70 |
71 | string letters |> should equal "ABCDEFGHIJ"
72 |
73 | let! moreLetters =
74 | (letters, ts)
75 | ||> TaskSeq.fold (fun state item -> state.Append(char item + '@'))
76 |
77 | string moreLetters |> should equal "ABCDEFGHIJKLMNOPQRST"
78 | }
79 |
80 | [)>]
81 | let ``TaskSeq-foldAsync folds with every item, next fold has different state`` variant = task {
82 | let ts = Gen.getSeqWithSideEffect variant
83 |
84 | let! letters =
85 | (StringBuilder(), ts)
86 | ||> TaskSeq.foldAsync (fun state item -> task { return state.Append(char item + '@') })
87 |
88 | string letters |> should equal "ABCDEFGHIJ"
89 |
90 | let! moreLetters =
91 | (letters, ts)
92 | ||> TaskSeq.foldAsync (fun state item -> task { return state.Append(char item + '@') })
93 |
94 | string moreLetters |> should equal "ABCDEFGHIJKLMNOPQRST"
95 | }
96 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Forall
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.forall
10 | // TaskSeq.forallAsyncc
11 | //
12 |
13 | module EmptySeq =
14 | []
15 | let ``Null source is invalid`` () =
16 | assertNullArg
17 | <| fun () -> TaskSeq.forall (fun _ -> false) null
18 |
19 | assertNullArg
20 | <| fun () -> TaskSeq.forallAsync (fun _ -> Task.fromResult false) null
21 |
22 | [)>]
23 | let ``TaskSeq-forall always returns true`` variant =
24 | Gen.getEmptyVariant variant
25 | |> TaskSeq.forall ((=) 12)
26 | |> Task.map (should be True)
27 |
28 | [)>]
29 | let ``TaskSeq-forallAsync always returns true`` variant =
30 | Gen.getEmptyVariant variant
31 | |> TaskSeq.forallAsync (fun x -> task { return x = 12 })
32 | |> Task.map (should be True)
33 |
34 | module Immutable =
35 | [)>]
36 | let ``TaskSeq-forall sad path returns false`` variant = task {
37 | do!
38 | Gen.getSeqImmutable variant
39 | |> TaskSeq.forall ((=) 0)
40 | |> Task.map (should be False)
41 |
42 | do!
43 | Gen.getSeqImmutable variant
44 | |> TaskSeq.forall ((>) 9) // lt
45 | |> Task.map (should be False)
46 | }
47 |
48 | [)>]
49 | let ``TaskSeq-forallAsync sad path returns false`` variant = task {
50 | do!
51 | Gen.getSeqImmutable variant
52 | |> TaskSeq.forallAsync (fun x -> task { return x = 0 })
53 | |> Task.map (should be False)
54 |
55 | do!
56 | Gen.getSeqImmutable variant
57 | |> TaskSeq.forallAsync (fun x -> task { return x < 9 })
58 | |> Task.map (should be False)
59 | }
60 |
61 | [)>]
62 | let ``TaskSeq-forall happy path whole seq true`` variant =
63 | Gen.getSeqImmutable variant
64 | |> TaskSeq.forall (fun x -> x < 6 || x > 5)
65 | |> Task.map (should be True)
66 |
67 | [)>]
68 | let ``TaskSeq-forallAsync happy path whole seq true`` variant =
69 | Gen.getSeqImmutable variant
70 | |> TaskSeq.forallAsync (fun x -> task { return x <= 10 && x >= 0 })
71 | |> Task.map (should be True)
72 |
73 | module SideEffects =
74 | [)>]
75 | let ``TaskSeq-forall mutated state can change result`` variant = task {
76 | let ts = Gen.getSeqWithSideEffect variant
77 | let predicate x = x > 10
78 |
79 | // first: false
80 | let! found = TaskSeq.forall predicate ts
81 | found |> should be False // fails on first item, not many side effects yet
82 |
83 | // ensure side effects executes
84 | do! consumeTaskSeq ts
85 |
86 | // find again: found now, because of side effects
87 | let! found = TaskSeq.forall predicate ts
88 | found |> should be True
89 |
90 | // find once more, still true, as numbers increase
91 | do! consumeTaskSeq ts // ensure side effects executes
92 | let! found = TaskSeq.forall predicate ts
93 | found |> should be True
94 | }
95 |
96 | [)>]
97 | let ``TaskSeq-forallAsync mutated state can change result`` variant = task {
98 | let ts = Gen.getSeqWithSideEffect variant
99 | let predicate x = Task.fromResult (x > 10)
100 |
101 | // first: false
102 | let! found = TaskSeq.forallAsync predicate ts
103 | found |> should be False // fails on first item, not many side effects yet
104 |
105 | // ensure side effects executes
106 | do! consumeTaskSeq ts
107 |
108 | // find again: found now, because of side effects
109 | let! found = TaskSeq.forallAsync predicate ts
110 | found |> should be True
111 |
112 | // find once more, still true, as numbers increase
113 | do! consumeTaskSeq ts // ensure side effects executes
114 | let! found = TaskSeq.forallAsync predicate ts
115 | found |> should be True
116 | }
117 |
118 | []
119 | let ``TaskSeq-forall _specialcase_ prove we don't read past the first failing item`` () = task {
120 | let mutable i = 0
121 |
122 | let ts = taskSeq {
123 | for _ in 0..9 do
124 | i <- i + 1
125 | yield i
126 | }
127 |
128 | let! found = ts |> TaskSeq.forall ((>) 3)
129 | found |> should be False
130 | i |> should equal 3 // only partial evaluation!
131 |
132 | // find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
133 | let! found = ts |> TaskSeq.forall ((<=) 4)
134 | found |> should be True
135 | i |> should equal 13 // we evaluated to the end
136 | }
137 |
138 | []
139 | let ``TaskSeq-forallAsync _specialcase_ prove we don't read past the first failing item`` () = task {
140 | let mutable i = 0
141 |
142 | let ts = taskSeq {
143 | for _ in 0..9 do
144 | i <- i + 1
145 | yield i
146 | }
147 |
148 | let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 3))
149 | found |> should be False
150 | i |> should equal 3 // only partial evaluation!
151 |
152 | // find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
153 | let! found =
154 | ts
155 | |> TaskSeq.forallAsync (fun x -> Task.fromResult (x >= 4))
156 |
157 | found |> should be True
158 | i |> should equal 13 // we evaluated to the end
159 | }
160 |
161 |
162 | []
163 | let ``TaskSeq-forall _specialcase_ prove statement after first false result is not evaluated`` () = task {
164 | let mutable i = 0
165 |
166 | let ts = taskSeq {
167 | for _ in 0..9 do
168 | yield i
169 | i <- i + 1
170 | }
171 |
172 | let! found = ts |> TaskSeq.forall ((>) 0)
173 | found |> should be False
174 | i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated
175 |
176 | // find some next item. We do get a new iterator, but mutable state is still starting at '0'
177 | let! found = ts |> TaskSeq.forall ((>) 4)
178 | found |> should be False
179 | i |> should equal 4 // only partial evaluation!
180 | }
181 |
182 | []
183 | let ``TaskSeq-forallAsync _specialcase_ prove statement after first false result is not evaluated`` () = task {
184 | let mutable i = 0
185 |
186 | let ts = taskSeq {
187 | for _ in 0..9 do
188 | yield i
189 | i <- i + 1
190 | }
191 |
192 | let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 0))
193 | found |> should be False
194 | i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated
195 |
196 | // find some next item. We do get a new iterator, but mutable state is still starting at '0'
197 | let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 4))
198 | found |> should be False
199 | i |> should equal 4 // only partial evaluation!
200 | }
201 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Head.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Head
2 |
3 | open System
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.head
12 | // TaskSeq.tryHead
13 | //
14 |
15 | module EmptySeq =
16 | []
17 | let ``Null source is invalid`` () =
18 | assertNullArg <| fun () -> TaskSeq.head null
19 | assertNullArg <| fun () -> TaskSeq.tryHead null
20 |
21 | [)>]
22 | let ``TaskSeq-head throws`` variant = task {
23 | fun () -> Gen.getEmptyVariant variant |> TaskSeq.head |> Task.ignore
24 | |> should throwAsyncExact typeof
25 | }
26 |
27 | [)>]
28 | let ``TaskSeq-tryHead returns None`` variant = task {
29 | let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryHead
30 | nothing |> should be None'
31 | }
32 |
33 | []
34 | let ``TaskSeq-head throws, but side effect is executed`` () = task {
35 | let mutable x = 0
36 |
37 | fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.head |> Task.ignore
38 | |> should throwAsyncExact typeof
39 |
40 | // side effect must have run!
41 | x |> should equal 1
42 | }
43 |
44 |
45 | module Immutable =
46 | [)>]
47 | let ``TaskSeq-head gets the head item of longer sequence`` variant = task {
48 | let ts = Gen.getSeqImmutable variant
49 |
50 | let! head = TaskSeq.head ts
51 | head |> should equal 1
52 |
53 | let! head = TaskSeq.head ts //immutable, so re-iteration does not change outcome
54 | head |> should equal 1
55 | }
56 |
57 | [)>]
58 | let ``TaskSeq-tryHead gets the head item of longer sequence`` variant = task {
59 | let ts = Gen.getSeqImmutable variant
60 |
61 | let! head = TaskSeq.tryHead ts
62 | head |> should equal (Some 1)
63 |
64 | let! head = TaskSeq.tryHead ts //immutable, so re-iteration does not change outcome
65 | head |> should equal (Some 1)
66 | }
67 |
68 | []
69 | let ``TaskSeq-head gets the only item in a singleton sequence`` () = task {
70 | let ts = taskSeq { yield 42 }
71 |
72 | let! head = TaskSeq.head ts
73 | head |> should equal 42
74 |
75 | let! head = TaskSeq.head ts // doing it twice is fine
76 | head |> should equal 42
77 | }
78 |
79 | []
80 | let ``TaskSeq-tryHead gets the only item in a singleton sequence`` () = task {
81 | let ts = taskSeq { yield 42 }
82 |
83 | let! head = TaskSeq.tryHead ts
84 | head |> should equal (Some 42)
85 |
86 | let! head = TaskSeq.tryHead ts // doing it twice is fine
87 | head |> should equal (Some 42)
88 | }
89 |
90 |
91 | module SideEffects =
92 | []
93 | let ``TaskSeq-head __special-case__ prove it does not read beyond first yield`` () = task {
94 | let mutable x = 42
95 |
96 | let one = taskSeq {
97 | yield x
98 | x <- x + 1 // we never get here
99 | }
100 |
101 | let! fortyTwo = one |> TaskSeq.head
102 | let! stillFortyTwo = one |> TaskSeq.head // the statement after 'yield' will never be reached
103 |
104 | fortyTwo |> should equal 42
105 | stillFortyTwo |> should equal 42
106 | }
107 |
108 | []
109 | let ``TaskSeq-tryHead __special-case__ prove it does not read beyond first yield`` () = task {
110 | let mutable x = 42
111 |
112 | let one = taskSeq {
113 | yield x
114 | x <- x + 1 // we never get here
115 | }
116 |
117 | let! fortyTwo = one |> TaskSeq.tryHead
118 | fortyTwo |> should equal (Some 42)
119 |
120 | // the statement after 'yield' will never be reached, the mutable will not be updated
121 | let! stillFortyTwo = one |> TaskSeq.tryHead
122 | stillFortyTwo |> should equal (Some 42)
123 |
124 | }
125 |
126 | []
127 | let ``TaskSeq-head __special-case__ prove early side effect is executed`` () = task {
128 | let mutable x = 42
129 |
130 | let one = taskSeq {
131 | x <- x + 1
132 | x <- x + 1
133 | yield 42
134 | x <- x + 200 // we won't get here!
135 | }
136 |
137 | let! fortyTwo = one |> TaskSeq.head
138 | fortyTwo |> should equal 42
139 | x |> should equal 44
140 | let! fortyTwo = one |> TaskSeq.head
141 | fortyTwo |> should equal 42
142 | x |> should equal 46
143 | }
144 |
145 | []
146 | let ``TaskSeq-tryHead __special-case__ prove early side effect is executed`` () = task {
147 | let mutable x = 42
148 |
149 | let one = taskSeq {
150 | x <- x + 1
151 | x <- x + 1
152 | yield 42
153 | x <- x + 200 // we won't get here!
154 | }
155 |
156 | let! fortyTwo = one |> TaskSeq.tryHead
157 | fortyTwo |> should equal (Some 42)
158 | x |> should equal 44
159 | let! fortyTwo = one |> TaskSeq.tryHead
160 | fortyTwo |> should equal (Some 42)
161 | x |> should equal 46
162 |
163 | }
164 |
165 | [)>]
166 | let ``TaskSeq-head gets the head item in a longer sequence, with mutation`` variant = task {
167 | let ts = Gen.getSeqWithSideEffect variant
168 |
169 | let! ten = TaskSeq.head ts
170 | ten |> should equal 1
171 |
172 | // side effect, reiterating causes it to execute again!
173 | let! twenty = TaskSeq.head ts
174 | twenty |> should not' (equal 1) // different test data changes first item counter differently
175 | }
176 |
177 | [)>]
178 | let ``TaskSeq-tryHead gets the head item in a longer sequence, with mutation`` variant = task {
179 | let ts = Gen.getSeqWithSideEffect variant
180 |
181 | let! ten = TaskSeq.tryHead ts
182 | ten |> should equal (Some 1)
183 |
184 | // side effect, reiterating causes it to execute again!
185 | let! twenty = TaskSeq.tryHead ts
186 | twenty |> should not' (equal (Some 1)) // different test data changes first item counter differently
187 | }
188 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Indexed.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Indexed
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.indexed
10 | //
11 |
12 | module EmptySeq =
13 | []
14 | let ``Null source is invalid`` () = assertNullArg <| fun () -> TaskSeq.indexed null
15 |
16 | [)>]
17 | let ``TaskSeq-indexed on empty`` variant =
18 | Gen.getEmptyVariant variant
19 | |> TaskSeq.indexed
20 | |> verifyEmpty
21 |
22 | module Immutable =
23 | []
24 | let ``TaskSeq-indexed starts at zero`` () =
25 | taskSeq { yield 99 }
26 | |> TaskSeq.indexed
27 | |> TaskSeq.head
28 | |> Task.map (should equal (0, 99))
29 |
30 | [)>]
31 | let ``TaskSeq-indexed`` variant =
32 | Gen.getSeqImmutable variant
33 | |> TaskSeq.indexed
34 | |> TaskSeq.toArrayAsync
35 | |> Task.map (Array.forall (fun (x, y) -> x + 1 = y))
36 | |> Task.map (should be True)
37 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Init.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Init
2 |
3 | open System
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.init
12 | // TaskSeq.initInfinite
13 | // TaskSeq.initAsync
14 | // TaskSeq.initInfiniteAsync
15 | //
16 |
17 | /// Asserts that a sequence contains the char values 'A'..'J'.
18 |
19 | module EmptySeq =
20 | []
21 | let ``TaskSeq-init can generate an empty sequence`` () = TaskSeq.init 0 (fun x -> x) |> verifyEmpty
22 |
23 | []
24 | let ``TaskSeq-initAsync can generate an empty sequence`` () =
25 | TaskSeq.initAsync 0 (fun x -> Task.fromResult x)
26 | |> verifyEmpty
27 |
28 | []
29 | let ``TaskSeq-init with a negative count gives an error`` () =
30 | fun () ->
31 | TaskSeq.init -1 (fun x -> Task.fromResult x)
32 | |> TaskSeq.toArrayAsync
33 | |> Task.ignore
34 |
35 | |> should throwAsyncExact typeof
36 |
37 | fun () ->
38 | TaskSeq.init Int32.MinValue (fun x -> Task.fromResult x)
39 | |> TaskSeq.toArrayAsync
40 | |> Task.ignore
41 |
42 | |> should throwAsyncExact typeof
43 |
44 | []
45 | let ``TaskSeq-initAsync with a negative count gives an error`` () =
46 | fun () ->
47 | TaskSeq.initAsync Int32.MinValue (fun x -> Task.fromResult x)
48 | |> TaskSeq.toArrayAsync
49 | |> Task.ignore
50 |
51 | |> should throwAsyncExact typeof
52 |
53 | module Immutable =
54 | []
55 | let ``TaskSeq-init singleton`` () =
56 | TaskSeq.init 1 id
57 | |> TaskSeq.head
58 | |> Task.map (should equal 0)
59 |
60 | []
61 | let ``TaskSeq-initAsync singleton`` () =
62 | TaskSeq.initAsync 1 (id >> Task.fromResult)
63 | |> TaskSeq.head
64 | |> Task.map (should equal 0)
65 |
66 | []
67 | let ``TaskSeq-init some values`` () =
68 | TaskSeq.init 42 (fun x -> x / 2)
69 | |> TaskSeq.length
70 | |> Task.map (should equal 42)
71 |
72 | []
73 | let ``TaskSeq-initAsync some values`` () =
74 | TaskSeq.init 42 (fun x -> Task.fromResult (x / 2))
75 | |> TaskSeq.length
76 | |> Task.map (should equal 42)
77 |
78 | []
79 | let ``TaskSeq-initInfinite`` () =
80 | TaskSeq.initInfinite (fun x -> x / 2)
81 | |> TaskSeq.item 1_000_001
82 | |> Task.map (should equal 500_000)
83 |
84 | []
85 | let ``TaskSeq-initInfiniteAsync`` () =
86 | TaskSeq.initInfiniteAsync (fun x -> Task.fromResult (x / 2))
87 | |> TaskSeq.item 1_000_001
88 | |> Task.map (should equal 500_000)
89 |
90 | module SideEffects =
91 | let inc (i: int byref) =
92 | i <- i + 1
93 | i
94 |
95 | []
96 | let ``TaskSeq-init singleton with side effects`` () = task {
97 | let mutable x = 0
98 |
99 | let ts = TaskSeq.init 1 (fun _ -> inc &x)
100 |
101 | do! TaskSeq.head ts |> Task.map (should equal 1)
102 | do! TaskSeq.head ts |> Task.map (should equal 2)
103 | do! TaskSeq.head ts |> Task.map (should equal 3) // state mutates
104 | }
105 |
106 | []
107 | let ``TaskSeq-init singleton with side effects -- Current`` () = task {
108 | let mutable x = 0
109 |
110 | let ts = TaskSeq.init 1 (fun _ -> inc &x)
111 |
112 | let enumerator = ts.GetAsyncEnumerator()
113 | let! _ = enumerator.MoveNextAsync()
114 | do enumerator.Current |> should equal 1
115 | do enumerator.Current |> should equal 1
116 | do enumerator.Current |> should equal 1 // current state does not mutate
117 | }
118 |
119 | []
120 | let ``TaskSeq-initAsync singleton with side effects`` () = task {
121 | let mutable x = 0
122 |
123 | let ts = TaskSeq.initAsync 1 (fun _ -> Task.fromResult (inc &x))
124 |
125 | do! TaskSeq.head ts |> Task.map (should equal 1)
126 | do! TaskSeq.head ts |> Task.map (should equal 2)
127 | do! TaskSeq.head ts |> Task.map (should equal 3) // state mutates
128 | }
129 |
130 | []
131 | let ``TaskSeq-initAsync singleton with side effects -- Current`` () = task {
132 | let mutable x = 0
133 |
134 | let ts = TaskSeq.initAsync 1 (fun _ -> Task.fromResult (inc &x))
135 |
136 | let enumerator = ts.GetAsyncEnumerator()
137 | let! _ = enumerator.MoveNextAsync()
138 | do enumerator.Current |> should equal 1
139 | do enumerator.Current |> should equal 1
140 | do enumerator.Current |> should equal 1 // current state does not mutate
141 | }
142 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.IsEmpty.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.IsEmpty
2 |
3 | open Xunit
4 | open FsUnit.Xunit
5 |
6 | open FSharp.Control
7 |
8 | //
9 | // TaskSeq.isEmpty
10 | //
11 |
12 | module EmptySeq =
13 | []
14 | let ``Null source is invalid`` () = assertNullArg <| fun () -> TaskSeq.head null
15 |
16 | [)>]
17 | let ``TaskSeq-isEmpty returns true for empty`` variant =
18 | Gen.getEmptyVariant variant
19 | |> TaskSeq.isEmpty
20 | |> Task.map (should be True)
21 |
22 | module Immutable =
23 | []
24 | let ``TaskSeq-isEmpty returns false for singleton`` () =
25 | taskSeq { yield 42 }
26 | |> TaskSeq.isEmpty
27 | |> Task.map (should be False)
28 |
29 | []
30 | let ``TaskSeq-isEmpty returns false for delayed singleton sequence`` () =
31 | Gen.sideEffectTaskSeqMs 200 400 3
32 | |> TaskSeq.isEmpty
33 | |> Task.map (should be False)
34 |
35 | [)>]
36 | let ``TaskSeq-isEmpty returns false for non-empty`` variant =
37 | Gen.getSeqImmutable variant
38 | |> TaskSeq.isEmpty
39 | |> Task.map (should be False)
40 |
41 | module SideEffects =
42 | []
43 | let ``TaskSeq-isEmpty prove that it won't execute side effects after the first item`` () =
44 | let mutable i = 0
45 |
46 | taskSeq {
47 | i <- i + 1
48 | yield 42
49 | i <- i + 1
50 | }
51 | |> TaskSeq.isEmpty
52 | |> Task.map (should be False)
53 | |> Task.map (fun () -> i |> should equal 1)
54 |
55 | []
56 | let ``TaskSeq-isEmpty prove that it does execute side effects if empty`` () =
57 | let mutable i = 0
58 |
59 | taskSeq {
60 | i <- i + 1
61 | i <- i + 1
62 | }
63 | |> TaskSeq.isEmpty
64 | |> Task.map (should be True)
65 | |> Task.map (fun () -> i |> should equal 2)
66 |
67 | []
68 | let ``TaskSeq-isEmpty executes side effects each time`` () =
69 | let mutable i = 0
70 |
71 | taskSeq {
72 | i <- i + 1
73 | i <- i + 1
74 | }
75 | |>> TaskSeq.isEmpty
76 | |>> TaskSeq.isEmpty
77 | |>> TaskSeq.isEmpty
78 | |> TaskSeq.isEmpty // 4th time: 8
79 | |> Task.map (should be True)
80 | |> Task.map (fun () -> i |> should equal 8)
81 |
82 | [)>]
83 | let ``TaskSeq-isEmpty returns false for non-empty`` variant =
84 | Gen.getSeqWithSideEffect variant
85 | |> TaskSeq.isEmpty
86 | |> Task.map (should be False)
87 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Last
2 |
3 | open System
4 |
5 | open Xunit
6 | open FsUnit.Xunit
7 |
8 | open FSharp.Control
9 |
10 | //
11 | // TaskSeq.last
12 | // TaskSeq.tryLast
13 | //
14 |
15 | module EmptySeq =
16 | []
17 | let ``Null source is invalid`` () =
18 | assertNullArg <| fun () -> TaskSeq.last null
19 | assertNullArg <| fun () -> TaskSeq.tryLast null
20 |
21 | [)>]
22 | let ``TaskSeq-last throws`` variant = task {
23 | fun () -> Gen.getEmptyVariant variant |> TaskSeq.last |> Task.ignore
24 | |> should throwAsyncExact typeof
25 | }
26 |
27 | [)>]
28 | let ``TaskSeq-tryLast returns None`` variant = task {
29 | let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryLast
30 | nothing |> should be None'
31 | }
32 |
33 | []
34 | let ``TaskSeq-last executes side effect`` () = task {
35 | let mutable x = 0
36 |
37 | fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.last |> Task.ignore
38 | |> should throwAsyncExact typeof
39 |
40 | // side effect must have run!
41 | x |> should equal 1
42 | }
43 |
44 | []
45 | let ``TaskSeq-tryLast executes side effect`` () = task {
46 | let mutable x = 0
47 |
48 | let! nothing = taskSeq { do x <- x + 1 } |> TaskSeq.tryLast
49 | nothing |> should be None'
50 |
51 | // side effect must have run!
52 | x |> should equal 1
53 | }
54 |
55 |
56 | module Immutable =
57 | [)>]
58 | let ``TaskSeq-last gets the last item`` variant = task {
59 | let ts = Gen.getSeqImmutable variant
60 |
61 | let! last = TaskSeq.last ts
62 | last |> should equal 10
63 |
64 | let! last = TaskSeq.last ts //immutable, so re-iteration does not change outcome
65 | last |> should equal 10
66 | }
67 |
68 | []
69 | let ``TaskSeq-last gets the only item in a singleton sequence`` () = task {
70 | let ts = taskSeq { yield 42 }
71 |
72 | let! last = TaskSeq.last ts
73 | last |> should equal 42
74 |
75 | let! last = TaskSeq.last ts // doing it twice is fine
76 | last |> should equal 42
77 | }
78 |
79 | [)>]
80 | let ``TaskSeq-tryLast gets the last item`` variant = task {
81 | let ts = Gen.getSeqImmutable variant
82 |
83 | let! last = TaskSeq.tryLast ts
84 | last |> should equal (Some 10)
85 |
86 | let! last = TaskSeq.tryLast ts //immutable, so re-iteration does not change outcome
87 | last |> should equal (Some 10)
88 | }
89 |
90 | []
91 | let ``TaskSeq-tryLast gets the only item in a singleton sequence`` () = task {
92 | let ts = taskSeq { yield 42 }
93 |
94 | let! last = TaskSeq.tryLast ts
95 | last |> should equal (Some 42)
96 |
97 | let! last = TaskSeq.tryLast ts // doing it twice is fine
98 | last |> should equal (Some 42)
99 | }
100 |
101 |
102 | module SideEffects =
103 | []
104 | let ``TaskSeq-last executes side effect after first item`` () = task {
105 | let mutable x = 42
106 |
107 | let one = taskSeq {
108 | yield x
109 | x <- x + 1
110 | }
111 |
112 | let! fortyTwo = one |> TaskSeq.last
113 | let! fortyThree = one |> TaskSeq.last // side effect, re-iterating!
114 |
115 | fortyTwo |> should equal 42
116 | fortyThree |> should equal 43
117 | }
118 |
119 | []
120 | let ``TaskSeq-tryLast executes side effect after first item`` () = task {
121 | let mutable x = 42
122 |
123 | let one = taskSeq {
124 | yield x
125 | x <- x + 1
126 | }
127 |
128 | let! fortyTwo = one |> TaskSeq.tryLast
129 | fortyTwo |> should equal (Some 42)
130 |
131 | // side effect, reiterating causes it to execute again!
132 | let! fortyThree = one |> TaskSeq.tryLast
133 | fortyThree |> should equal (Some 43)
134 | }
135 |
136 | [)>]
137 | let ``TaskSeq-last gets the last item`` variant = task {
138 | let ts = Gen.getSeqWithSideEffect variant
139 |
140 | let! ten = TaskSeq.last ts
141 | ten |> should equal 10
142 |
143 | // side effect, reiterating causes it to execute again!
144 | let! twenty = TaskSeq.last ts
145 | twenty |> should equal 20
146 | }
147 |
148 | [)>]
149 | let ``TaskSeq-tryLast gets the last item`` variant = task {
150 | let ts = Gen.getSeqWithSideEffect variant
151 |
152 | let! ten = TaskSeq.tryLast ts
153 | ten |> should equal (Some 10)
154 |
155 | // side effect, reiterating causes it to execute again!
156 | let! twenty = TaskSeq.tryLast ts
157 | twenty |> should equal (Some 20)
158 | }
159 |
--------------------------------------------------------------------------------
/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs:
--------------------------------------------------------------------------------
1 | module TaskSeq.Tests.Let
2 |
3 | open System.Threading.Tasks
4 |
5 | open FsUnit
6 | open Xunit
7 |
8 | open FSharp.Control
9 |
10 | []
11 | let ``CE taskSeq: use 'let'`` () =
12 | let mutable value = 0
13 |
14 | taskSeq {
15 | let value1 = value + 1
16 | let value2 = value1 + 1
17 | yield value2
18 | }
19 | |> TaskSeq.exactlyOne
20 | |> Task.map (should equal 2)
21 |
22 | []
23 | let ``CE taskSeq: use 'let!' with a task`` () =
24 | let mutable value = 0
25 |
26 | taskSeq {
27 | let! unit' = task { do value <- value + 1 }
28 | do unit'
29 | }
30 | |> verifyEmpty
31 | |> Task.map (fun _ -> value |> should equal 1)
32 |
33 | [