Skip to content

feat: support to run tests with multiple threads #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions cmd/run.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -31,31 +39,84 @@ 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
}
}
}
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, "/") {
Expand Down
2 changes: 0 additions & 2 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"fmt"
"net/http"
"testing"

Expand Down Expand Up @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (

func main() {
cmd := &cobra.Command{
Use: "atest",
Use: "atest",
Short: "API testing tool",
}
cmd.AddCommand(c.CreateInitCommand(), c.CreateRunCommand())

Expand Down
2 changes: 2 additions & 0 deletions pkg/render/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package render provides a simple way to render as template
package render
21 changes: 21 additions & 0 deletions pkg/render/template.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions pkg/render/template_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
5 changes: 4 additions & 1 deletion pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"reflect"
"strings"
"time"

"github.com/andreyvit/diff"
"github.com/antonmedv/expr"
Expand All @@ -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)
Expand Down
42 changes: 17 additions & 25 deletions pkg/testing/parser.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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 != "" {
Expand All @@ -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
}
}

Expand Down