diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5c845725..4ac8d90b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,7 +16,7 @@ jobs: - name: setup-dotnet uses: actions/setup-dotnet@v3 # build it, test it, pack it - - name: Run dotnet build + - name: Run dotnet build (release) run: ./build.cmd # deploy: diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7d06a2c3..08b0c03f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,4 +1,4 @@ -name: ci +name: Build main (release) on: push: @@ -19,7 +19,7 @@ jobs: - name: setup-dotnet uses: actions/setup-dotnet@v3 # build it, test it, pack it - - name: Run dotnet build + - name: Run dotnet build (release) run: ./build.cmd test-release: @@ -36,7 +36,7 @@ jobs: uses: actions/setup-dotnet@v3 # build it, test it, pack it - name: Run dotnet test - release - run: dotnet test -c Release --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-release.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj + run: ./build.cmd ci -release - name: Publish test results - release uses: dorny/test-reporter@v1 if: always() @@ -46,30 +46,6 @@ jobs: path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-release.trx reporter: dotnet-trx - test-debug: - name: Test Debug Build - runs-on: windows-latest - steps: - # checkout the code - - name: checkout-code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # setup dotnet based on global.json - - name: setup-dotnet - uses: actions/setup-dotnet@v3 - # build it, test it, pack it - - name: Run dotnet test - debug - run: dotnet test -c Debug --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-debug.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj - - name: Publish test results - debug - uses: dorny/test-reporter@v1 - if: always() - with: - name: Report debug tests - # this path glob pattern requires forward slashes! - path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-debug.trx - reporter: dotnet-trx - # deploy: # name: deploy # runs-on: ubuntu-latest diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3dac727f..166ca10e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,9 +15,9 @@ jobs: # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v3 - # build it, test it, pack it + # build it, test it - name: Run dotnet test - release - run: dotnet test -c Release --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-release.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj + run: ./build.cmd ci -release - name: Publish test results - release uses: dorny/test-reporter@v1 if: always() @@ -39,9 +39,9 @@ jobs: # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v3 - # build it, test it, pack it + # build it, test it - name: Run dotnet test - debug - run: dotnet test -c Debug --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-debug.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj + run: ./build.cmd ci -debug - name: Publish test results - debug uses: dorny/test-reporter@v1 if: always() @@ -50,4 +50,3 @@ jobs: # this path glob pattern requires forward slashes! path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-debug.trx reporter: dotnet-trx - \ No newline at end of file diff --git a/README.md b/README.md index 024b8683..be591941 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,76 @@ -[![build](https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml/badge.svg)](https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml) -[![test](https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml/badge.svg)](https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml) +[![build][buildstatus_img]][buildstatus] +[![test][teststatus_img]][teststatus] # TaskSeq -An implementation [`IAsyncEnumerable<'T>`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-7.0) as a `taskSeq` CE for F# with accompanying `TaskSeq` module. +An implementation [`IAsyncEnumerable<'T>`][3] as a `taskSeq` CE for F# with accompanying `TaskSeq` module. -The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, where each page is a [`MoveNextAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1.movenextasync?view=net-7.0) call on the [`IAsyncEnumerator<'T>`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1?view=net-7.0) given by a call to [`GetAsyncEnumerator()`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1.getasyncenumerator?view=net-7.0). It has been relatively challenging to work properly with this type and dealing with each step being asynchronous, and the enumerator implementing [`IAsyncDisposable`](https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable?view=net-7.0) as well, which requires careful handling. +The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, where each page is a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. It has been relatively challenging to work properly with this type and dealing with each step being asynchronous, and the enumerator implementing [`IAsyncDisposable`][7] as well, which requires careful handling. + +A good C#-based introduction on `IAsyncEnumerable` [can be found in this blog][8]. Another resource is [this MSDN article shortly after its introductiono][9]. + +## Building & testing + +TLDR: just run `build`. Or load the `sln` file in Visual Studio or VS Code and compile. + +### Prerequisites + +* .NET 6 or .NET 7 Preview +* F# 6.0 compiler +* To use `build.cmd`, the `dotnet` command must be accessible from your path. + +Just checkout this repo locally. Then, from the root of the repo, you can do: + +### Build the solution + +``` +build [release|debug] +``` + +### Run the tests + +``` +build test [release|debug] +``` + +By default, all tests are output to the console. If you don't want that, you can use `--logger console;verbosity=summary`. +Furthermore, no TRX file is generated and the `--blame-xxx` flags aren't set. + +### Run the CI command + +``` +build ci [release|debug] +``` + +This will run `dotnet test` with the `--blame-xxx` settings enabled to [prevent hanging tests][1] caused by +an [xUnit runner bug][2]. + +### Advanced + +You can pass any additional options that are valid for `dotnet test` and `dotnet build` respectively. However, +these cannot be the very first argument, so you should either use `build build --myadditionalOptions fizz buzz`, or +just specify the build-kind, i.e. this is fine: + +``` +build debug --verbosity detailed +build test --logger console;verbosity=summary +``` + +At this moment, additional options cannot have quotes in them. + +Command modifiers, like `release` and `debug`, can be specified with `-` or `/` if you so prefer: `dotnet build /release`. + +### Get help (duh!) + +``` +build help +``` + +For more info, see this PR: https://github.com/abelbraaksma/TaskSeq/pull/29. -A good C#-based introduction on `IAsyncEnumerable` [can be found in this blog](https://stu.dev/iasyncenumerable-introduction/). Another resource is [this MSDN article shortly after its introductiono](https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8). ## In progress!!! -It's based on [Don Symes `taskSeq.fs`](https://github.com/dotnet/fsharp/blob/d5312aae8aad650f0043f055bb14c3aa8117e12e/tests/benchmarks/CompiledCodeBenchmarks/TaskPerf/TaskPerf/taskSeq.fs) +It's based on [Don Symes `taskSeq.fs`][10] but expanded with useful utility functions and a few extra binding overloads. ## Short-term feature planning @@ -307,3 +367,19 @@ module TaskSeq = val foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State> ``` + +[buildstatus]: https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml +[buildstatus_img]: https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml/badge.svg +[teststatus]: https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml +[teststatus_img]: https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml/badge.svg + +[1]: https://github.com/abelbraaksma/TaskSeq/issues/25 +[2]: https://github.com/xunit/xunit/issues/2587 +[3]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-7.0 +[4]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1.movenextasync?view=net-7.0 +[5]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1?view=net-7.0 +[6]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1.getasyncenumerator?view=net-7.0 +[7]: https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable?view=net-7.0 +[8]: https://stu.dev/iasyncenumerable-introduction/ +[9]: https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8 +[10]: https://github.com/dotnet/fsharp/blob/d5312aae8aad650f0043f055bb14c3aa8117e12e/tests/benchmarks/CompiledCodeBenchmarks/TaskPerf/TaskPerf/taskSeq.fs \ No newline at end of file diff --git a/build.cmd b/build.cmd index 12a43f8f..4501ffa2 100644 --- a/build.cmd +++ b/build.cmd @@ -1,4 +1,189 @@ -echo Restoring dotnet tools... +@ECHO OFF + +REM Make environment variables local to the batch script +SETLOCAL + +REM Default local parameters (BUILD_MODE must remain empty, otherwise, 'help' doesn't work) +SET BUILD_CONFIG=Release +SET BUILD_MODE= + +SET DOTNET_TEST_ARGS= +SET DOTNET_TEST_PROJECT_LOCATION= + +SET DOTNET_CI_ARGS=--blame-hang-timeout 60000ms --logger "console;verbosity=detailed" +SET DOTNET_TEST_ARGS=--logger "console;verbosity=detailed" +SET DOTNET_TEST_PROJECT_LOCATION=".\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj" + +REM This is used to get a 'rest of arguments' list, which allows passing +REM other arguments to the dotnet build and test commands +SET REST_ARGS=%* + +:parseArgs +IF "%~1"=="build" ( + SET BUILD_MODE=build + REM Drop 'build' from the remaining args + CALL :shiftArg %REST_ARGS% + +) ELSE IF "%~1"=="test" ( + SET BUILD_MODE=test + REM Drop 'test' from the remaining args + CALL :shiftArg %REST_ARGS% + +) ELSE IF "%~1"=="ci" ( + SET BUILD_MODE=ci + REM Drop 'ci' from the remaining args + CALL :shiftArg %REST_ARGS% + +) ELSE IF "%~1"=="help" ( + GOTO :showHelp + +) ELSE IF "%~1"=="/help" ( + GOTO :showHelp + +) ELSE IF "%~1"=="-help" ( + GOTO :showHelp + +) ELSE IF "%~1"=="" ( + REM No args, default: build + SET BUILD_MODE=build + SET BUILD_CONFIG=release +) + +CALL :tryBuildConfig %REST_ARGS% +ECHO Additional arguments: %REST_ARGS% + +REM Main branching starts here +IF "%BUILD_MODE%"=="build" GOTO :runBuild +IF "%BUILD_MODE%"=="test" GOTO :runTest +IF "%BUILD_MODE%"=="ci" GOTO :runCi + + +REM Something wrong, we don't recognize the given arguments +REM Display help: + +ECHO Argument not recognized + +:showHelp +ECHO. +ECHO Available options are: +ECHO. +ECHO build Run 'dotnet build' (default if omitted) +ECHO test Run 'dotnet test' with default configuration and no CI logging. +ECHO ci Run 'dotnet test' with CI configuration and TRX logging. +ECHO. +ECHO Optionally combined with: +ECHO. +ECHO release Build release configuration (default). +ECHO debug Build debug configuration. +ECHO. +ECHO Any arguments that follow the special arguments will be passed on to 'dotnet test' or 'dotnet build' +ECHO Such user-supplied arguments can only be given when one of the above specific commands is used. +ECHO +ECHO Optional arguments may be given with a leading '/' or '-', if so preferred. +ECHO. +ECHO Examples: +ECHO. +ECHO Run default build (release): +ECHO build +ECHO. +ECHO Run debug build: +ECHO build debug +ECHO. +ECHO Run debug build with detailed verbosity: +ECHO build debug --verbosity detailed +ECHO. +ECHO Run the tests in default CI configuration +ECHO build ci +ECHO. +ECHO Run the tests as in CI, but with the Debug configuration +ECHO build ci -debug +ECHO. +ECHO Run the tests without TRX logging +ECHO build test -release +ECHO. +GOTO :EOF + +REM Normal building +:runBuild +SET BUILD_COMMAND=dotnet build src/FSharpy.TaskSeq.sln -c %BUILD_CONFIG% %REST_ARGS% +ECHO Building for %BUILD_CONFIG% configuration... +ECHO. +ECHO Executing: +ECHO %BUILD_COMMAND% +ECHO. +ECHO Restoring dotnet tools... +dotnet tool restore +%BUILD_COMMAND% +GOTO :EOF + +REM Testing +:runTest +SET TEST_COMMAND=dotnet test -c %BUILD_CONFIG% %DOTNET_TEST_ARGS% %DOTNET_TEST_PROJECT_LOCATION% %REST_ARGS% +ECHO. +ECHO Testing: %BUILD_CONFIG% configuration... +ECHO. +ECHO Restoring dotnet tools... +dotnet tool restore + +ECHO Executing: +ECHO %TEST_COMMAND% +%TEST_COMMAND% +GOTO :EOF + +REM Continuous integration +:runCi +SET TRX_LOGGER=--logger "trx;LogFileName=test-results-%BUILD_CONFIG%.trx" +SET CI_COMMAND=dotnet test -c %BUILD_CONFIG% %DOTNET_CI_ARGS% %DOTNET_TEST_PROJECT_LOCATION% %TRX_LOGGER% %REST_ARGS% +ECHO. +ECHO Continuous integration: %BUILD_CONFIG% configuration... +ECHO. +ECHO Restoring dotnet tools... dotnet tool restore -dotnet build src/FSharpy.TaskSeq.sln -c Release \ No newline at end of file +ECHO Executing: +ECHO %CI_COMMAND% +%CI_COMMAND% +GOTO :EOF + + +REM Callable label, will resume after 'CALL' line +:tryBuildConfig +IF "%~1"=="release" ( + SET BUILD_CONFIG=release + CALL :shiftArg %REST_ARGS% +) +IF "%~1"=="-release" ( + SET BUILD_CONFIG=release + CALL :shiftArg %REST_ARGS% +) +IF "%~1"=="/release" ( + SET BUILD_CONFIG=release + CALL :shiftArg %REST_ARGS% +) +IF "%~1"=="debug" ( + SET BUILD_CONFIG=debug + CALL :shiftArg %REST_ARGS% +) +IF "%~1"=="-debug" ( + SET BUILD_CONFIG=debug + CALL :shiftArg %REST_ARGS% +) +IF "%~1"=="/debug" ( + SET BUILD_CONFIG=debug + CALL :shiftArg %REST_ARGS% +) +GOTO :EOF + +REM Callable label, will resume after 'CALL' line +:shiftArg +REM WARNING!!! +REM If called from inside an IF-statement, it will NOT keep the resulting +REM variable %REST_ARGS%, until execution gets OUTSIDE of the IF-block + +REM Do not call 'SHIFT' here, as we do it manually +REM Here, '%*' means the arguments given in the CALL command to this label +SET REST_ARGS=%* + +REM Shift by stripping until and including the first argument +IF NOT "%REST_ARGS%"=="" CALL SET REST_ARGS=%%REST_ARGS:*%1=%% +GOTO :EOF diff --git a/src/FSharpy.TaskSeq.sln b/src/FSharpy.TaskSeq.sln index 50bdd390..f0b74f7f 100644 --- a/src/FSharpy.TaskSeq.sln +++ b/src/FSharpy.TaskSeq.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B252135E-C676-4542-8B72-412DF1B9487C}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + ..\build.cmd = ..\build.cmd ..\README.md = ..\README.md EndProjectSection EndProject diff --git a/src/FSharpy.TaskSeq.v3.ncrunchsolution b/src/FSharpy.TaskSeq.v3.ncrunchsolution new file mode 100644 index 00000000..10420ac9 --- /dev/null +++ b/src/FSharpy.TaskSeq.v3.ncrunchsolution @@ -0,0 +1,6 @@ + + + True + True + + \ No newline at end of file