├── result.go ├── Makefile ├── go.mod ├── .deepsource.toml ├── CHANGELOG.md ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── async.go ├── go.sum ├── LICENSE ├── coverage.txt ├── CONTRIBUTING.md ├── awaiter.go ├── waiter.go ├── README.md ├── awaiter_test.go └── waiter_test.go /result.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | type Result[T any] struct { 4 | Data T 5 | Error error 6 | } 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint unit-tests 2 | lint: 3 | golangci-lint run 4 | 5 | unit-tests: 6 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yaitoo/async 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["*_test.go"] 4 | 5 | exclude_patterns = ["*_test.go"] 6 | 7 | [[analyzers]] 8 | name = "go" 9 | 10 | [analyzers.meta] 11 | import_root = "github.com/yaitoo/async" 12 | 13 | [[transformers]] 14 | name = "gofmt" 15 | 16 | 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [1.0.4] - 2024-03-18 10 | - added `Action` support (#4) 11 | 12 | ## [1.0.3] - 2024-03-12 13 | - added `WaitN` (#1) 14 | 15 | ## [1.0.0] - 2024-03-08 16 | - 1st release 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | coverage.txt 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | # open-pull-requests-limit: 10 13 | labels: 14 | - gomod -------------------------------------------------------------------------------- /async.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrTooLessDone = errors.New("async: too less tasks/actions to completed without error") 10 | ) 11 | 12 | // Task a task with result T 13 | type Task[T any] func(ctx context.Context) (T, error) 14 | 15 | // New create a task waiter 16 | func New[T any](tasks ...Task[T]) Waiter[T] { 17 | return &waiter[T]{ 18 | tasks: tasks, 19 | } 20 | } 21 | 22 | // Action a task without result 23 | type Action func(ctx context.Context) error 24 | 25 | // NewA create an action awaiter 26 | func NewA(actions ...Action) Awaiter { 27 | return &awaiter{ 28 | actions: actions, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dayi Chen 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 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | concurrency: 4 | group: "tests-${{ github.ref }}" 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | - dev 12 | tags: 13 | pull_request: 14 | workflow_dispatch: 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Clone 22 | uses: actions/checkout@v4 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ^1.22 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v3 29 | with: 30 | version: v1.54 31 | unit-tests: 32 | name: Unit Tests 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Clone 36 | uses: actions/checkout@v4 37 | - name: Set up Go 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: ^1.22 41 | - name: Unit Tests 42 | run: | 43 | make unit-tests 44 | 45 | - name: Codecov 46 | uses: codecov/codecov-action@v4 47 | with: 48 | files: ./coverage.txt 49 | flags: Unit-Tests 50 | verbose: true 51 | env: 52 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 53 | -------------------------------------------------------------------------------- /coverage.txt: -------------------------------------------------------------------------------- 1 | mode: atomic 2 | github.com/yaitoo/async/async.go:5.75,9.2 1 12 3 | github.com/yaitoo/async/awaiter.go:17.69,19.2 1 2 4 | github.com/yaitoo/async/awaiter.go:21.61,25.31 3 4 5 | github.com/yaitoo/async/awaiter.go:25.31,26.50 1 12 6 | github.com/yaitoo/async/awaiter.go:26.50,32.4 2 12 7 | github.com/yaitoo/async/awaiter.go:35.2,39.25 4 4 8 | github.com/yaitoo/async/awaiter.go:39.25,40.10 1 10 9 | github.com/yaitoo/async/awaiter.go:41.19,42.22 1 8 10 | github.com/yaitoo/async/awaiter.go:42.22,44.5 1 1 11 | github.com/yaitoo/async/awaiter.go:44.10,46.5 1 7 12 | github.com/yaitoo/async/awaiter.go:47.21,48.27 1 2 13 | github.com/yaitoo/async/awaiter.go:53.2,53.17 1 2 14 | github.com/yaitoo/async/awaiter.go:53.17,55.3 1 1 15 | github.com/yaitoo/async/awaiter.go:57.2,57.19 1 1 16 | github.com/yaitoo/async/awaiter.go:60.62,68.31 5 8 17 | github.com/yaitoo/async/awaiter.go:68.31,69.50 1 24 18 | github.com/yaitoo/async/awaiter.go:69.50,75.4 2 24 19 | github.com/yaitoo/async/awaiter.go:78.2,83.25 4 8 20 | github.com/yaitoo/async/awaiter.go:83.25,84.10 1 12 21 | github.com/yaitoo/async/awaiter.go:85.19,86.22 1 10 22 | github.com/yaitoo/async/awaiter.go:86.22,88.5 1 5 23 | github.com/yaitoo/async/awaiter.go:90.4,90.28 1 5 24 | github.com/yaitoo/async/awaiter.go:91.21,92.23 1 2 25 | github.com/yaitoo/async/awaiter.go:96.2,96.17 1 1 26 | github.com/yaitoo/async/awaiter.go:96.17,98.3 1 1 27 | github.com/yaitoo/async/awaiter.go:100.2,100.15 1 0 28 | github.com/yaitoo/async/errors.go:7.33,9.2 1 0 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | SQLE is actively looking for contributors so feel free to help out when: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | 9 | ### We Develop with Github 10 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 11 | 12 | ### All Code Changes Happen Through Pull Requests 13 | 1. Fork the repo and create your branch from `main`. 14 | 2. If you've added code that should be tested, add tests. 15 | 3. If you've changed APIs, update the documentation. 16 | 4. Ensure the test suite passes: `make unit-tests`. 17 | 5. Make sure your code lints: `make lint`. 18 | 6. Issue that pull request! 19 | 20 | ### Any contributions you make will be under the MIT Software License 21 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 22 | 23 | ### Report bugs using Github's [Issues](https://github.com/yaitoo/async/issues) 24 | We use GitHub issues to track public bugs. Report a bug by opening a new issue; it's that easy! 25 | 26 | ### Write bug reports with detail, background, and sample code 27 | **Great Bug Reports** tend to have: 28 | 29 | - A quick summary and/or background 30 | - Steps to reproduce 31 | - Be specific! 32 | - Give sample code if you can. 33 | - Write unit tests if you can. 34 | - What you expected would happen 35 | - What actually happens 36 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 37 | 38 | ### License 39 | By contributing, you agree that your contributions will be licensed under its MIT License. -------------------------------------------------------------------------------- /awaiter.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Awaiter interface { 8 | // Add add an action 9 | Add(action Action) 10 | // Wait wail for all actions to completed 11 | Wait(context.Context) ([]error, error) 12 | // WaitAny wait for any action to completed without error, can cancel other tasks 13 | WaitAny(context.Context) ([]error, error) 14 | // WaitN wait for N actions to completed without error 15 | WaitN(context.Context, int) ([]error, error) 16 | } 17 | 18 | type awaiter struct { 19 | actions []Action 20 | } 21 | 22 | func (a *awaiter) Add(action Action) { 23 | a.actions = append(a.actions, action) 24 | } 25 | 26 | func (a *awaiter) Wait(ctx context.Context) ([]error, error) { 27 | wait := make(chan error) 28 | 29 | for _, action := range a.actions { 30 | go func(action Action) { 31 | 32 | wait <- action(ctx) 33 | }(action) 34 | } 35 | 36 | var taskErrs []error 37 | 38 | tt := len(a.actions) 39 | for i := 0; i < tt; i++ { 40 | select { 41 | case err := <-wait: 42 | if err != nil { 43 | taskErrs = append(taskErrs, err) 44 | } 45 | case <-ctx.Done(): 46 | return taskErrs, ctx.Err() 47 | } 48 | } 49 | 50 | if len(taskErrs) > 0 { 51 | return taskErrs, ErrTooLessDone 52 | } 53 | 54 | return taskErrs, nil 55 | } 56 | 57 | func (a *awaiter) WaitN(ctx context.Context, n int) ([]error, error) { 58 | wait := make(chan error) 59 | 60 | cancelCtx, cancel := context.WithCancel(ctx) 61 | defer cancel() 62 | 63 | for _, action := range a.actions { 64 | go func(action Action) { 65 | wait <- action(cancelCtx) 66 | 67 | }(action) 68 | } 69 | 70 | var taskErrs []error 71 | tt := len(a.actions) 72 | 73 | var done int 74 | for i := 0; i < tt; i++ { 75 | select { 76 | case err := <-wait: 77 | if err != nil { 78 | taskErrs = append(taskErrs, err) 79 | } else { 80 | 81 | done++ 82 | if done == n { 83 | return taskErrs, nil 84 | } 85 | } 86 | case <-ctx.Done(): 87 | return taskErrs, ctx.Err() 88 | } 89 | 90 | } 91 | 92 | return taskErrs, ErrTooLessDone 93 | } 94 | 95 | func (a *awaiter) WaitAny(ctx context.Context) ([]error, error) { 96 | return a.WaitN(ctx, 1) 97 | } 98 | -------------------------------------------------------------------------------- /waiter.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Waiter[T any] interface { 8 | // Add add a task 9 | Add(task Task[T]) 10 | // Wait wail for all tasks to completed 11 | Wait(context.Context) ([]T, []error, error) 12 | // WaitAny wait for any task to completed without error, can cancel other tasks 13 | WaitAny(context.Context) (T, []error, error) 14 | // WaitN wait for N tasks to completed without error 15 | WaitN(context.Context, int) ([]T, []error, error) 16 | } 17 | 18 | type waiter[T any] struct { 19 | tasks []Task[T] 20 | } 21 | 22 | func (a *waiter[T]) Add(task Task[T]) { 23 | a.tasks = append(a.tasks, task) 24 | } 25 | 26 | func (a *waiter[T]) Wait(ctx context.Context) ([]T, []error, error) { 27 | wait := make(chan Result[T]) 28 | 29 | for _, task := range a.tasks { 30 | go func(task func(context.Context) (T, error)) { 31 | r, err := task(ctx) 32 | wait <- Result[T]{ 33 | Data: r, 34 | Error: err, 35 | } 36 | }(task) 37 | } 38 | 39 | var r Result[T] 40 | var taskErrs []error 41 | var items []T 42 | 43 | tt := len(a.tasks) 44 | for i := 0; i < tt; i++ { 45 | select { 46 | case r = <-wait: 47 | if r.Error != nil { 48 | taskErrs = append(taskErrs, r.Error) 49 | } else { 50 | items = append(items, r.Data) 51 | } 52 | case <-ctx.Done(): 53 | return items, taskErrs, ctx.Err() 54 | } 55 | } 56 | 57 | if len(items) == tt { 58 | return items, taskErrs, nil 59 | } 60 | 61 | return items, taskErrs, ErrTooLessDone 62 | } 63 | 64 | func (a *waiter[T]) WaitN(ctx context.Context, n int) ([]T, []error, error) { 65 | wait := make(chan Result[T]) 66 | 67 | cancelCtx, cancel := context.WithCancel(ctx) 68 | defer cancel() 69 | 70 | for _, task := range a.tasks { 71 | go func(task func(context.Context) (T, error)) { 72 | r, err := task(cancelCtx) 73 | wait <- Result[T]{ 74 | Data: r, 75 | Error: err, 76 | } 77 | }(task) 78 | } 79 | 80 | var r Result[T] 81 | var taskErrs []error 82 | var items []T 83 | tt := len(a.tasks) 84 | var done int 85 | for i := 0; i < tt; i++ { 86 | select { 87 | case r = <-wait: 88 | if r.Error != nil { 89 | taskErrs = append(taskErrs, r.Error) 90 | } else { 91 | items = append(items, r.Data) 92 | done++ 93 | if done == n { 94 | return items, taskErrs, nil 95 | } 96 | } 97 | case <-ctx.Done(): 98 | return items, taskErrs, ctx.Err() 99 | } 100 | 101 | } 102 | 103 | return items, taskErrs, ErrTooLessDone 104 | } 105 | 106 | func (a *waiter[T]) WaitAny(ctx context.Context) (T, []error, error) { 107 | var t T 108 | result, taskErrs, err := a.WaitN(ctx, 1) 109 | 110 | if len(result) == 1 { 111 | t = result[0] 112 | } 113 | 114 | return t, taskErrs, err 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async 2 | Async is an async/await like task package for Go 3 | 4 | ![License](https://img.shields.io/badge/license-MIT-green.svg) 5 | [![Tests](https://github.com/yaitoo/async/actions/workflows/tests.yml/badge.svg)](https://github.com/yaitoo/async/actions/workflows/tests.yml) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/yaitoo/async.svg)](https://pkg.go.dev/github.com/yaitoo/async) 7 | [![Codecov](https://codecov.io/gh/yaitoo/async/branch/main/graph/badge.svg)](https://codecov.io/gh/yaitoo/async) 8 | [![GitHub Release](https://img.shields.io/github/v/release/yaitoo/async)](https://github.com/yaitoo/sqle/blob/main/CHANGELOG.md) 9 | [![Go Report Card](https://goreportcard.com/badge/yaitoo/async)](http://goreportcard.com/report/yaitoo/async) 10 | 11 | 12 | ## Features 13 | - Wait/WaitAny/WaitN for `Task` and `Action` 14 | - `context.Context` with `timeout`, `cancel` support 15 | - Works with generic instead of `interface{}` 16 | 17 | ## Tutorials 18 | see more examples on [tasks](./waiter_test.go), [actions](./awaiter_test.go) or [go.dev](https://go.dev/play/p/7jgcRltbwts) 19 | 20 | ### Install async 21 | - install latest commit from `main` branch 22 | ``` 23 | go get github.com/yaitoo/async@main 24 | ``` 25 | 26 | - install latest release 27 | ``` 28 | go get github.com/yaitoo/async@latest 29 | ``` 30 | 31 | ### Wait 32 | wait all tasks to completed. 33 | 34 | ``` 35 | t := async.New[int](func(ctx context.Context) (int, error) { 36 | return 1, nil 37 | }, func(ctx context.Context) (int, error) { 38 | return 2, nil 39 | }) 40 | 41 | result, err, taskErrs := t.Wait(context.Background()) 42 | 43 | 44 | fmt.Println(result) //[1,2] or [2,1] 45 | fmt.Println(err) // nil 46 | fmt.Println(taskErrs) //nil 47 | 48 | 49 | ``` 50 | 51 | 52 | ### WaitAny 53 | wait any task to completed 54 | 55 | ``` 56 | t := async.New[int](func(ctx context.Context) (int, error) { 57 | time.Sleep(2 * time.Second) 58 | return 1, nil 59 | }, func(ctx context.Context) (int, error) { 60 | return 2, nil 61 | }) 62 | 63 | result, err, taskErrs := t.WaitAny(context.Background()) 64 | 65 | fmt.Println(result) //2 66 | fmt.Println(err) //nil 67 | fmt.Println(taskErrs) //nil 68 | 69 | ``` 70 | 71 | ### WaitN 72 | wait N tasks to completed. 73 | 74 | ``` 75 | t := async.New[int](func(ctx context.Context) (int, error) { 76 | time.Sleep(2 * time.Second) 77 | return 1, nil 78 | }, func(ctx context.Context) (int, error) { 79 | return 2, nil 80 | }, func(ctx context.Context) (int, error) { 81 | return 3, nil 82 | }) 83 | 84 | result, err, taskErrs := t.WaitN(context.Background(),2) 85 | 86 | 87 | fmt.Println(result) //[2,3] or [3,2] 88 | fmt.Println(err) //nil 89 | fmt.Println(taskErrs) //nil 90 | 91 | ``` 92 | 93 | ### Timeout 94 | cancel all tasks if it is timeout. 95 | ``` 96 | t := async.New[int](func(ctx context.Context) (int, error) { 97 | time.Sleep(2 * time.Second) 98 | return 1, nil 99 | }, func(ctx context.Context) (int, error) { 100 | time.Sleep(2 * time.Second) 101 | return 2, nil 102 | }) 103 | 104 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 105 | defer cancel() 106 | 107 | result, err, tasks := t.WaitAny(ctx) 108 | //result, err, tasks := t.Wait(ctx) 109 | 110 | 111 | fmt.Println(result) //nil 112 | fmt.Println(err) // context.DeadlineExceeded 113 | fmt.Println(taskErrs) //nil 114 | ``` 115 | 116 | ### Cancel 117 | manually cancel all tasks. 118 | 119 | ``` 120 | t := async.New[int](func(ctx context.Context) (int, error) { 121 | time.Sleep(2 * time.Second) 122 | return 1, nil 123 | }, func(ctx context.Context) (int, error) { 124 | time.Sleep(2 * time.Second) 125 | return 2, nil 126 | }) 127 | 128 | ctx, cancel := context.WithCancel(context.Background()) 129 | go func(){ 130 | time.Sleep(1 * time.Second) 131 | cancel() 132 | }() 133 | 134 | //result, err, taskErrs := t.WaitAny(ctx) 135 | result, err, taskErrs := t.Wait(ctx) 136 | 137 | 138 | fmt.Println(result) //nil 139 | fmt.Println(err) // context.Cancelled 140 | fmt.Println(taskErrs) // nil 141 | 142 | 143 | ``` 144 | 145 | 146 | ## Contributing 147 | Contributions are welcome! If you're interested in contributing, please feel free to [contribute](CONTRIBUTING.md) 148 | 149 | 150 | ## License 151 | [MIT License](LICENSE) -------------------------------------------------------------------------------- /awaiter_test.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestAwait(t *testing.T) { 13 | 14 | wantedErr := errors.New("wanted") 15 | 16 | tests := []struct { 17 | name string 18 | ctx func() context.Context 19 | withCancel bool 20 | setup func() Awaiter 21 | wantedErr error 22 | wantedErrs []error 23 | }{ 24 | { 25 | name: "wait_should_work", 26 | ctx: context.Background, 27 | setup: func() Awaiter { 28 | a := NewA(func(ctx context.Context) error { 29 | return nil 30 | }, func(ctx context.Context) error { 31 | return nil 32 | }) 33 | 34 | a.Add(func(ctx context.Context) error { 35 | return nil 36 | }) 37 | 38 | return a 39 | }, 40 | 41 | wantedErr: nil, 42 | }, 43 | { 44 | name: "error_should_work", 45 | ctx: context.Background, 46 | setup: func() Awaiter { 47 | return NewA(func(ctx context.Context) error { 48 | return nil 49 | }, func(ctx context.Context) error { 50 | return nil 51 | }, func(ctx context.Context) error { 52 | return wantedErr 53 | }) 54 | }, 55 | wantedErr: ErrTooLessDone, 56 | wantedErrs: []error{wantedErr}, 57 | }, 58 | { 59 | name: "errors_should_work", 60 | ctx: context.Background, 61 | setup: func() Awaiter { 62 | return NewA(func(ctx context.Context) error { 63 | return wantedErr 64 | }, func(ctx context.Context) error { 65 | return wantedErr 66 | }, func(ctx context.Context) error { 67 | return wantedErr 68 | }) 69 | }, 70 | wantedErr: ErrTooLessDone, 71 | wantedErrs: []error{wantedErr, wantedErr, wantedErr}, 72 | }, 73 | { 74 | name: "context_should_work", 75 | ctx: func() context.Context { 76 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 77 | return ctx 78 | }, 79 | setup: func() Awaiter { 80 | return NewA(func(ctx context.Context) error { 81 | time.Sleep(5 * time.Second) 82 | return nil 83 | }, func(ctx context.Context) error { 84 | time.Sleep(5 * time.Second) 85 | return nil 86 | }, func(ctx context.Context) error { 87 | return nil 88 | }) 89 | }, 90 | 91 | wantedErr: context.DeadlineExceeded, 92 | }, 93 | { 94 | name: "cancel_should_work", 95 | ctx: context.Background, 96 | setup: func() Awaiter { 97 | return NewA(func(ctx context.Context) error { 98 | time.Sleep(5 * time.Second) 99 | return nil 100 | }, func(ctx context.Context) error { 101 | time.Sleep(5 * time.Second) 102 | return nil 103 | }, func(ctx context.Context) error { 104 | return nil 105 | }) 106 | }, 107 | withCancel: true, 108 | wantedErr: context.Canceled, 109 | }, 110 | } 111 | 112 | for _, test := range tests { 113 | t.Run(test.name, func(t *testing.T) { 114 | a := test.setup() 115 | 116 | var err error 117 | var taskErrs []error 118 | 119 | if test.withCancel { 120 | ctx, cancel := context.WithCancel(context.Background()) 121 | go func() { 122 | time.Sleep(1 * time.Second) 123 | cancel() 124 | }() 125 | taskErrs, err = a.Wait(ctx) 126 | } else { 127 | taskErrs, err = a.Wait(test.ctx()) 128 | } 129 | 130 | require.Equal(t, test.wantedErr, err) 131 | require.Equal(t, test.wantedErrs, taskErrs) 132 | 133 | }) 134 | 135 | } 136 | } 137 | 138 | func TestAwaitAny(t *testing.T) { 139 | 140 | wantedErr := errors.New("wanted") 141 | 142 | tests := []struct { 143 | name string 144 | ctx func() context.Context 145 | withCancel bool 146 | setup func() Awaiter 147 | wantedErr error 148 | wantedErrs []error 149 | }{ 150 | { 151 | name: "1st_should_work", 152 | ctx: context.Background, 153 | setup: func() Awaiter { 154 | a := NewA(func(ctx context.Context) error { 155 | time.Sleep(5 * time.Second) 156 | return nil 157 | }, func(ctx context.Context) error { 158 | time.Sleep(5 * time.Second) 159 | return nil 160 | }) 161 | 162 | a.Add(func(ctx context.Context) error { 163 | return nil 164 | }) 165 | 166 | return a 167 | }, 168 | }, 169 | { 170 | name: "2nd_should_work", 171 | ctx: context.Background, 172 | setup: func() Awaiter { 173 | return NewA(func(ctx context.Context) error { 174 | time.Sleep(5 * time.Second) 175 | return nil 176 | }, func(ctx context.Context) error { 177 | return nil 178 | }, func(ctx context.Context) error { 179 | time.Sleep(5 * time.Second) 180 | return nil 181 | }) 182 | }, 183 | }, 184 | { 185 | name: "3rd_should_work", 186 | ctx: context.Background, 187 | setup: func() Awaiter { 188 | return NewA(func(ctx context.Context) error { 189 | time.Sleep(5 * time.Second) 190 | return nil 191 | }, func(ctx context.Context) error { 192 | time.Sleep(5 * time.Second) 193 | return nil 194 | }, func(ctx context.Context) error { 195 | 196 | return nil 197 | }) 198 | }, 199 | }, 200 | { 201 | name: "slowest_should_work", 202 | ctx: context.Background, 203 | setup: func() Awaiter { 204 | return NewA(func(ctx context.Context) error { 205 | time.Sleep(3 * time.Second) 206 | return nil 207 | }, func(ctx context.Context) error { 208 | return wantedErr 209 | }, func(ctx context.Context) error { 210 | return wantedErr 211 | }) 212 | }, 213 | wantedErrs: []error{wantedErr, wantedErr}, 214 | }, 215 | { 216 | name: "fastest_should_work", 217 | ctx: context.Background, 218 | setup: func() Awaiter { 219 | return NewA(func(ctx context.Context) error { 220 | return nil 221 | }, func(ctx context.Context) error { 222 | time.Sleep(2 * time.Second) 223 | return wantedErr 224 | }, func(ctx context.Context) error { 225 | time.Sleep(3 * time.Second) 226 | return wantedErr 227 | }) 228 | }, 229 | }, 230 | { 231 | name: "errors_should_work", 232 | ctx: context.Background, 233 | setup: func() Awaiter { 234 | return NewA(func(ctx context.Context) error { 235 | return wantedErr 236 | }, func(ctx context.Context) error { 237 | return wantedErr 238 | }, func(ctx context.Context) error { 239 | return wantedErr 240 | }) 241 | }, 242 | wantedErr: ErrTooLessDone, 243 | wantedErrs: []error{wantedErr, wantedErr, wantedErr}, 244 | }, 245 | { 246 | name: "error_should_work", 247 | ctx: context.Background, 248 | setup: func() Awaiter { 249 | return NewA(func(ctx context.Context) error { 250 | return wantedErr 251 | }) 252 | }, 253 | wantedErr: ErrTooLessDone, 254 | wantedErrs: []error{wantedErr}, 255 | }, 256 | { 257 | name: "context_should_work", 258 | ctx: func() context.Context { 259 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 260 | return ctx 261 | }, 262 | setup: func() Awaiter { 263 | return NewA(func(ctx context.Context) error { 264 | time.Sleep(5 * time.Second) 265 | return nil 266 | }, func(ctx context.Context) error { 267 | time.Sleep(5 * time.Second) 268 | return nil 269 | }, func(ctx context.Context) error { 270 | time.Sleep(5 * time.Second) 271 | return nil 272 | }) 273 | }, 274 | wantedErr: context.DeadlineExceeded, 275 | }, 276 | { 277 | name: "cancel_should_work", 278 | ctx: context.Background, 279 | setup: func() Awaiter { 280 | return NewA(func(ctx context.Context) error { 281 | time.Sleep(5 * time.Second) 282 | return nil 283 | }, func(ctx context.Context) error { 284 | time.Sleep(5 * time.Second) 285 | return nil 286 | }, func(ctx context.Context) error { 287 | time.Sleep(5 * time.Second) 288 | return nil 289 | }) 290 | }, 291 | withCancel: true, 292 | wantedErr: context.Canceled, 293 | }, 294 | } 295 | 296 | for _, test := range tests { 297 | t.Run(test.name, func(t *testing.T) { 298 | a := test.setup() 299 | 300 | var err error 301 | var taskErrs []error 302 | 303 | if test.withCancel { 304 | ctx, cancel := context.WithCancel(context.Background()) 305 | go func() { 306 | time.Sleep(1 * time.Second) 307 | cancel() 308 | }() 309 | taskErrs, err = a.WaitAny(ctx) 310 | } else { 311 | taskErrs, err = a.WaitAny(test.ctx()) 312 | } 313 | 314 | require.Equal(t, test.wantedErr, err) 315 | require.Equal(t, test.wantedErrs, taskErrs) 316 | }) 317 | 318 | } 319 | } 320 | 321 | func TestAwaitN(t *testing.T) { 322 | 323 | wantedErr := errors.New("wanted") 324 | 325 | tests := []struct { 326 | name string 327 | ctx func() context.Context 328 | withCancel bool 329 | setup func() Awaiter 330 | wantedN int 331 | wantedErr error 332 | wantedErrs []error 333 | }{ 334 | { 335 | name: "wait_n_should_work", 336 | ctx: context.Background, 337 | setup: func() Awaiter { 338 | a := NewA(func(ctx context.Context) error { 339 | return nil 340 | }, func(ctx context.Context) error { 341 | return nil 342 | }) 343 | 344 | a.Add(func(ctx context.Context) error { 345 | time.Sleep(1 * time.Second) 346 | return nil 347 | }) 348 | 349 | return a 350 | }, 351 | wantedN: 2, 352 | }, 353 | { 354 | name: "error_n_should_work", 355 | ctx: context.Background, 356 | setup: func() Awaiter { 357 | return NewA(func(ctx context.Context) error { 358 | time.Sleep(1 * time.Second) 359 | return nil 360 | }, func(ctx context.Context) error { 361 | time.Sleep(1 * time.Second) 362 | return nil 363 | }, func(ctx context.Context) error { 364 | return wantedErr 365 | }) 366 | }, 367 | wantedN: 2, 368 | wantedErrs: []error{wantedErr}, 369 | }, 370 | { 371 | name: "context_should_work", 372 | ctx: func() context.Context { 373 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 374 | return ctx 375 | }, 376 | setup: func() Awaiter { 377 | return NewA(func(ctx context.Context) error { 378 | time.Sleep(5 * time.Second) 379 | return nil 380 | }, func(ctx context.Context) error { 381 | time.Sleep(5 * time.Second) 382 | return nil 383 | }, func(ctx context.Context) error { 384 | return nil 385 | }) 386 | }, 387 | wantedErr: context.DeadlineExceeded, 388 | }, 389 | { 390 | name: "cancel_should_work", 391 | ctx: context.Background, 392 | setup: func() Awaiter { 393 | return NewA(func(ctx context.Context) error { 394 | time.Sleep(5 * time.Second) 395 | return nil 396 | }, func(ctx context.Context) error { 397 | time.Sleep(5 * time.Second) 398 | return nil 399 | }, func(ctx context.Context) error { 400 | return nil 401 | }) 402 | }, 403 | withCancel: true, 404 | 405 | wantedErr: context.Canceled, 406 | }, 407 | } 408 | 409 | for _, test := range tests { 410 | t.Run(test.name, func(t *testing.T) { 411 | a := test.setup() 412 | var err error 413 | var taskErrs []error 414 | 415 | if test.withCancel { 416 | ctx, cancel := context.WithCancel(context.Background()) 417 | go func() { 418 | time.Sleep(1 * time.Second) 419 | cancel() 420 | }() 421 | taskErrs, err = a.WaitN(ctx, test.wantedN) 422 | } else { 423 | taskErrs, err = a.WaitN(test.ctx(), test.wantedN) 424 | } 425 | 426 | require.Equal(t, test.wantedErr, err) 427 | require.Equal(t, test.wantedErrs, taskErrs) 428 | 429 | }) 430 | 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /waiter_test.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "slices" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWait(t *testing.T) { 14 | 15 | wantedErr := errors.New("wanted") 16 | wantedErrs := []error{wantedErr} 17 | 18 | tests := []struct { 19 | name string 20 | ctx func() context.Context 21 | withCancel bool 22 | setup func() Waiter[int] 23 | wantedResult []int 24 | wantedErr error 25 | wantedErrs []error 26 | }{ 27 | { 28 | name: "wait_should_work", 29 | ctx: context.Background, 30 | setup: func() Waiter[int] { 31 | a := New[int](func(ctx context.Context) (int, error) { 32 | return 1, nil 33 | }, func(ctx context.Context) (int, error) { 34 | return 2, nil 35 | }) 36 | 37 | a.Add(func(ctx context.Context) (int, error) { 38 | return 3, nil 39 | }) 40 | 41 | return a 42 | }, 43 | wantedResult: []int{1, 2, 3}, 44 | wantedErr: nil, 45 | }, 46 | { 47 | name: "error_should_work", 48 | ctx: context.Background, 49 | setup: func() Waiter[int] { 50 | return New[int](func(ctx context.Context) (int, error) { 51 | return 1, nil 52 | }, func(ctx context.Context) (int, error) { 53 | return 2, nil 54 | }, func(ctx context.Context) (int, error) { 55 | return 0, wantedErr 56 | }) 57 | }, 58 | wantedResult: []int{1, 2}, 59 | wantedErr: ErrTooLessDone, 60 | wantedErrs: wantedErrs, 61 | }, 62 | { 63 | name: "errors_should_work", 64 | ctx: context.Background, 65 | setup: func() Waiter[int] { 66 | return New[int](func(ctx context.Context) (int, error) { 67 | return 0, wantedErr 68 | }, func(ctx context.Context) (int, error) { 69 | return 0, wantedErr 70 | }, func(ctx context.Context) (int, error) { 71 | return 0, wantedErr 72 | }) 73 | }, 74 | wantedErr: ErrTooLessDone, 75 | wantedErrs: []error{wantedErr, wantedErr, wantedErr}, 76 | }, 77 | { 78 | name: "context_should_work", 79 | ctx: func() context.Context { 80 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 81 | return ctx 82 | }, 83 | setup: func() Waiter[int] { 84 | return New[int](func(ctx context.Context) (int, error) { 85 | time.Sleep(5 * time.Second) 86 | return 1, nil 87 | }, func(ctx context.Context) (int, error) { 88 | time.Sleep(5 * time.Second) 89 | return 2, nil 90 | }, func(ctx context.Context) (int, error) { 91 | return 3, nil 92 | }) 93 | }, 94 | wantedResult: []int{3}, 95 | wantedErr: context.DeadlineExceeded, 96 | }, 97 | { 98 | name: "cancel_should_work", 99 | ctx: context.Background, 100 | setup: func() Waiter[int] { 101 | return New[int](func(ctx context.Context) (int, error) { 102 | time.Sleep(5 * time.Second) 103 | return 1, nil 104 | }, func(ctx context.Context) (int, error) { 105 | time.Sleep(5 * time.Second) 106 | return 2, nil 107 | }, func(ctx context.Context) (int, error) { 108 | return 3, nil 109 | }) 110 | }, 111 | withCancel: true, 112 | wantedResult: []int{3}, 113 | wantedErr: context.Canceled, 114 | }, 115 | } 116 | 117 | for _, test := range tests { 118 | t.Run(test.name, func(t *testing.T) { 119 | a := test.setup() 120 | var result []int 121 | var err error 122 | var taskErrs []error 123 | 124 | if test.withCancel { 125 | ctx, cancel := context.WithCancel(context.Background()) 126 | go func() { 127 | time.Sleep(1 * time.Second) 128 | cancel() 129 | }() 130 | result, taskErrs, err = a.Wait(ctx) 131 | } else { 132 | result, taskErrs, err = a.Wait(test.ctx()) 133 | } 134 | 135 | slices.Sort(result) 136 | 137 | require.Equal(t, test.wantedResult, result) 138 | require.Equal(t, test.wantedErr, err) 139 | require.Equal(t, test.wantedErrs, taskErrs) 140 | 141 | }) 142 | 143 | } 144 | } 145 | 146 | func TestWaitAny(t *testing.T) { 147 | 148 | wantedErr := errors.New("wanted") 149 | 150 | tests := []struct { 151 | name string 152 | ctx func() context.Context 153 | withCancel bool 154 | setup func() Waiter[int] 155 | wantedResult int 156 | wantedErr error 157 | wantedErrs []error 158 | }{ 159 | { 160 | name: "1st_should_work", 161 | ctx: context.Background, 162 | setup: func() Waiter[int] { 163 | a := New[int](func(ctx context.Context) (int, error) { 164 | time.Sleep(5 * time.Second) 165 | return 2, nil 166 | }, func(ctx context.Context) (int, error) { 167 | time.Sleep(5 * time.Second) 168 | return 3, nil 169 | }) 170 | 171 | a.Add(func(ctx context.Context) (int, error) { 172 | return 1, nil 173 | }) 174 | 175 | return a 176 | }, 177 | wantedResult: 1, 178 | }, 179 | { 180 | name: "2nd_should_work", 181 | ctx: context.Background, 182 | setup: func() Waiter[int] { 183 | return New[int](func(ctx context.Context) (int, error) { 184 | time.Sleep(5 * time.Second) 185 | return 1, nil 186 | }, func(ctx context.Context) (int, error) { 187 | return 2, nil 188 | }, func(ctx context.Context) (int, error) { 189 | time.Sleep(5 * time.Second) 190 | return 3, nil 191 | }) 192 | }, 193 | wantedResult: 2, 194 | }, 195 | { 196 | name: "3rd_should_work", 197 | ctx: context.Background, 198 | setup: func() Waiter[int] { 199 | return New[int](func(ctx context.Context) (int, error) { 200 | time.Sleep(5 * time.Second) 201 | return 1, nil 202 | }, func(ctx context.Context) (int, error) { 203 | time.Sleep(5 * time.Second) 204 | return 2, nil 205 | }, func(ctx context.Context) (int, error) { 206 | 207 | return 3, nil 208 | }) 209 | }, 210 | wantedResult: 3, 211 | }, 212 | { 213 | name: "slowest_should_work", 214 | ctx: context.Background, 215 | setup: func() Waiter[int] { 216 | return New[int](func(ctx context.Context) (int, error) { 217 | time.Sleep(3 * time.Second) 218 | return 1, nil 219 | }, func(ctx context.Context) (int, error) { 220 | return 0, wantedErr 221 | }, func(ctx context.Context) (int, error) { 222 | return 0, wantedErr 223 | }) 224 | }, 225 | wantedResult: 1, 226 | wantedErrs: []error{wantedErr, wantedErr}, 227 | }, 228 | { 229 | name: "fastest_should_work", 230 | ctx: context.Background, 231 | setup: func() Waiter[int] { 232 | return New[int](func(ctx context.Context) (int, error) { 233 | return 1, nil 234 | }, func(ctx context.Context) (int, error) { 235 | time.Sleep(2 * time.Second) 236 | return 0, wantedErr 237 | }, func(ctx context.Context) (int, error) { 238 | time.Sleep(3 * time.Second) 239 | return 0, wantedErr 240 | }) 241 | }, 242 | wantedResult: 1, 243 | }, 244 | { 245 | name: "errors_should_work", 246 | ctx: context.Background, 247 | setup: func() Waiter[int] { 248 | return New[int](func(ctx context.Context) (int, error) { 249 | return 0, wantedErr 250 | }, func(ctx context.Context) (int, error) { 251 | return 0, wantedErr 252 | }, func(ctx context.Context) (int, error) { 253 | return 0, wantedErr 254 | }) 255 | }, 256 | wantedErr: ErrTooLessDone, 257 | wantedErrs: []error{wantedErr, wantedErr, wantedErr}, 258 | }, 259 | { 260 | name: "error_should_work", 261 | ctx: context.Background, 262 | setup: func() Waiter[int] { 263 | return New[int](func(ctx context.Context) (int, error) { 264 | return 0, wantedErr 265 | }) 266 | }, 267 | wantedErr: ErrTooLessDone, 268 | wantedErrs: []error{wantedErr}, 269 | }, 270 | { 271 | name: "context_should_work", 272 | ctx: func() context.Context { 273 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 274 | return ctx 275 | }, 276 | setup: func() Waiter[int] { 277 | return New[int](func(ctx context.Context) (int, error) { 278 | time.Sleep(5 * time.Second) 279 | return 1, nil 280 | }, func(ctx context.Context) (int, error) { 281 | time.Sleep(5 * time.Second) 282 | return 2, nil 283 | }, func(ctx context.Context) (int, error) { 284 | time.Sleep(5 * time.Second) 285 | return 3, nil 286 | }) 287 | }, 288 | wantedErr: context.DeadlineExceeded, 289 | }, 290 | { 291 | name: "cancel_should_work", 292 | ctx: context.Background, 293 | setup: func() Waiter[int] { 294 | return New[int](func(ctx context.Context) (int, error) { 295 | time.Sleep(5 * time.Second) 296 | return 1, nil 297 | }, func(ctx context.Context) (int, error) { 298 | time.Sleep(5 * time.Second) 299 | return 2, nil 300 | }, func(ctx context.Context) (int, error) { 301 | time.Sleep(5 * time.Second) 302 | return 3, nil 303 | }) 304 | }, 305 | withCancel: true, 306 | wantedErr: context.Canceled, 307 | }, 308 | } 309 | 310 | for _, test := range tests { 311 | t.Run(test.name, func(t *testing.T) { 312 | a := test.setup() 313 | var result int 314 | var err error 315 | var taskErrs []error 316 | 317 | if test.withCancel { 318 | ctx, cancel := context.WithCancel(context.Background()) 319 | go func() { 320 | time.Sleep(1 * time.Second) 321 | cancel() 322 | }() 323 | result, taskErrs, err = a.WaitAny(ctx) 324 | } else { 325 | result, taskErrs, err = a.WaitAny(test.ctx()) 326 | } 327 | 328 | require.Equal(t, test.wantedResult, result) 329 | require.Equal(t, test.wantedErr, err) 330 | require.Equal(t, test.wantedErrs, taskErrs) 331 | }) 332 | 333 | } 334 | } 335 | 336 | func TestWaitN(t *testing.T) { 337 | 338 | wantedErr := errors.New("wanted") 339 | 340 | tests := []struct { 341 | name string 342 | ctx func() context.Context 343 | withCancel bool 344 | setup func() Waiter[int] 345 | wantedN int 346 | wantedResult []int 347 | wantedErr error 348 | wantedErrs []error 349 | }{ 350 | { 351 | name: "wait_n_should_work", 352 | ctx: context.Background, 353 | setup: func() Waiter[int] { 354 | a := New[int](func(ctx context.Context) (int, error) { 355 | return 1, nil 356 | }, func(ctx context.Context) (int, error) { 357 | return 2, nil 358 | }) 359 | 360 | a.Add(func(ctx context.Context) (int, error) { 361 | time.Sleep(1 * time.Second) 362 | return 3, nil 363 | }) 364 | 365 | return a 366 | }, 367 | wantedN: 2, 368 | wantedResult: []int{1, 2}, 369 | }, 370 | { 371 | name: "error_n_should_work", 372 | ctx: context.Background, 373 | setup: func() Waiter[int] { 374 | return New[int](func(ctx context.Context) (int, error) { 375 | time.Sleep(1 * time.Second) 376 | return 1, nil 377 | }, func(ctx context.Context) (int, error) { 378 | time.Sleep(1 * time.Second) 379 | return 2, nil 380 | }, func(ctx context.Context) (int, error) { 381 | return 0, wantedErr 382 | }) 383 | }, 384 | wantedN: 2, 385 | wantedResult: []int{1, 2}, 386 | wantedErrs: []error{wantedErr}, 387 | }, 388 | { 389 | name: "context_should_work", 390 | ctx: func() context.Context { 391 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint 392 | return ctx 393 | }, 394 | setup: func() Waiter[int] { 395 | return New[int](func(ctx context.Context) (int, error) { 396 | time.Sleep(5 * time.Second) 397 | return 1, nil 398 | }, func(ctx context.Context) (int, error) { 399 | time.Sleep(5 * time.Second) 400 | return 2, nil 401 | }, func(ctx context.Context) (int, error) { 402 | return 3, nil 403 | }) 404 | }, 405 | wantedResult: []int{3}, 406 | wantedErr: context.DeadlineExceeded, 407 | }, 408 | { 409 | name: "cancel_should_work", 410 | ctx: context.Background, 411 | setup: func() Waiter[int] { 412 | return New[int](func(ctx context.Context) (int, error) { 413 | time.Sleep(5 * time.Second) 414 | return 1, nil 415 | }, func(ctx context.Context) (int, error) { 416 | time.Sleep(5 * time.Second) 417 | return 2, nil 418 | }, func(ctx context.Context) (int, error) { 419 | return 3, nil 420 | }) 421 | }, 422 | withCancel: true, 423 | wantedResult: []int{3}, 424 | wantedErr: context.Canceled, 425 | }, 426 | } 427 | 428 | for _, test := range tests { 429 | t.Run(test.name, func(t *testing.T) { 430 | a := test.setup() 431 | var result []int 432 | var err error 433 | var taskErrs []error 434 | 435 | if test.withCancel { 436 | ctx, cancel := context.WithCancel(context.Background()) 437 | go func() { 438 | time.Sleep(1 * time.Second) 439 | cancel() 440 | }() 441 | result, taskErrs, err = a.WaitN(ctx, test.wantedN) 442 | } else { 443 | result, taskErrs, err = a.WaitN(test.ctx(), test.wantedN) 444 | } 445 | 446 | slices.Sort(result) 447 | 448 | require.Equal(t, test.wantedResult, result) 449 | require.Equal(t, test.wantedErr, err) 450 | require.Equal(t, test.wantedErrs, taskErrs) 451 | 452 | }) 453 | 454 | } 455 | } 456 | --------------------------------------------------------------------------------