├── .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 List existing tags: 22 | git tag -n 23 | 24 | ECHO. 25 | ECHO Checkout to tag: 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 Retrieve original commit date 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 Updated tag: 46 | git tag --points-at HEAD -n 47 | ECHO. 48 | 49 | REM Push to remove and override (with --force) 50 | ECHO Push changes to remote 51 | git push --tags --force 52 | 53 | REM Go back to original HEAD 54 | ECHO. 55 | ECHO Back to original HEAD 56 | git checkout - 57 | 58 | ECHO. 59 | ECHO List of all tags 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 | [] 34 | let ``CE taskSeq: use 'let!' with a task`` () = 35 | taskSeq { 36 | let! test = task { return "test" } 37 | yield test 38 | } 39 | |> TaskSeq.exactlyOne 40 | |> Task.map (should equal "test") 41 | 42 | [] 43 | let ``CE taskSeq: use 'let!' with a ValueTask`` () = 44 | let mutable value = 0 45 | 46 | taskSeq { 47 | let! unit' = ValueTask.ofTask (task { do value <- value + 1 }) 48 | do unit' 49 | } 50 | |> verifyEmpty 51 | |> Task.map (fun _ -> value |> should equal 1) 52 | 53 | [] 54 | let ``CE taskSeq: use 'let!' with a ValueTask`` () = 55 | taskSeq { 56 | let! test = ValueTask.ofTask (task { return "test" }) 57 | yield test 58 | } 59 | |> TaskSeq.exactlyOne 60 | |> Task.map (should equal "test") 61 | 62 | [] 63 | let ``CE taskSeq: use 'let!' with a non-generic ValueTask`` () = 64 | let mutable value = 0 65 | 66 | taskSeq { 67 | let! unit' = ValueTask(task { do value <- value + 1 }) 68 | do unit' 69 | } 70 | |> verifyEmpty 71 | |> Task.map (fun _ -> value |> should equal 1) 72 | 73 | [] 74 | let ``CE taskSeq: use 'let!' with a non-generic task`` () = 75 | let mutable value = 0 76 | 77 | taskSeq { 78 | let! unit' = (task { do value <- value + 1 }) |> Task.ignore 79 | do unit' 80 | } 81 | |> verifyEmpty 82 | |> Task.map (fun _ -> value |> should equal 1) 83 | 84 | [] 85 | let ``CE taskSeq: use 'let!' with Async`` () = 86 | let mutable value = 0 87 | 88 | taskSeq { 89 | do value <- value + 1 90 | let! _ = Async.Sleep 50 91 | do value <- value + 1 92 | } 93 | |> verifyEmpty 94 | |> Task.map (fun _ -> value |> should equal 2) 95 | 96 | [] 97 | let ``CE taskSeq: use 'let!' with Async - mutables`` () = 98 | let mutable value = 0 99 | 100 | taskSeq { 101 | do! async { value <- value + 1 } 102 | do value |> should equal 1 103 | let! x = async { return value + 1 } 104 | do x |> should equal 2 105 | do! Async.Sleep 50 106 | do! async { value <- value + 1 } 107 | do value |> should equal 2 108 | let! ret = async { return value + 1 } 109 | do value |> should equal 2 110 | do ret |> should equal 3 111 | yield x + ret // eq 5 112 | } 113 | |> TaskSeq.exactlyOne 114 | |> Task.map (should equal 5) 115 | 116 | [] 117 | let ``CE taskSeq: use 'let!' with all kinds of overloads at once`` () = 118 | let mutable value = 0 119 | 120 | // this test should be expanded in case any new overload is added 121 | // that is supported by `let!`, to ensure the high/low priority 122 | // overloads still work properly 123 | taskSeq { 124 | let! a = task { // eq 1 125 | do! Task.Delay 10 126 | do value <- value + 1 127 | return value 128 | } 129 | 130 | let! b = // eq 2 131 | task { 132 | do! Task.Delay 50 133 | do value <- value + 1 134 | return value 135 | } 136 | |> ValueTask 137 | 138 | let! c = ValueTask<_>(4) // ValueTask that completes immediately 139 | let! _ = Task.Factory.StartNew(fun () -> value <- value + 1) // non-generic Task with side effect 140 | let! d = Task.fromResult 99 // normal Task that completes immediately 141 | let! _ = Async.Sleep 0 // unit Async 142 | 143 | let! e = async { 144 | do! Async.Sleep 40 145 | do value <- value + 1 // eq 4 now 146 | return value 147 | } 148 | 149 | yield! [ a; b; c; d; e ] 150 | } 151 | |> TaskSeq.toListAsync 152 | |> Task.map (should equal [ 1; 2; 4; 99; 4 ]) 153 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.OfXXX.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.``Conversion-From`` 2 | 3 | open Xunit 4 | open FsUnit.Xunit 5 | 6 | open FSharp.Control 7 | 8 | let validateSequence sq = 9 | TaskSeq.toArrayAsync sq 10 | |> Task.map (Seq.toArray >> should equal [| 0..9 |]) 11 | 12 | module EmptySeq = 13 | [] 14 | let ``Null source is invalid`` () = 15 | // note: ofList and its variants do not have null as proper value 16 | assertNullArg <| fun () -> TaskSeq.ofAsyncArray null 17 | assertNullArg <| fun () -> TaskSeq.ofAsyncSeq null 18 | assertNullArg <| fun () -> TaskSeq.ofTaskArray null 19 | assertNullArg <| fun () -> TaskSeq.ofTaskSeq null 20 | assertNullArg <| fun () -> TaskSeq.ofResizeArray null 21 | assertNullArg <| fun () -> TaskSeq.ofArray null 22 | assertNullArg <| fun () -> TaskSeq.ofSeq null 23 | 24 | [] 25 | let ``TaskSeq-ofAsyncArray with empty set`` () = 26 | Array.init 0 (fun x -> async { return x }) 27 | |> TaskSeq.ofAsyncArray 28 | |> verifyEmpty 29 | 30 | [] 31 | let ``TaskSeq-ofAsyncList with empty set`` () = 32 | List.init 0 (fun x -> async { return x }) 33 | |> TaskSeq.ofAsyncList 34 | |> verifyEmpty 35 | 36 | [] 37 | let ``TaskSeq-ofAsyncSeq with empty set`` () = 38 | Seq.init 0 (fun x -> async { return x }) 39 | |> TaskSeq.ofAsyncSeq 40 | |> verifyEmpty 41 | 42 | [] 43 | let ``TaskSeq-ofTaskArray with empty set`` () = 44 | Array.init 0 (fun x -> task { return x }) 45 | |> TaskSeq.ofTaskArray 46 | |> verifyEmpty 47 | 48 | [] 49 | let ``TaskSeq-ofTaskList with empty set`` () = 50 | List.init 0 (fun x -> task { return x }) 51 | |> TaskSeq.ofTaskList 52 | |> verifyEmpty 53 | 54 | [] 55 | let ``TaskSeq-ofTaskSeq with empty set`` () = 56 | Seq.init 0 (fun x -> task { return x }) 57 | |> TaskSeq.ofTaskSeq 58 | |> verifyEmpty 59 | 60 | [] 61 | let ``TaskSeq-ofResizeArray with empty set`` () = ResizeArray() |> TaskSeq.ofResizeArray |> verifyEmpty 62 | 63 | [] 64 | let ``TaskSeq-ofArray with empty set`` () = Array.init 0 id |> TaskSeq.ofArray |> verifyEmpty 65 | 66 | [] 67 | let ``TaskSeq-ofList with empty set`` () = List.init 0 id |> TaskSeq.ofList |> verifyEmpty 68 | 69 | [] 70 | let ``TaskSeq-ofSeq with empty set`` () = Seq.init 0 id |> TaskSeq.ofSeq |> verifyEmpty 71 | 72 | 73 | module Immutable = 74 | [] 75 | let ``TaskSeq-ofAsyncArray should succeed`` () = 76 | Array.init 10 (fun x -> async { return x }) 77 | |> TaskSeq.ofAsyncArray 78 | |> validateSequence 79 | 80 | [] 81 | let ``TaskSeq-ofAsyncList should succeed`` () = 82 | List.init 10 (fun x -> async { return x }) 83 | |> TaskSeq.ofAsyncList 84 | |> validateSequence 85 | 86 | [] 87 | let ``TaskSeq-ofAsyncSeq should succeed`` () = 88 | Seq.init 10 (fun x -> async { return x }) 89 | |> TaskSeq.ofAsyncSeq 90 | |> validateSequence 91 | 92 | [] 93 | let ``TaskSeq-ofTaskArray should succeed`` () = 94 | Array.init 10 (fun x -> task { return x }) 95 | |> TaskSeq.ofTaskArray 96 | |> validateSequence 97 | 98 | [] 99 | let ``TaskSeq-ofTaskList should succeed`` () = 100 | List.init 10 (fun x -> task { return x }) 101 | |> TaskSeq.ofTaskList 102 | |> validateSequence 103 | 104 | [] 105 | let ``TaskSeq-ofTaskSeq should succeed`` () = 106 | Seq.init 10 (fun x -> task { return x }) 107 | |> TaskSeq.ofTaskSeq 108 | |> validateSequence 109 | 110 | [] 111 | let ``TaskSeq-ofResizeArray should succeed`` () = 112 | ResizeArray [ 0..9 ] 113 | |> TaskSeq.ofResizeArray 114 | |> validateSequence 115 | 116 | [] 117 | let ``TaskSeq-ofArray should succeed`` () = Array.init 10 id |> TaskSeq.ofArray |> validateSequence 118 | 119 | [] 120 | let ``TaskSeq-ofList should succeed`` () = List.init 10 id |> TaskSeq.ofList |> validateSequence 121 | 122 | [] 123 | let ``TaskSeq-ofSeq should succeed`` () = Seq.init 10 id |> TaskSeq.ofSeq |> validateSequence 124 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.Singleton 2 | 3 | open Xunit 4 | open FsUnit.Xunit 5 | 6 | open FSharp.Control 7 | 8 | // 9 | // TaskSeq.singleton 10 | // 11 | 12 | module EmptySeq = 13 | 14 | [)>] 15 | let ``TaskSeq-singleton with empty has length one`` variant = 16 | taskSeq { 17 | yield! TaskSeq.singleton 10 18 | yield! Gen.getEmptyVariant variant 19 | } 20 | |> TaskSeq.exactlyOne 21 | |> Task.map (should equal 10) 22 | 23 | module SideEffects = 24 | [] 25 | let ``TaskSeq-singleton with a mutable value`` () = 26 | let mutable x = 0 27 | let ts = TaskSeq.singleton x 28 | x <- x + 1 29 | 30 | // mutable value is dereferenced when passed to a function 31 | ts |> TaskSeq.exactlyOne |> Task.map (should equal 0) 32 | 33 | [] 34 | let ``TaskSeq-singleton with a ref cell`` () = 35 | let x = ref 0 36 | let ts = TaskSeq.singleton x 37 | x.Value <- x.Value + 1 38 | 39 | ts 40 | |> TaskSeq.exactlyOne 41 | |> Task.map (fun x -> x.Value |> should equal 1) 42 | 43 | module Other = 44 | [] 45 | let ``TaskSeq-singleton creates a sequence of one`` () = 46 | TaskSeq.singleton 42 47 | |> TaskSeq.exactlyOne 48 | |> Task.map (should equal 42) 49 | 50 | [] 51 | let ``TaskSeq-singleton with null as value`` () = 52 | TaskSeq.singleton null 53 | |> TaskSeq.exactlyOne 54 | |> Task.map (should be Null) 55 | 56 | [] 57 | let ``TaskSeq-singleton can be yielded multiple times`` () = 58 | let singleton = TaskSeq.singleton 42 59 | 60 | taskSeq { 61 | yield! singleton 62 | yield! singleton 63 | yield! singleton 64 | yield! singleton 65 | } 66 | |> TaskSeq.toList 67 | |> should equal [ 42; 42; 42; 42 ] 68 | 69 | [] 70 | let ``TaskSeq-singleton with isEmpty`` () = 71 | TaskSeq.singleton 42 72 | |> TaskSeq.isEmpty 73 | |> Task.map (should be False) 74 | 75 | [] 76 | let ``TaskSeq-singleton with append`` () = 77 | TaskSeq.singleton 42 78 | |> TaskSeq.append (TaskSeq.singleton 42) 79 | |> TaskSeq.toList 80 | |> should equal [ 42; 42 ] 81 | 82 | [)>] 83 | let ``TaskSeq-singleton with collect`` variant = 84 | Gen.getSeqImmutable variant 85 | |> TaskSeq.collect TaskSeq.singleton 86 | |> verify1To10 87 | 88 | [] 89 | let ``TaskSeq-singleton does not throw when getting Current before MoveNext`` () = task { 90 | let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() 91 | let defaultValue = enumerator.Current // should return the default value for int 92 | defaultValue |> should equal 0 93 | } 94 | 95 | [] 96 | let ``TaskSeq-singleton does not throw when getting Current after last MoveNext`` () = task { 97 | let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() 98 | let! isNext = enumerator.MoveNextAsync() 99 | isNext |> should be True 100 | let value = enumerator.Current // the first and only value 101 | value |> should equal 42 102 | 103 | // move past the end 104 | let! isNext = enumerator.MoveNextAsync() 105 | isNext |> should be False 106 | let defaultValue = enumerator.Current // should return the default value for int 107 | defaultValue |> should equal 0 108 | } 109 | 110 | [] 111 | let ``TaskSeq-singleton multiple MoveNext is fine`` () = task { 112 | let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() 113 | let! isNext = enumerator.MoveNextAsync() 114 | isNext |> should be True 115 | let! _ = enumerator.MoveNextAsync() 116 | let! _ = enumerator.MoveNextAsync() 117 | let! _ = enumerator.MoveNextAsync() 118 | let! isNext = enumerator.MoveNextAsync() 119 | isNext |> should be False 120 | 121 | // should return the default value for int after moving past the end 122 | let defaultValue = enumerator.Current 123 | defaultValue |> should equal 0 124 | } 125 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Skip.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.Skip 2 | 3 | open System 4 | 5 | open Xunit 6 | open FsUnit.Xunit 7 | 8 | open FSharp.Control 9 | 10 | // 11 | // TaskSeq.skip 12 | // TaskSeq.drop 13 | // 14 | 15 | exception SideEffectPastEnd of string 16 | 17 | module EmptySeq = 18 | [)>] 19 | let ``TaskSeq-skip(0) has no effect on empty input`` variant = 20 | // no `task` block needed 21 | Gen.getEmptyVariant variant |> TaskSeq.skip 0 |> verifyEmpty 22 | 23 | [)>] 24 | let ``TaskSeq-skip(1) on empty input should throw InvalidOperation`` variant = 25 | fun () -> 26 | Gen.getEmptyVariant variant 27 | |> TaskSeq.skip 1 28 | |> consumeTaskSeq 29 | 30 | |> should throwAsyncExact typeof 31 | 32 | [] 33 | let ``TaskSeq-skip(-1) should throw ArgumentException on any input`` () = 34 | fun () -> TaskSeq.empty |> TaskSeq.skip -1 |> consumeTaskSeq 35 | |> should throwAsyncExact typeof 36 | 37 | fun () -> TaskSeq.init 10 id |> TaskSeq.skip -1 |> consumeTaskSeq 38 | |> should throwAsyncExact typeof 39 | 40 | [] 41 | let ``TaskSeq-skip(-1) should throw ArgumentException before awaiting`` () = 42 | fun () -> 43 | taskSeq { 44 | do! longDelay () 45 | 46 | if false then 47 | yield 0 // type inference 48 | } 49 | |> TaskSeq.skip -1 50 | |> ignore // throws even without running the async. Bad coding, don't ignore a task! 51 | 52 | |> should throw typeof 53 | 54 | [)>] 55 | let ``TaskSeq-drop(0) has no effect on empty input`` variant = Gen.getEmptyVariant variant |> TaskSeq.drop 0 |> verifyEmpty 56 | 57 | [)>] 58 | let ``TaskSeq-drop(99) does not throw on empty input`` variant = 59 | Gen.getEmptyVariant variant 60 | |> TaskSeq.drop 99 61 | |> verifyEmpty 62 | 63 | 64 | [] 65 | let ``TaskSeq-drop(-1) should throw ArgumentException on any input`` () = 66 | fun () -> TaskSeq.empty |> TaskSeq.drop -1 |> consumeTaskSeq 67 | |> should throwAsyncExact typeof 68 | 69 | fun () -> TaskSeq.init 10 id |> TaskSeq.drop -1 |> consumeTaskSeq 70 | |> should throwAsyncExact typeof 71 | 72 | [] 73 | let ``TaskSeq-drop(-1) should throw ArgumentException before awaiting`` () = 74 | fun () -> 75 | taskSeq { 76 | do! longDelay () 77 | 78 | if false then 79 | yield 0 // type inference 80 | } 81 | |> TaskSeq.drop -1 82 | |> ignore // throws even without running the async. Bad coding, don't ignore a task! 83 | 84 | |> should throw typeof 85 | 86 | module Immutable = 87 | 88 | [)>] 89 | let ``TaskSeq-skip skips over exactly 'count' items`` variant = task { 90 | 91 | do! 92 | Gen.getSeqImmutable variant 93 | |> TaskSeq.skip 0 94 | |> verifyDigitsAsString "ABCDEFGHIJ" 95 | 96 | do! 97 | Gen.getSeqImmutable variant 98 | |> TaskSeq.skip 1 99 | |> verifyDigitsAsString "BCDEFGHIJ" 100 | 101 | do! 102 | Gen.getSeqImmutable variant 103 | |> TaskSeq.skip 5 104 | |> verifyDigitsAsString "FGHIJ" 105 | 106 | do! 107 | Gen.getSeqImmutable variant 108 | |> TaskSeq.skip 10 109 | |> verifyEmpty 110 | } 111 | 112 | [)>] 113 | let ``TaskSeq-skip throws when there are not enough elements`` variant = 114 | fun () -> TaskSeq.init 1 id |> TaskSeq.skip 2 |> consumeTaskSeq 115 | 116 | |> should throwAsyncExact typeof 117 | 118 | fun () -> 119 | Gen.getSeqImmutable variant 120 | |> TaskSeq.skip 11 121 | |> consumeTaskSeq 122 | 123 | |> should throwAsyncExact typeof 124 | 125 | fun () -> 126 | Gen.getSeqImmutable variant 127 | |> TaskSeq.skip 10_000_000 128 | |> consumeTaskSeq 129 | 130 | |> should throwAsyncExact typeof 131 | 132 | [)>] 133 | let ``TaskSeq-drop skips over at least 'count' items`` variant = task { 134 | do! 135 | Gen.getSeqImmutable variant 136 | |> TaskSeq.drop 0 137 | |> verifyDigitsAsString "ABCDEFGHIJ" 138 | 139 | do! 140 | Gen.getSeqImmutable variant 141 | |> TaskSeq.drop 1 142 | |> verifyDigitsAsString "BCDEFGHIJ" 143 | 144 | do! 145 | Gen.getSeqImmutable variant 146 | |> TaskSeq.drop 5 147 | |> verifyDigitsAsString "FGHIJ" 148 | 149 | do! 150 | Gen.getSeqImmutable variant 151 | |> TaskSeq.drop 10 152 | |> verifyEmpty 153 | 154 | do! 155 | Gen.getSeqImmutable variant 156 | |> TaskSeq.drop 11 // no exception 157 | |> verifyEmpty 158 | 159 | do! 160 | Gen.getSeqImmutable variant 161 | |> TaskSeq.drop 10_000_000 // no exception 162 | |> verifyEmpty 163 | } 164 | 165 | module SideEffects = 166 | [)>] 167 | let ``TaskSeq-skip skips over enough items`` variant = 168 | Gen.getSeqWithSideEffect variant 169 | |> TaskSeq.skip 5 170 | |> verifyDigitsAsString "FGHIJ" 171 | 172 | [)>] 173 | let ``TaskSeq-drop skips over enough items`` variant = 174 | Gen.getSeqWithSideEffect variant 175 | |> TaskSeq.drop 5 176 | |> verifyDigitsAsString "FGHIJ" 177 | 178 | [] 179 | let ``TaskSeq-skip prove we do not skip side effects`` () = task { 180 | let mutable x = 42 // for this test, the potential mutation should not actually occur 181 | 182 | let items = taskSeq { 183 | yield x 184 | yield x * 2 185 | x <- x + 1 // we are proving we never get here 186 | } 187 | 188 | let! first = items |> TaskSeq.skip 2 |> TaskSeq.toArrayAsync 189 | let! repeat = items |> TaskSeq.skip 2 |> TaskSeq.toArrayAsync 190 | 191 | first |> should equal Array.empty 192 | repeat |> should equal Array.empty 193 | x |> should equal 44 // expect: side-effect is executed twice by now 194 | } 195 | 196 | [] 197 | let ``TaskSeq-skip prove that an exception from the taskSeq is thrown instead of exception from function`` () = 198 | let items = taskSeq { 199 | yield 42 200 | yield! [ 1; 2 ] 201 | do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised 202 | } 203 | 204 | fun () -> items |> TaskSeq.skip 4 |> consumeTaskSeq // this would raise ArgumentException normally 205 | |> should throwAsyncExact typeof 206 | 207 | 208 | [] 209 | let ``TaskSeq-drop prove we do not skip side effects at the end`` () = task { 210 | let mutable x = 42 // for this test, the potential mutation should not actually occur 211 | 212 | let items = taskSeq { 213 | yield x 214 | yield x * 2 215 | x <- x + 1 // we are proving we never get here 216 | } 217 | 218 | let! first = items |> TaskSeq.drop 2 |> TaskSeq.toArrayAsync 219 | let! repeat = items |> TaskSeq.drop 2 |> TaskSeq.toArrayAsync 220 | 221 | first |> should equal Array.empty 222 | repeat |> should equal Array.empty 223 | x |> should equal 44 // expect: side-effect at end is executed twice by now 224 | } 225 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Tail.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.Tail 2 | 3 | open System 4 | 5 | open Xunit 6 | open FsUnit.Xunit 7 | 8 | open FSharp.Control 9 | 10 | // 11 | // TaskSeq.tail 12 | // TaskSeq.tryTail 13 | // 14 | 15 | module EmptySeq = 16 | [] 17 | let ``Null source is invalid`` () = 18 | assertNullArg <| fun () -> TaskSeq.tail null 19 | assertNullArg <| fun () -> TaskSeq.tryTail null 20 | 21 | [)>] 22 | let ``TaskSeq-tail throws`` variant = task { 23 | fun () -> Gen.getEmptyVariant variant |> TaskSeq.tail |> Task.ignore 24 | |> should throwAsyncExact typeof 25 | } 26 | 27 | [)>] 28 | let ``TaskSeq-tryTail returns None`` variant = task { 29 | let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryTail 30 | nothing |> should be None' 31 | } 32 | 33 | [] 34 | let ``TaskSeq-tail executes side effect`` () = task { 35 | let mutable x = 0 36 | 37 | fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.tail |> Task.ignore 38 | |> should throwAsyncExact typeof 39 | 40 | // side effect must have run! 41 | x |> should equal 1 42 | } 43 | 44 | [] 45 | let ``TaskSeq-tryTail executes side effect`` () = task { 46 | let mutable x = 0 47 | 48 | let! nothing = taskSeq { do x <- x + 1 } |> TaskSeq.tryTail 49 | nothing |> should be None' 50 | 51 | // side effect must have run! 52 | x |> should equal 1 53 | } 54 | 55 | 56 | module Immutable = 57 | let verifyTail tail = 58 | tail 59 | |> TaskSeq.toArrayAsync 60 | |> Task.map (should equal [| 2..10 |]) 61 | 62 | [)>] 63 | let ``TaskSeq-tail gets the tail items`` variant = task { 64 | let ts = Gen.getSeqImmutable variant 65 | 66 | let! tail = TaskSeq.tail ts 67 | do! verifyTail tail 68 | 69 | let! tail = TaskSeq.tail ts //immutable, so re-iteration does not change outcome 70 | do! verifyTail tail 71 | } 72 | 73 | [)>] 74 | let ``TaskSeq-tryTail gets the tail item`` variant = task { 75 | let ts = Gen.getSeqImmutable variant 76 | 77 | match! TaskSeq.tryTail ts with 78 | | Some tail -> do! verifyTail tail 79 | | x -> do x |> should not' (be None') 80 | 81 | } 82 | 83 | [] 84 | let ``TaskSeq-tail return empty from a singleton sequence`` () = task { 85 | let ts = taskSeq { yield 42 } 86 | 87 | let! tail = TaskSeq.tail ts 88 | do! verifyEmpty tail 89 | } 90 | 91 | [] 92 | let ``TaskSeq-tryTail gets the only item in a singleton sequence`` () = task { 93 | let ts = taskSeq { yield 42 } 94 | 95 | match! TaskSeq.tryTail ts with 96 | | Some tail -> do! verifyEmpty tail 97 | | x -> do x |> should not' (be None') 98 | } 99 | 100 | 101 | module SideEffects = 102 | [] 103 | let ``TaskSeq-tail does not execute side effect after the first item in singleton`` () = task { 104 | let mutable x = 42 105 | 106 | let one = taskSeq { 107 | yield x 108 | x <- x + 1 // <--- we should never get here 109 | } 110 | 111 | let! _ = one |> TaskSeq.tail 112 | let! _ = one |> TaskSeq.tail // side effect, re-iterating! 113 | 114 | x |> should equal 42 115 | } 116 | 117 | [] 118 | let ``TaskSeq-tryTail does not execute execute side effect after first item in singleton`` () = task { 119 | let mutable x = 42 120 | 121 | let one = taskSeq { 122 | yield x 123 | x <- x + 1 // <--- we should never get here 124 | } 125 | 126 | let! _ = one |> TaskSeq.tryTail 127 | let! _ = one |> TaskSeq.tryTail 128 | 129 | // side effect, reiterating causes it to execute again! 130 | x |> should equal 42 131 | 132 | } 133 | 134 | [] 135 | let ``TaskSeq-tail executes side effect partially`` () = task { 136 | let mutable x = 42 137 | 138 | let ts = taskSeq { 139 | x <- x + 1 // <--- executed on tail, but not materializing rest 140 | yield 1 141 | x <- x + 1 // <--- not executed on tail, but on materializing rest 142 | yield 2 143 | x <- x + 1 // <--- id 144 | } 145 | 146 | let! tail1 = ts |> TaskSeq.tail 147 | x |> should equal 43 // test side effect runs 1x 148 | 149 | let! tail2 = ts |> TaskSeq.tail 150 | x |> should equal 44 // test side effect ran again only 1x 151 | 152 | let! len = TaskSeq.length tail1 153 | x |> should equal 46 // now 2nd & 3rd side effect runs, but not the first 154 | len |> should equal 1 155 | 156 | let! len = TaskSeq.length tail2 157 | x |> should equal 48 // now again 2nd & 3rd side effect runs, but not the first 158 | len |> should equal 1 159 | } 160 | 161 | [] 162 | let ``TaskSeq-tryTail executes side effect partially`` () = task { 163 | let mutable x = 42 164 | 165 | let ts = taskSeq { 166 | x <- x + 1 // <--- executed on tail, but not materializing rest 167 | yield 1 168 | x <- x + 1 // <--- not executed on tail, but on materializing rest 169 | yield 2 170 | x <- x + 1 // <--- id 171 | } 172 | 173 | let! tail1 = ts |> TaskSeq.tryTail 174 | x |> should equal 43 // test side effect runs 1x 175 | 176 | let! tail2 = ts |> TaskSeq.tryTail 177 | x |> should equal 44 // test side effect ran again only 1x 178 | 179 | let! len = TaskSeq.length tail1.Value 180 | x |> should equal 46 // now 2nd side effect runs, but not the first 181 | len |> should equal 1 182 | 183 | let! len = TaskSeq.length tail2.Value 184 | x |> should equal 48 // now again 2nd side effect runs, but not the first 185 | len |> should equal 1 186 | } 187 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.TaskExtensions.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.TaskExtensions 2 | 3 | open Xunit 4 | open FsUnit.Xunit 5 | 6 | open FSharp.Control 7 | 8 | // 9 | // Task extensions 10 | // 11 | 12 | module EmptySeq = 13 | [)>] 14 | let ``Task-for CE with empty taskSeq`` variant = task { 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 ``Task-for CE must execute side effect in empty taskSeq`` () = task { 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 | module Immutable = 37 | [)>] 38 | let ``Task-for CE with taskSeq`` variant = task { 39 | let values = Gen.getSeqImmutable variant 40 | 41 | let mutable sum = 0 42 | 43 | for x in values do 44 | sum <- sum + x 45 | 46 | sum |> should equal 55 47 | } 48 | 49 | [)>] 50 | let ``Task-for CE with taskSeq multiple iterations`` variant = task { 51 | let values = Gen.getSeqImmutable variant 52 | 53 | let mutable sum = 0 54 | 55 | for x in values do 56 | sum <- sum + x 57 | 58 | // each following iteration should start at the beginning 59 | for x in values do 60 | sum <- sum + x 61 | 62 | for x in values do 63 | sum <- sum + x 64 | 65 | sum |> should equal 165 66 | } 67 | 68 | [] 69 | let ``Task-for mixing both types of for loops`` () = async { 70 | // this test ensures overload resolution is correct 71 | let ts = TaskSeq.singleton 20 72 | let sq = Seq.singleton 20 73 | let mutable sum = 2 74 | 75 | for x in ts do 76 | sum <- sum + x 77 | 78 | for x in sq do 79 | sum <- sum + x 80 | 81 | sum |> should equal 42 82 | } 83 | 84 | module SideEffects = 85 | [)>] 86 | let ``Task-for CE with taskSeq`` variant = task { 87 | let values = Gen.getSeqWithSideEffect variant 88 | 89 | let mutable sum = 0 90 | 91 | for x in values do 92 | sum <- sum + x 93 | 94 | sum |> should equal 55 95 | } 96 | 97 | [)>] 98 | let ``Task-for CE with taskSeq multiple iterations`` variant = task { 99 | let values = Gen.getSeqWithSideEffect variant 100 | 101 | let mutable sum = 0 102 | 103 | for x in values do 104 | sum <- sum + x 105 | 106 | // each following iteration should start at the beginning 107 | // with the "side effect" tests, the mutable state updates 108 | for x in values do 109 | sum <- sum + x // starts at 11 110 | 111 | for x in values do 112 | sum <- sum + x // starts at 21 113 | 114 | sum |> should equal 465 // eq to: List.sum [1..30] 115 | } 116 | 117 | module Other = 118 | [] 119 | let ``Task-for CE must call dispose in empty taskSeq`` () = async { 120 | let disposed = ref 0 121 | let values = Gen.getEmptyDisposableTaskSeq disposed 122 | 123 | for _ in values do 124 | () 125 | 126 | // the DisposeAsync should be called by now 127 | disposed.Value |> should equal 1 128 | } 129 | 130 | [] 131 | let ``Task-for CE must call dispose on singleton`` () = async { 132 | let disposed = ref 0 133 | let mutable sum = 0 134 | let values = Gen.getSingletonDisposableTaskSeq disposed 135 | 136 | for x in values do 137 | sum <- x 138 | 139 | // the DisposeAsync should be called by now 140 | disposed.Value |> should equal 1 141 | sum |> should equal 42 142 | } 143 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Tests.CE.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.``taskSeq Computation Expression`` 2 | 3 | open System 4 | 5 | open Xunit 6 | open FsUnit.Xunit 7 | 8 | open FSharp.Control 9 | 10 | [] 11 | let ``CE taskSeq using yield! with null`` () = task { 12 | let ts = taskSeq { 13 | yield! Gen.sideEffectTaskSeq 10 14 | yield! (null: TaskSeq) 15 | } 16 | 17 | fun () -> TaskSeq.toList ts 18 | |> assertThrows typeof 19 | } 20 | 21 | [] 22 | let ``CE taskSeq with several yield!`` () = task { 23 | let tskSeq = taskSeq { 24 | yield! Gen.sideEffectTaskSeq 10 25 | yield! Gen.sideEffectTaskSeq 5 26 | yield! Gen.sideEffectTaskSeq 10 27 | yield! Gen.sideEffectTaskSeq 5 28 | } 29 | 30 | let! data = tskSeq |> TaskSeq.toListAsync 31 | 32 | data 33 | |> should equal (List.concat [ [ 1..10 ]; [ 1..5 ]; [ 1..10 ]; [ 1..5 ] ]) 34 | } 35 | 36 | [] 37 | let ``CE taskSeq with nested yield!`` () = task { 38 | let control = seq { 39 | yield! [ 1..10 ] 40 | 41 | for _ in 0..9 do 42 | yield! [ 1..2 ] 43 | 44 | for _ in 0..2 do 45 | yield! seq { yield 42 } 46 | 47 | for i in 100..102 do 48 | yield! seq { yield! seq { yield i } } 49 | } 50 | 51 | let tskSeq = taskSeq { 52 | yield! Gen.sideEffectTaskSeq 10 53 | 54 | for _ in 0..9 do 55 | yield! Gen.sideEffectTaskSeq 2 56 | 57 | for _ in 0..2 do 58 | yield! taskSeq { yield 42 } 59 | 60 | for i in 100..102 do 61 | yield! taskSeq { yield! taskSeq { yield i } } 62 | } 63 | 64 | let! data = tskSeq |> TaskSeq.toListAsync 65 | 66 | data |> should equal (List.ofSeq control) 67 | data |> should haveLength 150 68 | } 69 | 70 | [] 71 | let ``CE taskSeq with nested deeply yield! perf test 8521 nested tasks`` () = task { 72 | let expected = seq { 73 | yield! [ 1..10 ] 74 | yield! Seq.concat <| Seq.init 4251 (fun _ -> [ 1; 2 ]) 75 | } 76 | 77 | let createTasks = Gen.sideEffectTaskSeqMicro 1L<µs> 10L<µs> 78 | // 79 | // NOTES: it appears that deeply nesting adds to performance degradation, need to benchmark/profile this 80 | // probable cause: since this is *fast* with DirectTask, the reason is likely the way the Task.Delay causes 81 | // *many* subtasks to be delayed, resulting in exponential delay. Reason: max accuracy of Delay is about 15ms (!) 82 | // 83 | // RESOLUTION: seems to have been caused by erratic Task.Delay which has only a 15ms resolution 84 | // 85 | 86 | let tskSeq = taskSeq { 87 | yield! createTasks 10 88 | 89 | // nestings amount to 8512 sequences of [1;2] 90 | for _ in 0..2 do 91 | yield! createTasks 2 92 | 93 | for _ in 0..2 do 94 | yield! createTasks 2 95 | 96 | for _ in 0..2 do 97 | yield! createTasks 2 98 | 99 | for _ in 0..2 do 100 | yield! createTasks 2 101 | 102 | for _ in 0..2 do 103 | yield! createTasks 2 104 | 105 | for _ in 0..2 do 106 | yield! createTasks 2 107 | 108 | for _ in 0..2 do 109 | yield! createTasks 2 110 | 111 | for _ in 0..2 do 112 | yield! createTasks 2 113 | 114 | for _ in 0..2 do 115 | yield! createTasks 2 116 | 117 | yield! TaskSeq.empty 118 | } 119 | 120 | let! data = tskSeq |> TaskSeq.toListAsync 121 | data |> List.length |> should equal 8512 122 | data |> should equal (List.ofSeq expected) // cannot compare seq this way, so, List.ofSeq it is 123 | } 124 | 125 | [] 126 | let ``CE taskSeq with mixing yield! and yield`` () = task { 127 | let tskSeq = taskSeq { 128 | yield! Gen.sideEffectTaskSeq 10 129 | yield 42 130 | yield! Gen.sideEffectTaskSeq 5 131 | yield 42 132 | yield! Gen.sideEffectTaskSeq 10 133 | yield 42 134 | yield! Gen.sideEffectTaskSeq 5 135 | } 136 | 137 | let! data = tskSeq |> TaskSeq.toListAsync 138 | 139 | data 140 | |> should equal (List.concat [ [ 1..10 ]; [ 42 ]; [ 1..5 ]; [ 42 ]; [ 1..10 ]; [ 42 ]; [ 1..5 ] ]) 141 | } 142 | 143 | [] 144 | let ``CE taskSeq: 1000 TaskDelay-delayed tasks using yield!`` () = task { 145 | // Smoke performance test 146 | // Runs in slightly over half a second (average of spin-wait, plus small overhead) 147 | // should generally be about as fast as `task`, see below for equivalent test. 148 | let tskSeq = taskSeq { yield! Gen.sideEffectTaskSeqMicro 50L<µs> 1000L<µs> 1000 } 149 | let! data = tskSeq |> TaskSeq.toListAsync 150 | data |> should equal [ 1..1000 ] 151 | } 152 | 153 | [] 154 | let ``CE taskSeq: 1000 sync-running tasks using yield!`` () = task { 155 | // Smoke performance test 156 | // Runs in a few 10's of ms, because of absence of Task.Delay 157 | // should generally be about as fast as `task`, see below 158 | let tskSeq = taskSeq { yield! Gen.sideEffectTaskSeq_Sequential 1000 } 159 | let! data = tskSeq |> TaskSeq.toListAsync 160 | data |> should equal [ 1..1000 ] 161 | } 162 | 163 | [] 164 | let ``CE taskSeq: 5000 sync-running tasks using yield!`` () = task { 165 | // Smoke performance test 166 | // Compare with task-ce test below. Uses a no-delay hot-started sequence of tasks. 167 | let tskSeq = taskSeq { yield! Gen.sideEffectTaskSeq_Sequential 5000 } 168 | let! data = tskSeq |> TaskSeq.toListAsync 169 | data |> should equal [ 1..5000 ] 170 | } 171 | 172 | [] 173 | let ``CE task: 1000 TaskDelay-delayed tasks using for-loop`` () = task { 174 | // Uses SpinWait for effective task-delaying 175 | // for smoke-test comparison with taskSeq 176 | let tasks = DummyTaskFactory(50L<µs>, 1000L<µs>).CreateDelayedTasks_SideEffect 1000 177 | let mutable i = 0 178 | 179 | for t in tasks do 180 | i <- i + 1 181 | do! t () |> Task.ignore 182 | 183 | i |> should equal 1000 184 | } 185 | 186 | [] 187 | let ``CE task: 1000 list of sync-running tasks using for-loop`` () = task { 188 | // runs in a few 10's of ms, because of absence of Task.Delay 189 | // for smoke-test comparison with taskSeq 190 | let tasks = DummyTaskFactory().CreateDirectTasks_SideEffect 1000 191 | let mutable i = 0 192 | 193 | for t in tasks do 194 | i <- i + 1 195 | do! t () |> Task.ignore 196 | 197 | i |> should equal 1000 198 | } 199 | 200 | [] 201 | let ``CE task: 5000 list of sync-running tasks using for-loop`` () = task { 202 | // runs in a few 100's of ms, because of absence of Task.Delay 203 | // for smoke-test comparison with taskSeq 204 | let tasks = DummyTaskFactory().CreateDirectTasks_SideEffect 5000 205 | let mutable i = 0 206 | 207 | for t in tasks do 208 | i <- i + 1 209 | do! t () |> Task.ignore 210 | 211 | i |> should equal 5000 212 | } 213 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.Using 2 | 3 | open System 4 | open System.Threading.Tasks 5 | 6 | open FSharp.Control 7 | open FsUnit 8 | open Xunit 9 | 10 | 11 | type private OneGetter() = 12 | member _.Get1() = 1 13 | 14 | type private Disposable(disposed: bool ref) = 15 | inherit OneGetter() 16 | 17 | interface IDisposable with 18 | member _.Dispose() = disposed.Value <- true 19 | 20 | type private AsyncDisposable(disposed: bool ref) = 21 | inherit OneGetter() 22 | 23 | interface IAsyncDisposable with 24 | member _.DisposeAsync() = ValueTask(task { do disposed.Value <- true }) 25 | 26 | type private MultiDispose(disposed: int ref) = 27 | inherit OneGetter() 28 | 29 | interface IDisposable with 30 | member _.Dispose() = disposed.Value <- 1 31 | 32 | interface IAsyncDisposable with 33 | member _.DisposeAsync() = ValueTask(task { do disposed.Value <- -1 }) 34 | 35 | let private check = TaskSeq.length >> Task.map (should equal 1) 36 | 37 | [] 38 | let ``CE taskSeq: Using when type implements IDisposable`` () = 39 | let disposed = ref false 40 | 41 | let ts = taskSeq { 42 | use x = new Disposable(disposed) 43 | yield x.Get1() 44 | } 45 | 46 | check ts 47 | |> Task.map (fun _ -> disposed.Value |> should be True) 48 | 49 | [] 50 | let ``CE taskSeq: Using when type implements IAsyncDisposable`` () = 51 | let disposed = ref false 52 | 53 | let ts = taskSeq { 54 | use x = AsyncDisposable(disposed) 55 | yield x.Get1() 56 | } 57 | 58 | check ts 59 | |> Task.map (fun _ -> disposed.Value |> should be True) 60 | 61 | [] 62 | let ``CE taskSeq: Using when type implements IDisposable and IAsyncDisposable`` () = 63 | let disposed = ref 0 64 | 65 | let ts = taskSeq { 66 | use x = new MultiDispose(disposed) // Used to fail to compile (see #97) 67 | yield x.Get1() 68 | } 69 | 70 | check ts 71 | |> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1 72 | 73 | [] 74 | let ``CE taskSeq: Using! when type implements IDisposable`` () = 75 | let disposed = ref false 76 | 77 | let ts = taskSeq { 78 | use! x = task { return new Disposable(disposed) } 79 | yield x.Get1() 80 | } 81 | 82 | check ts 83 | |> Task.map (fun _ -> disposed.Value |> should be True) 84 | 85 | [] 86 | let ``CE taskSeq: Using! when type implements IAsyncDisposable`` () = 87 | let disposed = ref false 88 | 89 | let ts = taskSeq { 90 | use! x = task { return AsyncDisposable(disposed) } 91 | yield x.Get1() 92 | } 93 | 94 | check ts 95 | |> Task.map (fun _ -> disposed.Value |> should be True) 96 | 97 | [] 98 | let ``CE taskSeq: Using! when type implements IDisposable and IAsyncDisposable`` () = 99 | let disposed = ref 0 100 | 101 | let ts = taskSeq { 102 | use! x = task { return new MultiDispose(disposed) } // Used to fail to compile (see #97) 103 | yield x.Get1() 104 | } 105 | 106 | check ts 107 | |> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1 108 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs: -------------------------------------------------------------------------------- 1 | module TaskSeq.Tests.Zip 2 | 3 | open Xunit 4 | open FsUnit.Xunit 5 | 6 | open FSharp.Control 7 | 8 | // 9 | // TaskSeq.zip 10 | // 11 | 12 | module EmptySeq = 13 | [] 14 | let ``Null source is invalid`` () = 15 | assertNullArg <| fun () -> TaskSeq.zip null TaskSeq.empty 16 | assertNullArg <| fun () -> TaskSeq.zip TaskSeq.empty null 17 | assertNullArg <| fun () -> TaskSeq.zip null null 18 | 19 | [)>] 20 | let ``TaskSeq-zip can zip empty sequences v1`` variant = 21 | TaskSeq.zip (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) 22 | |> verifyEmpty 23 | 24 | [)>] 25 | let ``TaskSeq-zip can zip empty sequences v2`` variant = 26 | TaskSeq.zip TaskSeq.empty (Gen.getEmptyVariant variant) 27 | |> verifyEmpty 28 | 29 | [)>] 30 | let ``TaskSeq-zip can zip empty sequences v3`` variant = 31 | TaskSeq.zip (Gen.getEmptyVariant variant) TaskSeq.empty 32 | |> verifyEmpty 33 | 34 | 35 | module Immutable = 36 | [)>] 37 | let ``TaskSeq-zip zips in correct order`` variant = task { 38 | let one = Gen.getSeqImmutable variant 39 | let two = Gen.getSeqImmutable variant 40 | let combined = TaskSeq.zip one two 41 | let! combined = TaskSeq.toArrayAsync combined 42 | 43 | combined 44 | |> Array.forall (fun (x, y) -> x = y) 45 | |> should be True 46 | 47 | combined |> should be (haveLength 10) 48 | 49 | combined 50 | |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) 51 | } 52 | 53 | [)>] 54 | let ``TaskSeq-zip zips in correct order for differently delayed sequences`` variant = task { 55 | let one = Gen.getSeqImmutable variant 56 | let two = taskSeq { yield! [ 1..10 ] } 57 | let combined = TaskSeq.zip one two 58 | let! combined = TaskSeq.toArrayAsync combined 59 | 60 | combined 61 | |> Array.forall (fun (x, y) -> x = y) 62 | |> should be True 63 | 64 | combined |> should be (haveLength 10) 65 | 66 | combined 67 | |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) 68 | } 69 | 70 | module SideEffects = 71 | [)>] 72 | let ``TaskSeq-zip zips can deal with side effects in sequences`` variant = task { 73 | let one = Gen.getSeqWithSideEffect variant 74 | let two = Gen.getSeqWithSideEffect variant 75 | let combined = TaskSeq.zip one two 76 | let! combined = TaskSeq.toArrayAsync combined 77 | 78 | combined 79 | |> Array.forall (fun (x, y) -> x = y) 80 | |> should be True 81 | 82 | combined |> should be (haveLength 10) 83 | 84 | combined 85 | |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) 86 | } 87 | 88 | [)>] 89 | let ``TaskSeq-zip zips combine a side-effect-free, and a side-effect-full sequence`` variant = task { 90 | let one = Gen.getSeqWithSideEffect variant 91 | let two = taskSeq { yield! [ 1..10 ] } 92 | let combined = TaskSeq.zip one two 93 | let! combined = TaskSeq.toArrayAsync combined 94 | 95 | combined 96 | |> Array.forall (fun (x, y) -> x = y) 97 | |> should be True 98 | 99 | combined |> should be (haveLength 10) 100 | 101 | combined 102 | |> should equal (Array.init 10 (fun x -> x + 1, x + 1)) 103 | } 104 | 105 | module Performance = 106 | [] 107 | let ``TaskSeq-zip zips large sequences just fine`` length = task { 108 | let one = Gen.sideEffectTaskSeqMicro 10L<µs> 50L<µs> length 109 | let two = Gen.sideEffectTaskSeq_Sequential length 110 | let combined = TaskSeq.zip one two 111 | let! combined = TaskSeq.toArrayAsync combined 112 | 113 | combined 114 | |> Array.forall (fun (x, y) -> x = y) 115 | |> should be True 116 | 117 | combined |> should be (haveLength length) 118 | combined |> Array.last |> should equal (length, length) 119 | } 120 | 121 | module Other = 122 | [] 123 | let ``TaskSeq-zip zips different types`` () = task { 124 | let one = taskSeq { 125 | yield "one" 126 | yield "two" 127 | } 128 | 129 | let two = taskSeq { 130 | yield 42L 131 | yield 43L 132 | } 133 | 134 | let combined = TaskSeq.zip one two 135 | let! combined = TaskSeq.toArrayAsync combined 136 | 137 | combined |> should equal [| ("one", 42L); ("two", 43L) |] 138 | } 139 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/Traces/TRACE_FAIL 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt: -------------------------------------------------------------------------------- 1 | at AfterCode<_, _>, after F# inits the sm, and we can attach extra info 2 | MoveNextAsync... 3 | at MoveNextAsync: normal resumption scenario 4 | at MoveNextAsync: start calling builder.MoveNext() 5 | Resuming at resumption point 0 6 | at Run.MoveNext start 7 | at Bind 8 | at Bind: with __stack_fin = false 9 | at Bind: calling AwaitUnsafeOnCompleted 10 | at Run.MoveNext, __stack_code_fin=False 11 | at Run.MoveNext, await 12 | at MoveNextAsync: done calling builder.MoveNext() 13 | at MoveNextAsyncResult: case pending/faulted/cancelled... 14 | at Bind: with __stack_fin = true 15 | at Bind: with getting result from awaiter 16 | at Bind: calling continuation 17 | at Bind 18 | at Bind: with __stack_fin = false 19 | at Bind: calling AwaitUnsafeOnCompleted 20 | at Run.MoveNext, __stack_code_fin=False 21 | at Run.MoveNext, await 22 | at Bind: with __stack_fin = true 23 | at Bind: with getting result from awaiter 24 | at Bind: calling continuation 25 | at Bind 26 | at Bind: with __stack_fin = false 27 | at Bind: calling AwaitUnsafeOnCompleted 28 | at Run.MoveNext, __stack_code_fin=False 29 | at Run.MoveNext, await 30 | at Bind: with __stack_fin = true 31 | at Bind: with getting result from awaiter 32 | at Bind: calling continuation 33 | at Zero() 34 | at Run.MoveNext, __stack_code_fin=True 35 | at Run.MoveNext, done 36 | Getting result for token on 'None' branch, status: Succeeded 37 | GetAsyncEnumerator, cloning... 38 | MoveNextAsync... 39 | at MoveNextAsync: normal resumption scenario 40 | at MoveNextAsync: start calling builder.MoveNext() 41 | at Bind: with __stack_fin = true 42 | at Bind: with getting result from awaiter 43 | Setting exception of PromiseOfValueOrEnd to: Object reference not set to an instance of an object. 44 | at MoveNextAsync: done calling builder.MoveNext() 45 | at MoveNextAsyncResult: case pending/faulted/cancelled... 46 | Getting result for token on 'None' branch, status: Faulted 47 | Error 'Object reference not set to an instance of an object.' for token: 2 48 | DisposeAsync... 49 | DisposeAsync... 50 | Setting exception of PromiseOfValueOrEnd to: An attempt was made to transition a task to a final state when it had already completed. 51 | System.NullReferenceException: Object reference not set to an instance of an object. 52 | at FSharpy.Tests.Bug #42 -- synchronous.CE empty taskSeq\, GetAsyncEnumerator - MoveNextAsync multiple times@54.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TaskSeq.StateTransitionBug.Tests.CE.fs:line 62 53 | at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 264 54 | --- End of stack trace from previous location --- 55 | at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48 56 | at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90 -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/Traces/TRACE_SUCCESS 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt: -------------------------------------------------------------------------------- 1 | at AfterCode<_, _>, after F# inits the sm, and we can attach extra info 2 | MoveNextAsync... 3 | at MoveNextAsync: normal resumption scenario 4 | at MoveNextAsync: start calling builder.MoveNext() 5 | Resuming at resumption point 0 6 | at Run.MoveNext start 7 | at Bind 8 | at Bind: with __stack_fin = false 9 | at Bind: calling AwaitUnsafeOnCompleted 10 | at Run.MoveNext, __stack_code_fin=False 11 | at Run.MoveNext, await 12 | at MoveNextAsync: done calling builder.MoveNext() 13 | at MoveNextAsyncResult: case pending/faulted/cancelled... 14 | at Bind: with __stack_fin = true 15 | at Bind: with getting result from awaiter 16 | at Bind: calling continuation 17 | at Bind 18 | at Bind: with __stack_fin = false 19 | at Bind: calling AwaitUnsafeOnCompleted 20 | at Run.MoveNext, __stack_code_fin=False 21 | at Run.MoveNext, await 22 | at Bind: with __stack_fin = true 23 | at Bind: with getting result from awaiter 24 | at Bind: calling continuation 25 | at Bind 26 | at Bind: with __stack_fin = false 27 | at Bind: calling AwaitUnsafeOnCompleted 28 | at Run.MoveNext, __stack_code_fin=False 29 | at Run.MoveNext, await 30 | at Bind: with __stack_fin = true 31 | at Bind: with getting result from awaiter 32 | at Bind: calling continuation 33 | at Zero() 34 | at Run.MoveNext, __stack_code_fin=True 35 | at Run.MoveNext, done 36 | Getting result for token on 'None' branch, status: Succeeded 37 | GetAsyncEnumerator, cloning... 38 | MoveNextAsync... 39 | at MoveNextAsync: completed = true 40 | MoveNextAsync... 41 | at MoveNextAsync: normal resumption scenario 42 | at MoveNextAsync: start calling builder.MoveNext() 43 | Resuming at resumption point 0 44 | at Run.MoveNext start 45 | at Bind 46 | at Bind: with __stack_fin = false 47 | at Bind: calling AwaitUnsafeOnCompleted 48 | at Run.MoveNext, __stack_code_fin=False 49 | at Run.MoveNext, await 50 | at MoveNextAsync: done calling builder.MoveNext() 51 | at MoveNextAsyncResult: case pending/faulted/cancelled... 52 | at Bind: with __stack_fin = true 53 | at Bind: with getting result from awaiter 54 | at Bind: calling continuation 55 | at Bind 56 | at Bind: with __stack_fin = false 57 | at Bind: calling AwaitUnsafeOnCompleted 58 | at Run.MoveNext, __stack_code_fin=False 59 | at Run.MoveNext, await 60 | at Bind: with __stack_fin = true 61 | at Bind: with getting result from awaiter 62 | at Bind: calling continuation 63 | at Bind 64 | at Bind: with __stack_fin = false 65 | at Bind: calling AwaitUnsafeOnCompleted 66 | at Run.MoveNext, __stack_code_fin=False 67 | at Run.MoveNext, await 68 | at Bind: with __stack_fin = true 69 | at Bind: with getting result from awaiter 70 | at Bind: calling continuation 71 | at Zero() 72 | at Run.MoveNext, __stack_code_fin=True 73 | at Run.MoveNext, done 74 | Getting result for token on 'None' branch, status: Succeeded 75 | DisposeAsync... 76 | DisposeAsync... -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/Xunit.Extensions.fs: -------------------------------------------------------------------------------- 1 | namespace TaskSeq.Tests 2 | 3 | open System 4 | open System.Threading.Tasks 5 | 6 | open FsUnit 7 | open NHamcrest.Core 8 | open Microsoft.FSharp.Reflection 9 | 10 | open Xunit 11 | open Xunit.Sdk 12 | 13 | 14 | [] 15 | module ExtraCustomMatchers = 16 | /// Tee operator, combine multiple FsUnit-style test assertions: 17 | /// x |>> should be (greaterThan 12) |> should be (lessThan 42) 18 | let (|>>) x sideEffect = 19 | sideEffect x |> ignore 20 | x 21 | 22 | let private baseResultTypeTest value = 23 | match value with 24 | | null -> 25 | EqualException.ForMismatchedValues("Result type", "", "Value or None is never Result.Ok or Result.Error") 26 | |> raise 27 | 28 | | _ -> 29 | let ty = value.GetType() 30 | 31 | if ty.FullName.StartsWith "Microsoft.FSharp.Core.FSharpResult" then 32 | FSharpValue.GetUnionFields(value, ty) |> fst 33 | else 34 | EqualException.ForMismatchedValues("Result type", ty.Name, "Type must be Result<_, _>") 35 | |> raise 36 | 37 | let private baseOptionTypeTest value = 38 | match value with 39 | | null -> 40 | // An option type interpreted as obj will be for None 41 | None 42 | 43 | | _ -> 44 | let ty = value.GetType() 45 | 46 | if ty.FullName.StartsWith "Microsoft.FSharp.Core.FSharpOption" then 47 | match (FSharpValue.GetUnionFields(value, ty) |> fst).Name with 48 | | "Some" -> Some() 49 | | "None" -> None 50 | | _ -> 51 | raise 52 | <| EqualException.ForMismatchedValues("Option type", ty.Name, "Unexpected field name for F# option type") 53 | else 54 | EqualException.ForMismatchedValues("Option type", ty.Name, "Type must be Option<_>") 55 | |> raise 56 | 57 | 58 | /// Type must be Result, value must be Result.Ok. Use with `not`` only succeeds if using the correct type. 59 | let Ok' = 60 | let check value = 61 | let field = baseResultTypeTest value 62 | 63 | match field.Name with 64 | | "Ok" -> true 65 | | _ -> false 66 | 67 | CustomMatcher("Result.Ok", check) 68 | 69 | /// Type must be Result, value must be Result.Error. Use with `not`` only succeeds if using the correct type. 70 | let Error' = 71 | let check value = 72 | let field = baseResultTypeTest value 73 | 74 | match field.Name with 75 | | "Error" -> true 76 | | _ -> false 77 | 78 | CustomMatcher("Result.Error", check) 79 | 80 | /// Succeeds for None or 81 | let None' = 82 | let check value = 83 | baseOptionTypeTest value 84 | |> Option.map (fun _ -> false) 85 | |> Option.defaultValue true 86 | 87 | CustomMatcher("Option.None", check) 88 | 89 | /// Succeeds for any value Some. Use with `not`` only succeeds if using the correct type. 90 | let Some' = 91 | let check value = 92 | baseOptionTypeTest (unbox value) 93 | |> Option.map (fun _ -> true) 94 | |> Option.defaultValue false 95 | 96 | CustomMatcher("Option.Some", check) 97 | 98 | 99 | /// Succeeds if item-under-test contains any of the items in the sequence 100 | let anyOf (lst: 'T seq) = 101 | CustomMatcher($"anyOf: %A{lst}", (fun item -> lst |> Seq.contains (item :?> 'T))) 102 | 103 | /// 104 | /// Asserts any exception that matches, or is derived from the given exception . 105 | /// Async exceptions are almost always nested in an , however, in an 106 | /// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and 107 | /// in cases where we just call , an will be raised regardless. 108 | /// This assertion will go over all nested exceptions and 'self', to find a matching exception. 109 | /// Function to evaluate MUST return a , not a generic 110 | /// . 111 | /// Calls of xUnit to ensure proper evaluation of async. 112 | /// 113 | let throwAsync (ex: Type) = 114 | let testForThrowing (fn: unit -> Task) = task { 115 | let! actualExn = Assert.ThrowsAnyAsync fn 116 | 117 | match actualExn with 118 | | :? AggregateException as aggregateEx -> 119 | if Object.ReferenceEquals(ex, typeof) then 120 | // in case the assertion is for AggregateException itself, just accept it as Passed. 121 | return true 122 | 123 | else 124 | for ty in aggregateEx.InnerExceptions do 125 | Assert.IsAssignableFrom(expectedType = ex, object = ty) 126 | 127 | //Assert.Contains(expected = ex, collection = types) 128 | return true // keep FsUnit happy 129 | | _ -> 130 | // checks if object is of a certain type 131 | Assert.IsAssignableFrom(ex, actualExn) 132 | return true //keep FsUnit happy 133 | } 134 | 135 | CustomMatcher( 136 | $"Throws %s{ex.Name} (Below, XUnit does not show actual value properly)", 137 | (fun fn -> (testForThrowing (fn :?> unit -> Task)).Result) 138 | ) 139 | 140 | /// 141 | /// This makes a test BLOCKING!!! (TODO: get a better test framework?) 142 | /// 143 | /// Asserts any exception that exactly matches the given exception . 144 | /// Async exceptions are almost always nested in an , however, in an 145 | /// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and 146 | /// in cases where we just call , and will be raised regardless. 147 | /// This assertion will go over all nested exceptions and 'self', to find a matching exception. 148 | /// 149 | /// Function to evaluate MUST return a , not a generic 150 | /// . 151 | /// Calls of xUnit to ensure proper evaluation of async. 152 | /// 153 | let throwAsyncExact (ex: Type) = 154 | let testForThrowing (fn: unit -> Task) = task { 155 | let! actualExn = Assert.ThrowsAnyAsync fn 156 | 157 | match actualExn with 158 | | :? AggregateException as aggregateEx -> 159 | let types = 160 | aggregateEx.InnerExceptions 161 | |> Seq.map (fun x -> x.GetType()) 162 | 163 | Assert.Contains(expected = ex, collection = types) 164 | return true // keep FsUnit happy 165 | | _ -> 166 | // checks if object is of a certain type 167 | Assert.IsType(ex, actualExn) 168 | return true //keep FsUnit happy 169 | } 170 | 171 | CustomMatcher( 172 | $"Throws %s{ex.Name} (Below, XUnit does not show actual value properly)", 173 | (fun fn -> (testForThrowing (fn :?> unit -> Task)).Result) 174 | ) 175 | 176 | let inline assertThrows ty (f: unit -> 'U) = f >> ignore |> should throw ty 177 | let inline assertNullArg f = assertThrows typeof f 178 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/fail-trace.txt: -------------------------------------------------------------------------------- 1 | 6 (false): at AfterCode<_, _>, after F# inits the sm, and we can attach extra info 2 | 6 (false): GetAsyncEnumerator, start cloning... 3 | 6 (false): GetAsyncEnumerator, finished cloning... 4 | 6 (false): MoveNextAsync... 5 | 6 (false): at MoveNextAsync: normal resumption scenario 6 | 6 (false): at MoveNextAsync: start calling builder.MoveNext() 7 | 6 (false): at IAsyncStateMatchine.MoveNext 8 | 6 (false): Resuming at resumption point 0 9 | 6 (false): at Run.MoveNext start 10 | 6 (false): at Bind 11 | 6 (false): at Bind: with __stack_fin = false 12 | 6 (false): at Bind: calling AwaitUnsafeOnCompleted 13 | 6 (false): at Run.MoveNext, __stack_code_fin=False 14 | 6 (false): at Run.MoveNext, await 15 | 6 (false): at MoveNextAsync: done calling builder.MoveNext() 16 | 6 (false): at MoveNextAsyncResult: case Pending... 17 | 13 (false): at IAsyncStateMatchine.MoveNext 18 | 13 (false): at Bind: with __stack_fin = true 19 | 13 (false): at Bind: with getting result from awaiter 20 | 13 (false): at Bind: calling continuation 21 | 13 (false): at Zero() 22 | 13 (false): at Run.MoveNext, __stack_code_fin=True 23 | 13 (false): at Run.MoveNext, done 24 | 14 (false): Getting result for token on 'None' branch, status: Succeeded 25 | 15 (false): GetAsyncEnumerator, start cloning... 26 | 15 (false): GetAsyncEnumerator, finished cloning... 27 | 15 (false): MoveNextAsync... 28 | 15 (false): at MoveNextAsync: normal resumption scenario 29 | 15 (false): at MoveNextAsync: start calling builder.MoveNext() 30 | 15 (false): at IAsyncStateMatchine.MoveNext 31 | 15 (false): at Bind: with __stack_fin = true 32 | 15 (false): at Bind: with getting result from awaiter 33 | 15 (false): Exception dump: 34 | 15 (false): System.NullReferenceException: Object reference not set to an instance of an object. 35 | at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 36 | 15 (false): Setting exception of PromiseOfValueOrEnd to: Object reference not set to an instance of an object. 37 | 15 (false): at MoveNextAsync: done calling builder.MoveNext() 38 | 15 (false): at MoveNextAsyncResult: case Faulted... 39 | 15 (false): Getting result for token on 'None' branch, status: Faulted 40 | 15 (false): Error 'Object reference not set to an instance of an object.' for token: 3 41 | 15 (false): DisposeAsync... 42 | 15 (false): DisposeAsync... 43 | 13 (false): Exception dump: 44 | 13 (false): System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed. 45 | at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(Task`1 task, TResult result) 46 | at System.Runtime.CompilerServices.AsyncIteratorMethodBuilder.Complete() 47 | at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 48 | 13 (false): Setting exception of PromiseOfValueOrEnd to: An attempt was made to transition a task to a final state when it had already completed. 49 | 13 (false): at IAsyncStatemachine EXCEPTION!!! 50 | 13 (false): System.InvalidOperationException: Operation is not valid due to the current state of the object. 51 | at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1.SignalCompletion() 52 | at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1.SetException(Exception error) 53 | at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 54 | at FSharpy.TaskSeqBuilders.TaskSeq`2.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq\TaskSeqBuilder.fs:line 249 55 | System.NullReferenceException: Object reference not set to an instance of an object. 56 | at FSharpy.Tests.Bug #42 -- synchronous.CE empty taskSeq\, GetAsyncEnumerator - MoveNextAsync multiple times@52.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TaskSeq.StateTransitionBug.Tests.CE.fs:line 60 57 | at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 264 58 | --- End of stack trace from previous location --- 59 | at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48 60 | at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90 -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.Test/success-trace.txt: -------------------------------------------------------------------------------- 1 | 6 (false): at AfterCode<_, _>, after F# inits the sm, and we can attach extra info 2 | 6 (false): GetAsyncEnumerator, start cloning... 3 | 6 (false): GetAsyncEnumerator, finished cloning... 4 | 6 (false): MoveNextAsync... 5 | 6 (false): at MoveNextAsync: normal resumption scenario 6 | 6 (false): at MoveNextAsync: start calling builder.MoveNext() 7 | 6 (false): at IAsyncStateMatchine.MoveNext 8 | 6 (false): Resuming at resumption point 0 9 | 6 (false): at Run.MoveNext start 10 | 6 (false): at Bind 11 | 6 (false): at Bind: with __stack_fin = false 12 | 6 (false): at Bind: calling AwaitUnsafeOnCompleted 13 | 6 (false): at Run.MoveNext, __stack_code_fin=False 14 | 6 (false): at Run.MoveNext, await 15 | 6 (false): at MoveNextAsync: done calling builder.MoveNext() 16 | 6 (false): at MoveNextAsyncResult: case Pending... 17 | 13 (false): at IAsyncStateMatchine.MoveNext 18 | 13 (false): at Bind: with __stack_fin = true 19 | 13 (false): at Bind: with getting result from awaiter 20 | 13 (false): at Bind: calling continuation 21 | 13 (false): at Zero() 22 | 13 (false): at Run.MoveNext, __stack_code_fin=True 23 | 13 (false): at Run.MoveNext, done 24 | 14 (false): Getting result for token on 'None' branch, status: Succeeded 25 | 15 (false): GetAsyncEnumerator, start cloning... 26 | 15 (false): GetAsyncEnumerator, finished cloning... 27 | 15 (false): MoveNextAsync... 28 | 15 (false): at MoveNextAsync: completed = true 29 | 15 (false): MoveNextAsync... 30 | 15 (false): at MoveNextAsync: normal resumption scenario 31 | 15 (false): at MoveNextAsync: start calling builder.MoveNext() 32 | 15 (false): at IAsyncStateMatchine.MoveNext 33 | 15 (false): Resuming at resumption point 0 34 | 15 (false): at Run.MoveNext start 35 | 15 (false): at Bind 36 | 15 (false): at Bind: with __stack_fin = false 37 | 15 (false): at Bind: calling AwaitUnsafeOnCompleted 38 | 15 (false): at Run.MoveNext, __stack_code_fin=False 39 | 15 (false): at Run.MoveNext, await 40 | 15 (false): at MoveNextAsync: done calling builder.MoveNext() 41 | 15 (false): at MoveNextAsyncResult: case Pending... 42 | 9 (true): at IAsyncStateMatchine.MoveNext 43 | 9 (true): at Bind: with __stack_fin = true 44 | 9 (true): at Bind: with getting result from awaiter 45 | 9 (true): at Bind: calling continuation 46 | 9 (true): at Zero() 47 | 9 (true): at Run.MoveNext, __stack_code_fin=True 48 | 9 (true): at Run.MoveNext, done 49 | 9 (true): Getting result for token on 'None' branch, status: Succeeded 50 | 9 (true): DisposeAsync... 51 | 9 (true): DisposeAsync... 52 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32811.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Control.TaskSeq", "FSharp.Control.TaskSeq\FSharp.Control.TaskSeq.fsproj", "{9A723760-A7AB-4C8D-9A6E-F0A38341827C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B252135E-C676-4542-8B72-412DF1B9487C}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | ..\.gitignore = ..\.gitignore 12 | ..\build.cmd = ..\build.cmd 13 | ..\Directory.Build.props = ..\Directory.Build.props 14 | ..\README.md = ..\README.md 15 | ..\release-notes.txt = ..\release-notes.txt 16 | ..\Version.props = ..\Version.props 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E55512EE-8DE2-4B44-9A4A-CF779734160B}" 20 | ProjectSection(SolutionItems) = preProject 21 | ..\.github\workflows\build.yaml = ..\.github\workflows\build.yaml 22 | ..\.github\dependabot.yml = ..\.github\dependabot.yml 23 | ..\.github\workflows\main.yaml = ..\.github\workflows\main.yaml 24 | ..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml 25 | ..\.github\workflows\test-report.yaml = ..\.github\workflows\test-report.yaml 26 | ..\.github\workflows\test.yaml = ..\.github\workflows\test.yaml 27 | EndProjectSection 28 | EndProject 29 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Control.TaskSeq.Test", "FSharp.Control.TaskSeq.Test\FSharp.Control.TaskSeq.Test.fsproj", "{06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}" 30 | EndProject 31 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{B198D5FE-A731-4AFA-96A5-E5DD94EE293D}" 32 | ProjectSection(SolutionItems) = preProject 33 | ..\assets\nuget-package-readme.md = ..\assets\nuget-package-readme.md 34 | ..\assets\taskseq-icon.png = ..\assets\taskseq-icon.png 35 | EndProjectSection 36 | EndProject 37 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Control.TaskSeq.SmokeTests", "FSharp.Control.TaskSeq.SmokeTests\FSharp.Control.TaskSeq.SmokeTests.fsproj", "{784DAB92-C61A-4A00-890B-E6E3B1805473}" 38 | EndProject 39 | Global 40 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 41 | Debug|Any CPU = Debug|Any CPU 42 | Release|Any CPU = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 45 | {9A723760-A7AB-4C8D-9A6E-F0A38341827C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {9A723760-A7AB-4C8D-9A6E-F0A38341827C}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {9A723760-A7AB-4C8D-9A6E-F0A38341827C}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {9A723760-A7AB-4C8D-9A6E-F0A38341827C}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {784DAB92-C61A-4A00-890B-E6E3B1805473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {784DAB92-C61A-4A00-890B-E6E3B1805473}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {784DAB92-C61A-4A00-890B-E6E3B1805473}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {784DAB92-C61A-4A00-890B-E6E3B1805473}.Release|Any CPU.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {E55512EE-8DE2-4B44-9A4A-CF779734160B} = {B252135E-C676-4542-8B72-412DF1B9487C} 63 | {B198D5FE-A731-4AFA-96A5-E5DD94EE293D} = {B252135E-C676-4542-8B72-412DF1B9487C} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {2AE57787-A847-4460-A627-1EB1D224FBC3} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq.v3.ncrunchsolution: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | TASKSEQ_LOG_VERBOSE = false 6 | 7 | False 8 | True 9 | True 10 | 11 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace TaskSeq.Tests 2 | 3 | open System.Runtime.CompilerServices 4 | 5 | // ensure the test project has access to the internal types 6 | [] 7 | 8 | do () 9 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/AsyncExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | [] 4 | module AsyncExtensions = 5 | 6 | // Add asynchronous for loop to the 'async' computation builder 7 | type Microsoft.FSharp.Control.AsyncBuilder with 8 | 9 | member _.For(source: TaskSeq<'T>, action: 'T -> Async) = 10 | source 11 | |> TaskSeq.iterAsync (action >> Async.StartImmediateAsTask) 12 | |> Async.AwaitTask 13 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/AsyncExtensions.fsi: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | [] 4 | module AsyncExtensions = 5 | 6 | type AsyncBuilder with 7 | 8 | /// 9 | /// Inside , iterate over all values of a . 10 | /// 11 | member For: source: TaskSeq<'T> * action: ('T -> Async) -> Async 12 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/DebugUtils.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | open System.Threading.Tasks 4 | open System 5 | open System.Diagnostics 6 | open System.Threading 7 | 8 | type Debug = 9 | 10 | [] 11 | static val mutable private verbose: bool option 12 | 13 | /// Setting from environment variable TASKSEQ_LOG_VERBOSE, which, 14 | /// when set, enables (very) verbose printing of flow and state 15 | static member private getVerboseSetting() = 16 | match Debug.verbose with 17 | | None -> 18 | let verboseEnv = 19 | try 20 | match Environment.GetEnvironmentVariable "TASKSEQ_LOG_VERBOSE" with 21 | | null -> false 22 | | x -> 23 | match x.ToLowerInvariant().Trim() with 24 | | "1" 25 | | "true" 26 | | "on" 27 | | "yes" -> true 28 | | _ -> false 29 | 30 | with _ -> 31 | false 32 | 33 | Debug.verbose <- Some verboseEnv 34 | verboseEnv 35 | 36 | | Some setting -> setting 37 | 38 | /// Private helper to log to stdout in DEBUG builds only 39 | [] 40 | static member private print value = 41 | match Debug.getVerboseSetting () with 42 | | false -> () 43 | | true -> 44 | // don't use ksprintf here, because the compiler does not remove all allocations due to 45 | // the way PrintfFormat types are compiled, even if we set the Conditional attribute. 46 | let ct = Thread.CurrentThread 47 | printfn "%i (%b): %s" ct.ManagedThreadId ct.IsThreadPoolThread value 48 | 49 | /// Log to stdout in DEBUG builds only 50 | [] 51 | static member logInfo(str) = Debug.print str 52 | 53 | /// Log to stdout in DEBUG builds only 54 | [] 55 | static member logInfo(str, data) = Debug.print $"%s{str}{data}" 56 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | True 6 | true 7 | True 8 | Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions 9 | $(Version) 10 | Abel Braaksma; Don Syme 11 | This library brings C#'s concept of 'await foreach' to F#, with a seamless implementation of IAsyncEnumerable<'T>. 12 | 13 | The 'taskSeq' computation expression adds support for awaitable asynchronous sequences with similar ease of use and performance to F#'s 'task' CE, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. 14 | 15 | Generates optimized IL code through resumable state machines, and comes with a comprehensive set of functions in module 'TaskSeq'. See README for documentation and more info. 16 | Copyright 2022-2024 17 | https://github.com/fsprojects/FSharp.Control.TaskSeq 18 | https://github.com/fsprojects/FSharp.Control.TaskSeq 19 | taskseq-icon.png 20 | ..\..\packages 21 | MIT 22 | False 23 | nuget-package-readme.md 24 | $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../release-notes.txt")) 25 | taskseq;f#;fsharp;asyncseq;seq;sequences;sequential;threading;computation expression;IAsyncEnumerable;task;async;iteration 26 | True 27 | snupkg 28 | 29 | 30 | 31 | 32 | 33 | 34 | True 35 | 36 | 37 | 38 | 39 | 40 | True 41 | \ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | true 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/TaskExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | open System.Collections.Generic 4 | open System.Threading 5 | open System.Threading.Tasks 6 | 7 | open Microsoft.FSharp.Core.CompilerServices 8 | open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators 9 | 10 | // note: these are *not* experimental features anymore, but they forgot to switch off the flag 11 | #nowarn "57" // Experimental library feature, requires '--langversion:preview'. 12 | #nowarn "1204" // This construct is for use by compiled F# code and should not be used directly. 13 | 14 | [] 15 | module TaskExtensions = 16 | 17 | // Add asynchronous for loop to the 'task' computation builder 18 | type Microsoft.FSharp.Control.TaskBuilder with 19 | 20 | /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the task CE 21 | /// This code is mostly a copy of TaskSeq.WhileAsync. 22 | member inline _.WhileAsync([] condition: unit -> ValueTask, body: TaskCode<_, unit>) : TaskCode<_, _> = 23 | let mutable condition_res = true 24 | 25 | // note that this While itself has both a dynamic and static implementation 26 | // so we don't need to add that here (TODO: how to verify?). 27 | ResumableCode.While( 28 | (fun () -> condition_res), 29 | TaskCode<_, _>(fun sm -> 30 | let mutable __stack_condition_fin = true 31 | let __stack_vtask = condition () 32 | 33 | let mutable awaiter = __stack_vtask.GetAwaiter() 34 | 35 | if awaiter.IsCompleted then 36 | Debug.logInfo "at Task.WhileAsync: returning completed task" 37 | 38 | __stack_condition_fin <- true 39 | condition_res <- awaiter.GetResult() 40 | else 41 | Debug.logInfo "at Task.WhileAsync: awaiting non-completed task" 42 | 43 | // This will yield with __stack_fin = false 44 | // This will resume with __stack_fin = true 45 | 46 | // NOTE (AB): if this extra let-binding isn't here, we get NRE exceptions, infinite loops (end of seq not signaled) and warning FS3513 47 | let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm) 48 | __stack_condition_fin <- __stack_yield_fin 49 | 50 | if __stack_condition_fin then 51 | condition_res <- awaiter.GetResult() 52 | 53 | 54 | if __stack_condition_fin then 55 | if condition_res then body.Invoke(&sm) else true 56 | else 57 | sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm) 58 | false) 59 | ) 60 | 61 | member inline this.For(source: TaskSeq<'T>, body: 'T -> TaskCode<_, unit>) : TaskCode<_, unit> = 62 | TaskCode<'TOverall, unit>(fun sm -> 63 | this 64 | .Using( 65 | source.GetAsyncEnumerator CancellationToken.None, 66 | (fun e -> 67 | this.WhileAsync( 68 | // __debugPoint is only available from FSharp.Core 6.0.4 69 | //(fun () -> 70 | // Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers.__debugPoint 71 | // "ForLoop.InOrToKeyword" 72 | 73 | // e.MoveNextAsync()), 74 | e.MoveNextAsync, 75 | (fun sm -> (body e.Current).Invoke(&sm)) 76 | )) 77 | ) 78 | .Invoke(&sm)) 79 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/TaskExtensions.fsi: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | #nowarn "1204" // This construct is for use by compiled F# code and should not be used directly. 4 | 5 | [] 6 | module TaskExtensions = 7 | 8 | type TaskBuilder with 9 | 10 | /// 11 | /// Inside , iterate over all values of a . 12 | /// 13 | member inline For: source: TaskSeq<'T> * body: ('T -> TaskCode<'TOverall, unit>) -> TaskCode<'TOverall, unit> 14 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/Utils.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | open System 4 | open System.Threading.Tasks 5 | 6 | [] 7 | module ValueTaskExtensions = 8 | type ValueTask with 9 | static member inline CompletedTask = 10 | // This mimics how it is done in net5.0 and later internally 11 | Unchecked.defaultof 12 | 13 | module ValueTask = 14 | let False = ValueTask() 15 | let True = ValueTask true 16 | let inline fromResult (value: 'T) = ValueTask<'T> value 17 | let inline ofSource taskSource version = ValueTask(taskSource, version) 18 | let inline ofTask (task: Task<'T>) = ValueTask<'T> task 19 | 20 | let inline ignore (valueTask: ValueTask<'T>) = 21 | // this implementation follows Stephen Toub's advice, see: 22 | // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 23 | if valueTask.IsCompletedSuccessfully then 24 | // ensure any side effect executes 25 | valueTask.Result |> ignore 26 | ValueTask() 27 | else 28 | ValueTask(valueTask.AsTask()) 29 | 30 | [] 31 | let inline FromResult (value: 'T) = ValueTask<'T> value 32 | 33 | [] 34 | let inline ofIValueTaskSource taskSource version = ofSource taskSource version 35 | 36 | module Task = 37 | let inline fromResult (value: 'U) : Task<'U> = Task.FromResult value 38 | let inline ofAsync (async: Async<'T>) = task { return! async } 39 | let inline ofTask (task': Task) = task { do! task' } 40 | let inline apply (func: _ -> _) = func >> Task.FromResult 41 | let inline toAsync (task: Task<'T>) = Async.AwaitTask task 42 | let inline toValueTask (task: Task<'T>) = ValueTask<'T> task 43 | let inline ofValueTask (valueTask: ValueTask<'T>) = task { return! valueTask } 44 | 45 | let inline ignore (task: Task<'T>) = 46 | TaskBuilder.task { 47 | // ensure the task is awaited 48 | let! _ = task 49 | return () 50 | } 51 | :> Task 52 | 53 | let inline map mapper (task: Task<'T>) : Task<'U> = TaskBuilder.task { 54 | let! result = task 55 | return mapper result 56 | } 57 | 58 | let inline bind binder (task: Task<'T>) : Task<'U> = TaskBuilder.task { 59 | let! t = task 60 | return! binder t 61 | } 62 | 63 | module Async = 64 | let inline ofTask (task: Task<'T>) = Async.AwaitTask task 65 | let inline ofUnitTask (task: Task) = Async.AwaitTask task 66 | let inline toTask (async: Async<'T>) = task { return! async } 67 | 68 | let inline ignore (async: Async<'T>) = Async.Ignore async 69 | 70 | let inline map mapper (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { 71 | let! result = async 72 | return mapper result 73 | } 74 | 75 | let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder async } 76 | -------------------------------------------------------------------------------- /src/FSharp.Control.TaskSeq/Utils.fsi: -------------------------------------------------------------------------------- 1 | namespace FSharp.Control 2 | 3 | open System 4 | open System.Threading.Tasks 5 | open System.Threading.Tasks.Sources 6 | 7 | [] 8 | module ValueTaskExtensions = 9 | 10 | /// Shims back-filling .NET 5+ functionality for use on netstandard2.1 11 | type ValueTask with 12 | 13 | /// (Extension member) Gets a ValueTask that has already completed successfully. 14 | static member inline CompletedTask: ValueTask 15 | 16 | module ValueTask = 17 | 18 | /// A successfully completed ValueTask of boolean that has the value false. 19 | val False: ValueTask 20 | 21 | /// A successfully completed ValueTask of boolean that has the value true. 22 | val True: ValueTask 23 | 24 | /// Creates a ValueTask with the supplied result of the successful operation. 25 | val inline fromResult: value: 'T -> ValueTask<'T> 26 | 27 | /// 28 | /// The function is deprecated since version 0.4.0, 29 | /// please use in its stead. See . 30 | /// 31 | [] 32 | val inline FromResult: value: 'T -> ValueTask<'T> 33 | 34 | /// 35 | /// Initializes a new instance of with an 36 | /// representing its operation. 37 | /// 38 | val inline ofSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask 39 | 40 | /// 41 | /// The function is deprecated since version 0.4.0, 42 | /// please use in its stead. See . 43 | /// 44 | [] 45 | val inline ofIValueTaskSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask 46 | 47 | /// Creates a ValueTask from a Task<'T> 48 | val inline ofTask: task: Task<'T> -> ValueTask<'T> 49 | 50 | /// Convert a ValueTask<'T> into a non-generic ValueTask, ignoring the result 51 | val inline ignore: valueTask: ValueTask<'T> -> ValueTask 52 | 53 | module Task = 54 | 55 | /// Creates a Task<'U> that's completed successfully with the specified result. 56 | val inline fromResult: value: 'U -> Task<'U> 57 | 58 | /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` 59 | val inline ofAsync: async: Async<'T> -> Task<'T> 60 | 61 | /// Convert a non-generic Task into a Task 62 | val inline ofTask: task': Task -> Task 63 | 64 | /// Convert a plain function into a task-returning function 65 | val inline apply: func: ('a -> 'b) -> ('a -> Task<'b>) 66 | 67 | /// Convert a Task<'T> into an Async<'T> 68 | val inline toAsync: task: Task<'T> -> Async<'T> 69 | 70 | /// Convert a Task<'T> into a ValueTask<'T> 71 | val inline toValueTask: task: Task<'T> -> ValueTask<'T> 72 | 73 | /// 74 | /// Convert a ValueTask<'T> to a Task<'T>. For a non-generic ValueTask, 75 | /// consider: . 76 | /// 77 | val inline ofValueTask: valueTask: ValueTask<'T> -> Task<'T> 78 | 79 | /// Convert a Task<'T> into a non-generic Task, ignoring the result 80 | val inline ignore: task: Task<'T> -> Task 81 | 82 | /// Map a Task<'T> 83 | val inline map: mapper: ('T -> 'U) -> task: Task<'T> -> Task<'U> 84 | 85 | /// Bind a Task<'T> 86 | val inline bind: binder: ('T -> #Task<'U>) -> task: Task<'T> -> Task<'U> 87 | 88 | module Async = 89 | 90 | /// Convert an Task<'T> into an Async<'T> 91 | val inline ofTask: task: Task<'T> -> Async<'T> 92 | 93 | /// Convert a non-generic Task into an Async 94 | val inline ofUnitTask: task: Task -> Async 95 | 96 | /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` 97 | val inline toTask: async: Async<'T> -> Task<'T> 98 | 99 | /// Convert an Async<'T> into an Async, ignoring the result 100 | val inline ignore: async: Async<'T> -> Async 101 | 102 | /// Map an Async<'T> 103 | val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> 104 | 105 | /// Bind an Async<'T> 106 | val inline bind: binder: (Async<'T> -> Async<'U>) -> async: Async<'T> -> Async<'U> 107 | --------------------------------------------------------------------------------