Skip to content

Commit 43bd28b

Browse files
committed
feat: add Until.
1 parent 8fd7c61 commit 43bd28b

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

error.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ var (
1818
// ErrUnmatchedParam indicates the function's parameter list does not match to the list from the
1919
// caller.
2020
ErrUnmatchedParam error = errors.New("parameters are unmatched")
21+
// ErrInvalidTestFunc indicates the test function is invalid.
22+
ErrInvalidTestFunc error = errors.New("invalid test function")
2123
)
2224

2325
type ExecutionError interface {

until.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package async
2+
3+
import (
4+
"context"
5+
"reflect"
6+
)
7+
8+
// Until repeatedly calls the function until the test function returns true. A valid test function
9+
// must match the following requirements.
10+
//
11+
// - The first return value of the test function must be a boolean value.
12+
// - The parameters' number of the test function must be equal to the return values' number of the
13+
// execution function (exclude context).
14+
// - The parameters' types of the test function must be the same or convertible to the return
15+
// values' types of the execution function.
16+
//
17+
// c := 0
18+
// Until(func() bool {
19+
// return c == 5
20+
// }, func() {
21+
// c++
22+
// })
23+
func Until(testFn, fn AsyncFn) ([]any, error) {
24+
return until(context.Background(), testFn, fn)
25+
}
26+
27+
// UntilWithContext repeatedly calls the function with the specified context until the test
28+
// function returns true.
29+
func UntilWithContext(ctx context.Context, testFn, fn AsyncFn) ([]any, error) {
30+
return until(ctx, testFn, fn)
31+
}
32+
33+
// until repeatedly calls the function until the test function returns true.
34+
func until(parent context.Context, testFn, fn AsyncFn) ([]any, error) {
35+
validateUntilFuncs(testFn, fn)
36+
37+
ctx := getContext(parent)
38+
39+
for {
40+
out, err := invokeAsyncFn(fn, ctx, nil)
41+
42+
testOut, _ := invokeAsyncFn(testFn, ctx, out)
43+
isDone := testOut[0].(bool)
44+
if isDone {
45+
return out, err
46+
}
47+
}
48+
}
49+
50+
// validateUntilFuncs validates the test function and the execution function.
51+
func validateUntilFuncs(testFn, fn AsyncFn) {
52+
if testFn == nil || fn == nil {
53+
panic(ErrNotFunction)
54+
}
55+
tft := reflect.TypeOf(testFn) // reflect.Type of the test function
56+
ft := reflect.TypeOf(fn) // reflect.Type of the function
57+
if tft.Kind() != reflect.Func || ft.Kind() != reflect.Func {
58+
panic(ErrNotFunction)
59+
}
60+
61+
if tft.NumOut() <= 0 || tft.Out(0).Kind() != reflect.Bool {
62+
panic(ErrInvalidTestFunc)
63+
}
64+
65+
ii := 0 // index of the test function input parameters list
66+
oi := 0 // index of the function return values list
67+
numIn := tft.NumIn()
68+
isTakeContext := isFuncTakesContext(tft)
69+
if isTakeContext {
70+
numIn--
71+
ii++
72+
}
73+
if numIn != ft.NumOut() {
74+
panic(ErrInvalidTestFunc)
75+
}
76+
77+
for oi < numIn {
78+
it := tft.In(ii) // type of the value in the test function input parameters list
79+
ot := ft.Out(oi) // type of the value in the function return values list
80+
81+
if it != ot && !it.ConvertibleTo(ot) {
82+
panic(ErrInvalidTestFunc)
83+
}
84+
85+
ii++
86+
oi++
87+
}
88+
}

until_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package async
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/ghosind/go-assert"
9+
)
10+
11+
func TestUntil(t *testing.T) {
12+
a := assert.New(t)
13+
count := 0
14+
15+
out, err := Until(func(c int) bool {
16+
return c == 5
17+
}, func() int {
18+
count++
19+
return count
20+
})
21+
a.NilNow(err)
22+
a.EqualNow(out, []any{5})
23+
}
24+
25+
func TestUntilInvalidParameters(t *testing.T) {
26+
a := assert.New(t)
27+
28+
a.PanicOfNow(func() {
29+
Until(nil, func() {})
30+
}, ErrNotFunction)
31+
a.PanicOfNow(func() {
32+
Until(func() {}, nil)
33+
}, ErrNotFunction)
34+
a.PanicOfNow(func() {
35+
Until(1, "hello")
36+
}, ErrNotFunction)
37+
a.PanicOfNow(func() {
38+
Until(func() {}, func() {})
39+
}, ErrInvalidTestFunc)
40+
a.NotPanicNow(func() {
41+
Until(func() bool { return true }, func() {})
42+
})
43+
a.NotPanicNow(func() {
44+
Until(func(err error) bool { return true }, func() error { return nil })
45+
})
46+
a.NotPanicNow(func() {
47+
Until(func(ctx context.Context, err error) bool { return true }, func() error { return nil })
48+
})
49+
a.PanicOfNow(func() {
50+
Until(func(ctx context.Context, i int) bool { return true }, func() error { return nil })
51+
}, ErrInvalidTestFunc)
52+
a.PanicOfNow(func() {
53+
Until(func(ctx context.Context, i int) bool { return true }, func() {})
54+
}, ErrInvalidTestFunc)
55+
}
56+
57+
func TestUntilWithContext(t *testing.T) {
58+
a := assert.New(t)
59+
ctx, canFunc := context.WithTimeout(context.Background(), 100*time.Millisecond)
60+
defer canFunc()
61+
62+
start := time.Now()
63+
out, err := UntilWithContext(ctx, func(ctx context.Context) bool {
64+
select {
65+
case <-ctx.Done():
66+
return true
67+
default:
68+
return false
69+
}
70+
}, func() {
71+
})
72+
a.NilNow(err)
73+
a.EqualNow(out, []any{})
74+
dur := time.Since(start)
75+
a.GteNow(dur, 100*time.Millisecond)
76+
a.LteNow(dur, 150*time.Millisecond)
77+
}

0 commit comments

Comments
 (0)