feat: support to run tests with multiple threads (#13)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
This commit is contained in:
parent
7c4f12c79e
commit
8564fc1e24
69
cmd/run.go
69
cmd/run.go
|
@ -1,18 +1,26 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/render"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
type runOption struct {
|
||||
pattern string
|
||||
pattern string
|
||||
duration time.Duration
|
||||
thread int64
|
||||
context context.Context
|
||||
}
|
||||
|
||||
// CreateRunCommand returns the run command
|
||||
|
@ -31,17 +39,19 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
|
|||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opt.pattern, "pattern", "p", "test-suite-*.yaml",
|
||||
"The file pattern which try to execute the test cases")
|
||||
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
|
||||
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
|
||||
return
|
||||
}
|
||||
|
||||
func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
|
||||
var files []string
|
||||
ctx := getDefaultContext()
|
||||
o.context = cmd.Context()
|
||||
|
||||
if files, err = filepath.Glob(o.pattern); err == nil {
|
||||
for i := range files {
|
||||
item := files[i]
|
||||
if err = runSuite(item, ctx); err != nil {
|
||||
if err = o.runSuiteWithDuration(item); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -49,13 +59,64 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (o *runOption) runSuiteWithDuration(suite string) (err error) {
|
||||
sem := semaphore.NewWeighted(o.thread)
|
||||
stop := false
|
||||
var timeout *time.Ticker
|
||||
if o.duration > 0 {
|
||||
timeout = time.NewTicker(o.duration)
|
||||
} else {
|
||||
// make sure having a valid timer
|
||||
timeout = time.NewTicker(time.Second)
|
||||
}
|
||||
errChannel := make(chan error, 10)
|
||||
var wait sync.WaitGroup
|
||||
|
||||
for !stop {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
stop = true
|
||||
case err = <-errChannel:
|
||||
if err != nil {
|
||||
stop = true
|
||||
}
|
||||
default:
|
||||
if err := sem.Acquire(o.context, 1); err != nil {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
if o.duration <= 0 {
|
||||
stop = true
|
||||
}
|
||||
|
||||
go func(ch chan error) {
|
||||
defer sem.Release(1)
|
||||
defer wait.Done()
|
||||
|
||||
ctx := getDefaultContext()
|
||||
ch <- runSuite(suite, ctx)
|
||||
}(errChannel)
|
||||
}
|
||||
}
|
||||
err = <-errChannel
|
||||
wait.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
func runSuite(suite string, ctx map[string]interface{}) (err error) {
|
||||
var testSuite *testing.TestSuite
|
||||
if testSuite, err = testing.Parse(suite); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
testSuite.API = strings.TrimSuffix(testSuite.API, "/")
|
||||
var result string
|
||||
if result, err = render.Render("base api", testSuite.API, ctx); err == nil {
|
||||
testSuite.API = result
|
||||
testSuite.API = strings.TrimSuffix(testSuite.API, "/")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
for _, testCase := range testSuite.Items {
|
||||
// reuse the API prefix
|
||||
if strings.HasPrefix(testCase.Request.API, "/") {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
|
@ -89,7 +88,6 @@ func TestRunCommand(t *testing.T) {
|
|||
|
||||
root.SetArgs(append([]string{"run"}, tt.args...))
|
||||
|
||||
fmt.Println(tt.args)
|
||||
err := root.Execute()
|
||||
assert.Equal(t, tt.hasErr, err != nil, err)
|
||||
})
|
||||
|
|
1
go.mod
1
go.mod
|
@ -30,5 +30,6 @@ require (
|
|||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/crypto v0.3.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -72,6 +72,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
3
main.go
3
main.go
|
@ -9,7 +9,8 @@ import (
|
|||
|
||||
func main() {
|
||||
cmd := &cobra.Command{
|
||||
Use: "atest",
|
||||
Use: "atest",
|
||||
Short: "API testing tool",
|
||||
}
|
||||
cmd.AddCommand(c.CreateInitCommand(), c.CreateRunCommand())
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// Package render provides a simple way to render as template
|
||||
package render
|
|
@ -0,0 +1,21 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
)
|
||||
|
||||
// Render render then return the result
|
||||
func Render(name, text string, ctx interface{}) (result string, err error) {
|
||||
var tpl *template.Template
|
||||
if tpl, err = template.New(name).Funcs(sprig.FuncMap()).Parse(text); err == nil {
|
||||
buf := new(bytes.Buffer)
|
||||
if err = tpl.Execute(buf, ctx); err == nil {
|
||||
result = strings.TrimSpace(buf.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
ctx interface{}
|
||||
expect string
|
||||
}{{
|
||||
name: "default",
|
||||
text: `{{default "hello" .Bar}}`,
|
||||
ctx: nil,
|
||||
expect: "hello",
|
||||
}, {
|
||||
name: "trim",
|
||||
text: `{{trim " hello "}}`,
|
||||
ctx: "",
|
||||
expect: "hello",
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Render(tt.name, tt.text, tt.ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.expect, result)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andreyvit/diff"
|
||||
"github.com/antonmedv/expr"
|
||||
|
@ -36,7 +37,9 @@ func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{
|
|||
}
|
||||
}()
|
||||
|
||||
client := http.Client{}
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
}
|
||||
var requestBody io.Reader
|
||||
if testcase.Request.Body != "" {
|
||||
requestBody = bytes.NewBufferString(testcase.Request.Body)
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/linuxsuren/api-testing/pkg/render"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -24,15 +22,12 @@ func Parse(configFile string) (testSuite *TestSuite, err error) {
|
|||
// Render injects the template based context
|
||||
func (r *Request) Render(ctx interface{}) (err error) {
|
||||
// template the API
|
||||
var tpl *template.Template
|
||||
if tpl, err = template.New("api").Funcs(sprig.FuncMap()).Parse(r.API); err != nil {
|
||||
var result string
|
||||
if result, err = render.Render("api", r.API, ctx); err == nil {
|
||||
r.API = result
|
||||
} else {
|
||||
return
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err = tpl.Execute(buf, ctx); err != nil {
|
||||
return
|
||||
}
|
||||
r.API = buf.String()
|
||||
|
||||
// read body from file
|
||||
if r.BodyFromFile != "" {
|
||||
|
@ -45,29 +40,26 @@ func (r *Request) Render(ctx interface{}) (err error) {
|
|||
|
||||
// template the header
|
||||
for key, val := range r.Header {
|
||||
if tpl, err = template.New("header").Funcs(sprig.FuncMap()).Parse(val); err == nil {
|
||||
buf = new(bytes.Buffer)
|
||||
if err = tpl.Execute(buf, ctx); err == nil {
|
||||
r.Header[key] = buf.String()
|
||||
}
|
||||
if result, err = render.Render("header", val, ctx); err == nil {
|
||||
r.Header[key] = result
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// template the body
|
||||
if tpl, err = template.New("body").Funcs(sprig.FuncMap()).Parse(r.Body); err == nil {
|
||||
buf = new(bytes.Buffer)
|
||||
if err = tpl.Execute(buf, ctx); err == nil {
|
||||
r.Body = buf.String()
|
||||
}
|
||||
if result, err = render.Render("body", r.Body, ctx); err == nil {
|
||||
r.Body = result
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// template the form
|
||||
for key, val := range r.Form {
|
||||
if tpl, err = template.New("form").Funcs(sprig.FuncMap()).Parse(val); err == nil {
|
||||
buf = new(bytes.Buffer)
|
||||
if err = tpl.Execute(buf, ctx); err == nil {
|
||||
r.Form[key] = buf.String()
|
||||
}
|
||||
if result, err = render.Render("form", val, ctx); err == nil {
|
||||
r.Form[key] = result
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue