Skip to content

Commit d862afa

Browse files
committed
feat: support to set the request timeout (#15)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
1 parent 8564fc1 commit d862afa

File tree

6 files changed

+195
-27
lines changed

6 files changed

+195
-27
lines changed

cmd/run.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010
"time"
1111

12+
"github.com/linuxsuren/api-testing/pkg/limit"
1213
"github.com/linuxsuren/api-testing/pkg/render"
1314
"github.com/linuxsuren/api-testing/pkg/runner"
1415
"github.com/linuxsuren/api-testing/pkg/testing"
@@ -17,10 +18,16 @@ import (
1718
)
1819

1920
type runOption struct {
20-
pattern string
21-
duration time.Duration
22-
thread int64
23-
context context.Context
21+
pattern string
22+
duration time.Duration
23+
requestTimeout time.Duration
24+
requestIgnoreError bool
25+
thread int64
26+
context context.Context
27+
qps int32
28+
burst int32
29+
limiter limit.RateLimiter
30+
startTime time.Time
2431
}
2532

2633
// CreateRunCommand returns the run command
@@ -40,13 +47,23 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
4047
flags.StringVarP(&opt.pattern, "pattern", "p", "test-suite-*.yaml",
4148
"The file pattern which try to execute the test cases")
4249
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
50+
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
51+
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
4352
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
53+
flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS")
54+
flags.Int32VarP(&opt.burst, "burst", "", 5, "burst")
4455
return
4556
}
4657

4758
func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
4859
var files []string
60+
o.startTime = time.Now()
4961
o.context = cmd.Context()
62+
o.limiter = limit.NewDefaultRateLimiter(o.qps, o.burst)
63+
defer func() {
64+
cmd.Printf("consume: %s\n", time.Now().Sub(o.startTime).String())
65+
o.limiter.Stop()
66+
}()
5067

5168
if files, err = filepath.Glob(o.pattern); err == nil {
5269
for i := range files {
@@ -69,13 +86,15 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
6986
// make sure having a valid timer
7087
timeout = time.NewTicker(time.Second)
7188
}
72-
errChannel := make(chan error, 10)
89+
errChannel := make(chan error, 10*o.thread)
90+
stopSingal := make(chan struct{}, 1)
7391
var wait sync.WaitGroup
7492

7593
for !stop {
7694
select {
7795
case <-timeout.C:
7896
stop = true
97+
stopSingal <- struct{}{}
7998
case err = <-errChannel:
8099
if err != nil {
81100
stop = true
@@ -85,32 +104,38 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
85104
continue
86105
}
87106
wait.Add(1)
88-
if o.duration <= 0 {
89-
stop = true
90-
}
91107

92-
go func(ch chan error) {
108+
go func(ch chan error, sem *semaphore.Weighted) {
109+
now := time.Now()
93110
defer sem.Release(1)
94111
defer wait.Done()
112+
defer func() {
113+
fmt.Println("routing end with", time.Now().Sub(now))
114+
}()
95115

96-
ctx := getDefaultContext()
97-
ch <- runSuite(suite, ctx)
98-
}(errChannel)
116+
dataContext := getDefaultContext()
117+
ch <- o.runSuite(suite, dataContext, o.context, stopSingal)
118+
}(errChannel, sem)
99119
}
100120
}
101-
err = <-errChannel
121+
122+
select {
123+
case err = <-errChannel:
124+
case <-stopSingal:
125+
}
126+
102127
wait.Wait()
103128
return
104129
}
105130

106-
func runSuite(suite string, ctx map[string]interface{}) (err error) {
131+
func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, ctx context.Context, stopSingal chan struct{}) (err error) {
107132
var testSuite *testing.TestSuite
108133
if testSuite, err = testing.Parse(suite); err != nil {
109134
return
110135
}
111136

112137
var result string
113-
if result, err = render.Render("base api", testSuite.API, ctx); err == nil {
138+
if result, err = render.Render("base api", testSuite.API, dataContext); err == nil {
114139
testSuite.API = result
115140
testSuite.API = strings.TrimSuffix(testSuite.API, "/")
116141
} else {
@@ -123,12 +148,25 @@ func runSuite(suite string, ctx map[string]interface{}) (err error) {
123148
testCase.Request.API = fmt.Sprintf("%s%s", testSuite.API, testCase.Request.API)
124149
}
125150

126-
setRelativeDir(suite, &testCase)
127151
var output interface{}
128-
if output, err = runner.RunTestCase(&testCase, ctx); err != nil {
152+
select {
153+
case <-stopSingal:
129154
return
155+
default:
156+
// reuse the API prefix
157+
if strings.HasPrefix(testCase.Request.API, "/") {
158+
testCase.Request.API = fmt.Sprintf("%s%s", testSuite.API, testCase.Request.API)
159+
}
160+
161+
setRelativeDir(suite, &testCase)
162+
o.limiter.Accept()
163+
164+
ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout)
165+
if output, err = runner.RunTestCase(&testCase, dataContext, ctxWithTimeout); err != nil && !o.requestIgnoreError {
166+
return
167+
}
130168
}
131-
ctx[testCase.Name] = output
169+
dataContext[testCase.Name] = output
132170
}
133171
return
134172
}

cmd/run_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package cmd
22

33
import (
4+
"context"
45
"net/http"
56
"testing"
7+
"time"
68

79
"github.com/h2non/gock"
10+
"github.com/linuxsuren/api-testing/pkg/limit"
811
"github.com/spf13/cobra"
912
"github.com/stretchr/testify/assert"
1013
)
@@ -46,7 +49,13 @@ func TestRunSuite(t *testing.T) {
4649

4750
tt.prepare()
4851
ctx := getDefaultContext()
49-
err := runSuite(tt.suiteFile, ctx)
52+
opt := &runOption{
53+
requestTimeout: 30 * time.Second,
54+
limiter: limit.NewDefaultRateLimiter(0, 0),
55+
}
56+
stopSingal := make(chan struct{}, 1)
57+
58+
err := opt.runSuite(tt.suiteFile, ctx, context.TODO(), stopSingal)
5059
assert.Equal(t, tt.hasError, err != nil, err)
5160
})
5261
}

pkg/limit/limiter.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package limit
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type RateLimiter interface {
9+
TryAccept() bool
10+
Accept()
11+
Stop()
12+
Burst() int32
13+
}
14+
15+
type defaultRateLimiter struct {
16+
qps int32
17+
burst int32
18+
lastToken time.Time
19+
singal chan struct{}
20+
mu sync.Mutex
21+
}
22+
23+
func NewDefaultRateLimiter(qps, burst int32) RateLimiter {
24+
if qps <= 0 {
25+
qps = 5
26+
}
27+
if burst <= 0 {
28+
burst = 5
29+
}
30+
limiter := &defaultRateLimiter{
31+
qps: qps,
32+
burst: burst,
33+
singal: make(chan struct{}, 1),
34+
}
35+
go limiter.updateBurst()
36+
return limiter
37+
}
38+
39+
func (r *defaultRateLimiter) TryAccept() bool {
40+
_, ok := r.resver()
41+
return ok
42+
}
43+
44+
func (r *defaultRateLimiter) resver() (delay time.Duration, ok bool) {
45+
delay = time.Now().Sub(r.lastToken) / time.Millisecond
46+
r.lastToken = time.Now()
47+
if delay > 0 {
48+
ok = true
49+
} else if r.Burst() > 0 {
50+
r.Setburst(r.Burst() - 1)
51+
ok = true
52+
} else {
53+
delay = time.Second / time.Duration(r.qps)
54+
}
55+
return
56+
}
57+
58+
func (r *defaultRateLimiter) Accept() {
59+
delay, ok := r.resver()
60+
if ok {
61+
return
62+
}
63+
64+
if delay > 0 {
65+
time.Sleep(delay)
66+
}
67+
return
68+
}
69+
70+
func (r *defaultRateLimiter) Setburst(burst int32) {
71+
r.mu.Lock()
72+
defer r.mu.Unlock()
73+
r.burst = burst
74+
}
75+
76+
func (r *defaultRateLimiter) Burst() int32 {
77+
r.mu.Lock()
78+
defer r.mu.Unlock()
79+
return r.burst
80+
}
81+
82+
func (r *defaultRateLimiter) Stop() {
83+
r.singal <- struct{}{}
84+
}
85+
86+
func (r *defaultRateLimiter) updateBurst() {
87+
for {
88+
select {
89+
case <-time.After(time.Second):
90+
r.Setburst(r.Burst() + r.qps)
91+
case <-r.singal:
92+
return
93+
}
94+
}
95+
}

pkg/limit/limiter_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package limit
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestXxx(t *testing.T) {
11+
limiter := NewDefaultRateLimiter(1, 1)
12+
num := 0
13+
14+
loop := true
15+
go func(l RateLimiter) {
16+
for loop {
17+
l.Accept()
18+
num += 1
19+
}
20+
}(limiter)
21+
22+
select {
23+
case <-time.After(time.Second):
24+
loop = false
25+
}
26+
assert.True(t, num <= 10)
27+
}

pkg/runner/simple.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package runner
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -11,7 +12,6 @@ import (
1112
"os"
1213
"reflect"
1314
"strings"
14-
"time"
1515

1616
"github.com/andreyvit/diff"
1717
"github.com/antonmedv/expr"
@@ -22,7 +22,7 @@ import (
2222
)
2323

2424
// RunTestCase runs the test case
25-
func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{}, err error) {
25+
func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error) {
2626
fmt.Printf("start to run: '%s'\n", testcase.Name)
2727
if err = doPrepare(testcase); err != nil {
2828
err = fmt.Errorf("failed to prepare, error: %v", err)
@@ -37,9 +37,7 @@ func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{
3737
}
3838
}()
3939

40-
client := http.Client{
41-
Timeout: time.Second * 30,
42-
}
40+
client := http.Client{}
4341
var requestBody io.Reader
4442
if testcase.Request.Body != "" {
4543
requestBody = bytes.NewBufferString(testcase.Request.Body)
@@ -51,7 +49,7 @@ func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{
5149
requestBody = bytes.NewBufferString(string(data))
5250
}
5351

54-
if err = testcase.Request.Render(ctx); err != nil {
52+
if err = testcase.Request.Render(dataContext); err != nil {
5553
return
5654
}
5755

@@ -76,7 +74,7 @@ func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{
7674
}
7775

7876
var request *http.Request
79-
if request, err = http.NewRequest(testcase.Request.Method, testcase.Request.API, requestBody); err != nil {
77+
if request, err = http.NewRequestWithContext(ctx, testcase.Request.Method, testcase.Request.API, requestBody); err != nil {
8078
return
8179
}
8280

pkg/runner/simple_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package runner
22

33
import (
4+
"context"
45
"errors"
56
"net/http"
67
"testing"
@@ -365,7 +366,7 @@ func TestTestCase(t *testing.T) {
365366
if tt.prepare != nil {
366367
tt.prepare()
367368
}
368-
output, err := RunTestCase(tt.testCase, tt.ctx)
369+
output, err := RunTestCase(tt.testCase, tt.ctx, context.TODO())
369370
tt.verify(t, output, err)
370371
})
371372
}

0 commit comments

Comments
 (0)