├── .github ├── dependabot.yml └── workflows │ ├── assign.yml │ ├── automerge.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── constraint └── constraint.go ├── go.mod ├── go.sum └── wait ├── wait.go ├── wait_example_test.go └── wait_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | on: 3 | issues: 4 | types: [ opened ] 5 | jobs: 6 | auto-assign: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: pozil/auto-assign-issue@v2 10 | if: github.actor != 'dependabot[bot]' 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | assignees: elgohr -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: 4 | pull_request_target: 5 | branches: [ main ] 6 | types: [ opened ] 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | enableAutoMerge: 14 | runs-on: ubuntu-latest 15 | if: github.event.pull_request.user.login == 'dependabot[bot]' 16 | steps: 17 | - name: Enable auto-merge for Dependabot PRs 18 | run: gh pr merge --auto --merge "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: '^1.18' 18 | - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 19 | - uses: codecov/codecov-action@v5 20 | with: 21 | file: coverage.txt 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lars Gohr 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stop-and-go 2 | [![Actions Status](https://github.com/elgohr/stop-and-go/workflows/Test/badge.svg)](https://github.com/elgohr/stop-and-go/actions) 3 | [![codecov](https://codecov.io/gh/elgohr/stop-and-go/branch/master/graph/badge.svg)](https://codecov.io/gh/elgohr/stop-and-go) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/elgohr/stop-and-go)](https://goreportcard.com/report/github.com/elgohr/stop-and-go) 5 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/elgohr/stop-and-go)](https://pkg.go.dev/github.com/elgohr/stop-and-go) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | Testing helper for concurrency 9 | 10 | ## Install 11 | 12 | ```bash 13 | go get -u github.com/elgohr/stop-and-go 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```go 19 | func TestExample(t *testing.T) { 20 | w1 := wait.NewWaiter(time.Second) 21 | w2 := wait.NewWaiter(time.Second) 22 | w3 := wait.NewWaiter(time.Second) 23 | 24 | ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | w.WriteHeader(http.StatusOK) 26 | w2.Done() 27 | })) 28 | defer ts1.Close() 29 | 30 | go func() { 31 | w3.Done() 32 | }() 33 | 34 | go func() { 35 | if _, err := http.Get(ts1.URL); err != nil { 36 | t.Error(err) 37 | } 38 | w1.Done() 39 | }() 40 | 41 | if err := wait.For( 42 | constraint.NoOrder(w3), 43 | constraint.Before(w1, w2), 44 | ); err != nil { 45 | t.Error(err) 46 | } 47 | } 48 | ``` -------------------------------------------------------------------------------- /constraint/constraint.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | import ( 4 | "github.com/elgohr/stop-and-go/wait" 5 | ) 6 | 7 | // NoOrder is used when the order of the Waiter does not matter to other Waiters 8 | func NoOrder(w wait.Waiter) func(wts []wait.Waiter) []wait.Waiter { 9 | return func(wts []wait.Waiter) []wait.Waiter { 10 | return append(wts, w) 11 | } 12 | } 13 | 14 | // Before is used when a Waiter has to be called before another Waiter 15 | func Before(first wait.Waiter, second wait.Waiter) func(wts []wait.Waiter) []wait.Waiter { 16 | return func(wts []wait.Waiter) []wait.Waiter { 17 | wts, fi := contains(wts, first) 18 | wts, si := contains(wts, second) 19 | return sort(wts, fi, si) 20 | } 21 | } 22 | 23 | func sort(wts []wait.Waiter, fi int, si int) []wait.Waiter { 24 | if fi > si { 25 | v := wts[fi] 26 | wts[fi] = wts[si] 27 | wts[si] = v 28 | } 29 | return wts 30 | } 31 | 32 | func contains(wts []wait.Waiter, nw wait.Waiter) ([]wait.Waiter, int) { 33 | for i, w := range wts { 34 | if w == nw { 35 | return wts, i 36 | } 37 | } 38 | wts = append(wts, nw) 39 | return wts, len(wts) - 1 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elgohr/stop-and-go 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.10.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 | -------------------------------------------------------------------------------- /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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.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 | -------------------------------------------------------------------------------- /wait/wait.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Waiter represents a point in tests to wait for 9 | type Waiter struct { 10 | timeout time.Duration 11 | w chan struct{} 12 | } 13 | 14 | // NewWaiter constructs a new Waiter 15 | // Needs a timeout, which is the longest time to wait for the Waiter 16 | func NewWaiter(timeout time.Duration) Waiter { 17 | return Waiter{ 18 | timeout: timeout, 19 | w: make(chan struct{}, 1), 20 | } 21 | } 22 | 23 | // Done marks the Waiter as called 24 | func (w *Waiter) Done() { 25 | w.w <- struct{}{} 26 | } 27 | 28 | // Option is used to configure the dependencies for Waiter 29 | type Option func(w []Waiter) []Waiter 30 | 31 | // For provides a way to configure dependencies between Waiters 32 | // It errors when at least one Waiter hasn't been called 33 | func For(opts ...Option) error { 34 | waiters := []Waiter{} 35 | for _, opt := range opts { 36 | waiters = opt(waiters) 37 | } 38 | for i, w := range waiters { 39 | select { 40 | case <-w.w: 41 | 42 | case <-time.Tick(w.timeout): 43 | return fmt.Errorf("failed to wait on waiter %d of %d", i+1, len(waiters)) 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /wait/wait_example_test.go: -------------------------------------------------------------------------------- 1 | package wait_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/elgohr/stop-and-go/constraint" 6 | "github.com/elgohr/stop-and-go/wait" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "time" 11 | ) 12 | 13 | func ExampleFor() { 14 | w1 := wait.NewWaiter(time.Second) 15 | w2 := wait.NewWaiter(time.Second) 16 | w3 := wait.NewWaiter(time.Second) 17 | 18 | ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | w.WriteHeader(http.StatusOK) 20 | w2.Done() 21 | })) 22 | defer ts1.Close() 23 | 24 | go func() { 25 | w3.Done() 26 | }() 27 | 28 | go func() { 29 | _, err := http.Get(ts1.URL) 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | w1.Done() 34 | }() 35 | 36 | fmt.Println(wait.For( 37 | constraint.NoOrder(w3), 38 | constraint.Before(w1, w2), 39 | )) 40 | // Output: 41 | } 42 | 43 | func ExampleFailing() { 44 | w1 := wait.NewWaiter(time.Second) 45 | fmt.Println(wait.For(constraint.NoOrder(w1))) 46 | // Output: failed to wait on waiter 1 of 1 47 | } 48 | -------------------------------------------------------------------------------- /wait/wait_test.go: -------------------------------------------------------------------------------- 1 | package wait_test 2 | 3 | import ( 4 | "github.com/elgohr/stop-and-go/constraint" 5 | "github.com/elgohr/stop-and-go/wait" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestWait(t *testing.T) { 15 | w1 := wait.NewWaiter(time.Second) 16 | w2 := wait.NewWaiter(2 * time.Second) 17 | w3 := wait.NewWaiter(3 * time.Second) 18 | w4 := wait.NewWaiter(4 * time.Second) 19 | var ( 20 | calledW1 bool 21 | calledW2 bool 22 | calledW3 bool 23 | calledW4 bool 24 | ) 25 | 26 | ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | w.WriteHeader(http.StatusOK) 28 | calledW2 = true 29 | w2.Done() 30 | })) 31 | defer ts1.Close() 32 | 33 | ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | w.WriteHeader(http.StatusOK) 35 | calledW4 = true 36 | w4.Done() 37 | })) 38 | defer ts2.Close() 39 | 40 | go func() { 41 | calledW1 = true 42 | w1.Done() 43 | }() 44 | go func() { 45 | calledW3 = true 46 | assert.False(t, calledW2) 47 | _, err := http.Get(ts1.URL) 48 | require.NoError(t, err) 49 | _, err = http.Get(ts2.URL) 50 | require.NoError(t, err) 51 | w3.Done() 52 | }() 53 | 54 | require.NoError(t, wait.For( 55 | constraint.NoOrder(w1), 56 | constraint.Before(w3, w2), 57 | constraint.Before(w3, w4), 58 | )) 59 | 60 | require.True(t, calledW1) 61 | require.True(t, calledW2) 62 | require.True(t, calledW3) 63 | require.True(t, calledW4) 64 | } 65 | 66 | func TestWait_Errors(t *testing.T) { 67 | w1 := wait.NewWaiter(time.Millisecond) 68 | require.EqualError(t, wait.For(constraint.NoOrder(w1)), "failed to wait on waiter 1 of 1") 69 | } 70 | 71 | func TestWait_ErrorsWithMultiple(t *testing.T) { 72 | w1 := wait.NewWaiter(time.Millisecond) 73 | w2 := wait.NewWaiter(time.Millisecond) 74 | go func() { 75 | w1.Done() 76 | }() 77 | require.EqualError(t, wait.For(constraint.Before(w1, w2)), "failed to wait on waiter 2 of 2") 78 | } 79 | 80 | func TestWait_ErrorsWithMultipleUncalled(t *testing.T) { 81 | w1 := wait.NewWaiter(time.Millisecond) 82 | w2 := wait.NewWaiter(time.Millisecond) 83 | w3 := wait.NewWaiter(time.Millisecond) 84 | w4 := wait.NewWaiter(time.Millisecond) 85 | w5 := wait.NewWaiter(time.Millisecond) 86 | require.EqualError(t, wait.For( 87 | constraint.Before(w1, w2), 88 | constraint.Before(w3, w1), 89 | constraint.Before(w4, w2), 90 | constraint.Before(w3, w4), 91 | constraint.Before(w5, w1), 92 | ), "failed to wait on waiter 1 of 5") 93 | } 94 | --------------------------------------------------------------------------------