From 8564fc1e2426bc6f1fa7df60106a880678ada4e0 Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:30:14 +0800 Subject: [PATCH] feat: support to run tests with multiple threads (#13) Co-authored-by: rick --- cmd/run.go | 69 ++++++++++++++++++++++++++++++++++--- cmd/run_test.go | 2 -- go.mod | 1 + go.sum | 2 ++ main.go | 3 +- pkg/render/doc.go | 2 ++ pkg/render/template.go | 21 +++++++++++ pkg/render/template_test.go | 33 ++++++++++++++++++ pkg/runner/simple.go | 5 ++- pkg/testing/parser.go | 42 +++++++++------------- 10 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 pkg/render/doc.go create mode 100644 pkg/render/template.go create mode 100644 pkg/render/template_test.go diff --git a/cmd/run.go b/cmd/run.go index 9e1b6bd..8a93803 100644 --- a/cmd/run.go +++ b/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, "/") { diff --git a/cmd/run_test.go b/cmd/run_test.go index 09791b0..8e3d9fc 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -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) }) diff --git a/go.mod b/go.mod index a8302c5..52b92ac 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 3db7b66..3b99749 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index d3e570d..90549b0 100644 --- a/main.go +++ b/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()) diff --git a/pkg/render/doc.go b/pkg/render/doc.go new file mode 100644 index 0000000..bb4eb94 --- /dev/null +++ b/pkg/render/doc.go @@ -0,0 +1,2 @@ +// Package render provides a simple way to render as template +package render diff --git a/pkg/render/template.go b/pkg/render/template.go new file mode 100644 index 0000000..84927ec --- /dev/null +++ b/pkg/render/template.go @@ -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 +} diff --git a/pkg/render/template_test.go b/pkg/render/template_test.go new file mode 100644 index 0000000..182df13 --- /dev/null +++ b/pkg/render/template_test.go @@ -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) + }) + } +} diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index a0bff44..ee0d7d9 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -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) diff --git a/pkg/testing/parser.go b/pkg/testing/parser.go index a5a13ca..6e3fd61 100644 --- a/pkg/testing/parser.go +++ b/pkg/testing/parser.go @@ -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 } }