From 675ded099ddf5e1663c557a0403e68e09e9c7497 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Jan 2022 15:18:30 +0800 Subject: [PATCH 01/11] refactor: plugin structure --- parser.go | 24 +++++- plugin/{shared => common}/call.go | 2 +- plugin/{shared => common}/call_test.go | 2 +- .../common/go_plugin_test.go | 41 ++++------ .../common/hashicorp_plugin_test.go | 33 ++++---- plugin.go => plugin/common/init.go | 82 ++++++++----------- plugin/plugin.go | 3 +- runner.go | 6 +- 8 files changed, 97 insertions(+), 96 deletions(-) rename plugin/{shared => common}/call.go (99%) rename plugin/{shared => common}/call_test.go (99%) rename go_plugin_test.go => plugin/common/go_plugin_test.go (58%) rename hashicorp_plugin_test.go => plugin/common/hashicorp_plugin_test.go (61%) rename plugin.go => plugin/common/init.go (67%) diff --git a/parser.go b/parser.go index b456f680..ce2a8fe3 100644 --- a/parser.go +++ b/parser.go @@ -11,6 +11,9 @@ import ( "github.com/maja42/goval" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/hrp/internal/builtin" + "github.com/httprunner/hrp/plugin/common" ) func newParser() *parser { @@ -18,7 +21,7 @@ func newParser() *parser { } type parser struct { - plugin hrpPlugin // plugin is used to call functions + plugin common.Plugin // plugin is used to call functions } func buildURL(baseURL, stepURL string) string { @@ -233,6 +236,25 @@ func (p *parser) parseString(raw string, variablesMapping map[string]interface{} return parsedString, nil } +// callFunc calls function with arguments +// only support return at most one result value +func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { + // call with plugin function + if p.plugin != nil && p.plugin.Has(funcName) { + return p.plugin.Call(funcName, arguments...) + } + + // get builtin function + function, ok := builtin.Functions[funcName] + if !ok { + return nil, fmt.Errorf("function %s is not found", funcName) + } + fn := reflect.ValueOf(function) + + // call with builtin function + return common.CallFunc(fn, arguments...) +} + // merge two variables mapping, the first variables have higher priority func mergeVariables(variables, overriddenVariables map[string]interface{}) map[string]interface{} { if overriddenVariables == nil { diff --git a/plugin/shared/call.go b/plugin/common/call.go similarity index 99% rename from plugin/shared/call.go rename to plugin/common/call.go index 8d30516d..02238cc9 100644 --- a/plugin/shared/call.go +++ b/plugin/common/call.go @@ -1,4 +1,4 @@ -package shared +package common import ( "fmt" diff --git a/plugin/shared/call_test.go b/plugin/common/call_test.go similarity index 99% rename from plugin/shared/call_test.go rename to plugin/common/call_test.go index d886f2e0..03b33ba6 100644 --- a/plugin/shared/call_test.go +++ b/plugin/common/call_test.go @@ -1,4 +1,4 @@ -package shared +package common import ( "errors" diff --git a/go_plugin_test.go b/plugin/common/go_plugin_test.go similarity index 58% rename from go_plugin_test.go rename to plugin/common/go_plugin_test.go index 6183c7ea..fec89180 100644 --- a/go_plugin_test.go +++ b/plugin/common/go_plugin_test.go @@ -1,7 +1,7 @@ // +build linux freebsd darwin // go plugin doesn't support windows -package hrp +package common import ( "fmt" @@ -16,7 +16,7 @@ func buildGoPlugin() { fmt.Println("[setup] build go plugin") // flag -race is necessary in order to be consistent with go test cmd := exec.Command("go", "build", "-buildmode=plugin", "-race", - "-o=examples/debugtalk.so", "examples/plugin/debugtalk.go") + "-o=debugtalk.so", "../../examples/plugin/debugtalk.go") if err := cmd.Run(); err != nil { panic(err) } @@ -24,50 +24,43 @@ func buildGoPlugin() { func removeGoPlugin() { fmt.Println("[teardown] remove go plugin") - os.Remove("examples/debugtalk.so") + os.Remove("debugtalk.so") } func TestLocatePlugin(t *testing.T) { buildGoPlugin() defer removeGoPlugin() - cwd, _ := os.Getwd() - _, err := locatePlugin(cwd, goPluginFile) + _, err := locateFile("../", goPluginFile) if !assert.Error(t, err) { t.Fail() } - _, err = locatePlugin("", goPluginFile) + _, err = locateFile("", goPluginFile) if !assert.Error(t, err) { t.Fail() } - startPath := "examples/debugtalk.so" - _, err = locatePlugin(startPath, goPluginFile) + startPath := "debugtalk.so" + _, err = locateFile(startPath, goPluginFile) if !assert.Nil(t, err) { t.Fail() } - startPath = "examples/demo.json" - _, err = locatePlugin(startPath, goPluginFile) + startPath = "call.go" + _, err = locateFile(startPath, goPluginFile) if !assert.Nil(t, err) { t.Fail() } - startPath = "examples/" - _, err = locatePlugin(startPath, goPluginFile) - if !assert.Nil(t, err) { - t.Fail() - } - - startPath = "examples/plugin/debugtalk.go" - _, err = locatePlugin(startPath, goPluginFile) + startPath = "." + _, err = locateFile(startPath, goPluginFile) if !assert.Nil(t, err) { t.Fail() } startPath = "/abc" - _, err = locatePlugin(startPath, goPluginFile) + _, err = locateFile(startPath, goPluginFile) if !assert.Error(t, err) { t.Fail() } @@ -75,17 +68,19 @@ func TestLocatePlugin(t *testing.T) { func TestCallPluginFunction(t *testing.T) { buildGoPlugin() - removeHashicorpPlugin() defer removeGoPlugin() - parser := newParser() - err := parser.initPlugin("examples/debugtalk.so") + plugin, err := Init("debugtalk.so") if err != nil { t.Fatal(err) } + if !assert.True(t, plugin.Has("Concatenate")) { + t.Fail() + } + // call function without arguments - result, err := parser.callFunc("Concatenate", "1", 2, "3.14") + result, err := plugin.Call("Concatenate", "1", 2, "3.14") if !assert.NoError(t, err) { t.Fail() } diff --git a/hashicorp_plugin_test.go b/plugin/common/hashicorp_plugin_test.go similarity index 61% rename from hashicorp_plugin_test.go rename to plugin/common/hashicorp_plugin_test.go index c9edf994..06713926 100644 --- a/hashicorp_plugin_test.go +++ b/plugin/common/hashicorp_plugin_test.go @@ -1,4 +1,4 @@ -package hrp +package common import ( "fmt" @@ -6,15 +6,14 @@ import ( "os/exec" "testing" - "github.com/httprunner/hrp/plugin/host" "github.com/stretchr/testify/assert" ) func buildHashicorpPlugin() { fmt.Println("[init] build hashicorp go plugin") cmd := exec.Command("go", "build", - "-o=examples/debugtalk.bin", - "examples/plugin/hashicorp.go", "examples/plugin/debugtalk.go") + "-o=debugtalk.bin", + "../../examples/plugin/hashicorp.go", "../../examples/plugin/debugtalk.go") if err := cmd.Run(); err != nil { panic(err) } @@ -22,46 +21,42 @@ func buildHashicorpPlugin() { func removeHashicorpPlugin() { fmt.Println("[teardown] remove hashicorp plugin") - os.Remove("examples/debugtalk.bin") + os.Remove("debugtalk.bin") } func TestInitHashicorpPlugin(t *testing.T) { buildHashicorpPlugin() defer removeHashicorpPlugin() - f, err := host.Init("examples/debugtalk.bin") + plugin, err := Init("debugtalk.bin") if err != nil { t.Fatal(err) } - defer host.Quit() + defer plugin.Quit() - v1, err := f.GetNames() - if err != nil { + if !assert.True(t, plugin.Has("sum_ints")) { t.Fatal(err) } - if !assert.Contains(t, v1, "sum_ints") { - t.Fatal(err) - } - if !assert.Contains(t, v1, "concatenate") { + if !assert.True(t, plugin.Has("concatenate")) { t.Fatal(err) } var v2 interface{} - v2, err = f.Call("sum_ints", 1, 2, 3, 4) + v2, err = plugin.Call("sum_ints", 1, 2, 3, 4) if err != nil { t.Fatal(err) } if !assert.Equal(t, 10, v2) { t.Fail() } - v2, err = f.Call("sum_two_int", 1, 2) + v2, err = plugin.Call("sum_two_int", 1, 2) if err != nil { t.Fatal(err) } if !assert.Equal(t, 3, v2) { t.Fail() } - v2, err = f.Call("sum", 1, 2, 3.4, 5) + v2, err = plugin.Call("sum", 1, 2, 3.4, 5) if err != nil { t.Fatal(err) } @@ -70,14 +65,14 @@ func TestInitHashicorpPlugin(t *testing.T) { } var v3 interface{} - v3, err = f.Call("sum_two_string", "a", "b") + v3, err = plugin.Call("sum_two_string", "a", "b") if err != nil { t.Fatal(err) } if !assert.Equal(t, "ab", v3) { t.Fail() } - v3, err = f.Call("sum_strings", "a", "b", "c") + v3, err = plugin.Call("sum_strings", "a", "b", "c") if err != nil { t.Fatal(err) } @@ -85,7 +80,7 @@ func TestInitHashicorpPlugin(t *testing.T) { t.Fail() } - v3, err = f.Call("concatenate", "a", 2, "c", 3.4) + v3, err = plugin.Call("concatenate", "a", 2, "c", 3.4) if err != nil { t.Fatal(err) } diff --git a/plugin.go b/plugin/common/init.go similarity index 67% rename from plugin.go rename to plugin/common/init.go index 389d58e3..0ff13a0d 100644 --- a/plugin.go +++ b/plugin/common/init.go @@ -1,4 +1,4 @@ -package hrp +package common import ( "fmt" @@ -10,7 +10,6 @@ import ( "github.com/rs/zerolog/log" - "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" pluginHost "github.com/httprunner/hrp/plugin/host" pluginShared "github.com/httprunner/hrp/plugin/shared" @@ -24,11 +23,11 @@ const ( hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py" ) -type hrpPlugin interface { - init(path string) error // init plugin - has(funcName string) bool // check if plugin has function - call(funcName string, args ...interface{}) (interface{}, error) // call function - quit() error // quit plugin +type Plugin interface { + Init(path string) error // init plugin + Has(funcName string) bool // check if plugin has function + Call(funcName string, args ...interface{}) (interface{}, error) // call function + Quit() error // quit plugin } // goPlugin implements golang official plugin @@ -37,7 +36,7 @@ type goPlugin struct { cachedFunctions map[string]reflect.Value // cache loaded functions to improve performance } -func (p *goPlugin) init(path string) error { +func (p *goPlugin) Init(path string) error { if runtime.GOOS == "windows" { log.Warn().Msg("go plugin does not support windows") return fmt.Errorf("go plugin does not support windows") @@ -67,7 +66,7 @@ func (p *goPlugin) init(path string) error { return nil } -func (p *goPlugin) has(funcName string) bool { +func (p *goPlugin) Has(funcName string) bool { fn, ok := p.cachedFunctions[funcName] if ok { return fn.IsValid() @@ -90,12 +89,15 @@ func (p *goPlugin) has(funcName string) bool { return true } -func (p *goPlugin) call(funcName string, args ...interface{}) (interface{}, error) { +func (p *goPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { + if !p.Has(funcName) { + return nil, fmt.Errorf("function %s not found", funcName) + } fn := p.cachedFunctions[funcName] - return pluginShared.CallFunc(fn, args...) + return CallFunc(fn, args...) } -func (p *goPlugin) quit() error { +func (p *goPlugin) Quit() error { // no need to quit for go plugin return nil } @@ -106,7 +108,7 @@ type hashicorpPlugin struct { cachedFunctions map[string]bool // cache loaded functions to improve performance } -func (p *hashicorpPlugin) init(path string) error { +func (p *hashicorpPlugin) Init(path string) error { f, err := pluginHost.Init(path) if err != nil { @@ -120,7 +122,7 @@ func (p *hashicorpPlugin) init(path string) error { return nil } -func (p *hashicorpPlugin) has(funcName string) bool { +func (p *hashicorpPlugin) Has(funcName string) bool { flag, ok := p.cachedFunctions[funcName] if ok { return flag @@ -142,45 +144,48 @@ func (p *hashicorpPlugin) has(funcName string) bool { return false } -func (p *hashicorpPlugin) call(funcName string, args ...interface{}) (interface{}, error) { +func (p *hashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { return p.FuncCaller.Call(funcName, args...) } -func (p *hashicorpPlugin) quit() error { +func (p *hashicorpPlugin) Quit() error { // kill hashicorp plugin process pluginHost.Quit() return nil } -func (p *parser) initPlugin(path string) error { +func Init(path string) (Plugin, error) { if path == "" { - return nil + return nil, nil } + var plugin Plugin // priority: hashicorp plugin > go plugin > builtin functions // locate hashicorp plugin file - pluginPath, err := locatePlugin(path, hashicorpGoPluginFile) + pluginPath, err := locateFile(path, hashicorpGoPluginFile) if err == nil { // found hashicorp go plugin file - p.plugin = &hashicorpPlugin{} - return p.plugin.init(pluginPath) + plugin = &hashicorpPlugin{} + err = plugin.Init(pluginPath) + return plugin, err } // locate go plugin file - pluginPath, err = locatePlugin(path, goPluginFile) + pluginPath, err = locateFile(path, goPluginFile) if err == nil { // found go plugin file - p.plugin = &goPlugin{} - return p.plugin.init(pluginPath) + plugin = &goPlugin{} + err = plugin.Init(pluginPath) + return plugin, err } // plugin not found - return nil + return nil, nil } -// locatePlugin searches destPluginFile upward recursively until current +// locateFile searches destFile upward recursively until current // working directory or system root dir. -func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) { +func locateFile(startPath string, destFile pluginFile) (string, error) { stat, err := os.Stat(startPath) if os.IsNotExist(err) { return "", err @@ -195,7 +200,7 @@ func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) { startDir, _ = filepath.Abs(startDir) // convention over configuration - pluginPath := filepath.Join(startDir, string(destPluginFile)) + pluginPath := filepath.Join(startDir, string(destFile)) if _, err := os.Stat(pluginPath); err == nil { return pluginPath, nil } @@ -212,24 +217,5 @@ func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) { return "", fmt.Errorf("searched to system root dir, plugin file not found") } - return locatePlugin(parentDir, destPluginFile) -} - -// callFunc calls function with arguments -// only support return at most one result value -func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { - // call with plugin function - if p.plugin != nil && p.plugin.has(funcName) { - return p.plugin.call(funcName, arguments...) - } - - // get builtin function - function, ok := builtin.Functions[funcName] - if !ok { - return nil, fmt.Errorf("function %s is not found", funcName) - } - fn := reflect.ValueOf(function) - - // call with builtin function - return pluginShared.CallFunc(fn, arguments...) + return locateFile(parentDir, destFile) } diff --git a/plugin/plugin.go b/plugin/plugin.go index c9301795..6a75dedb 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -8,6 +8,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/httprunner/hrp/plugin/common" pluginShared "github.com/httprunner/hrp/plugin/shared" ) @@ -36,7 +37,7 @@ func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{} return nil, fmt.Errorf("function %s not found", funcName) } - return pluginShared.CallFunc(fn, args...) + return common.CallFunc(fn, args...) } var functions = make(functionsMap) diff --git a/runner.go b/runner.go index 66844d23..b0e9b6d4 100644 --- a/runner.go +++ b/runner.go @@ -19,6 +19,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/hrp/internal/ga" + "github.com/httprunner/hrp/plugin/common" ) // Run starts to run API test with default configs. @@ -157,7 +158,7 @@ func (r *caseRunner) reset() *caseRunner { func (r *caseRunner) run() error { defer func() { if r.parser.plugin != nil { - r.parser.plugin.quit() + r.parser.plugin.Quit() } }() config := r.TestCase.Config @@ -504,7 +505,8 @@ func (r *caseRunner) parseConfig(config IConfig) error { cfg := config.ToStruct() // init plugin - err := r.parser.initPlugin(cfg.Path) + var err error + r.parser.plugin, err = common.Init(cfg.Path) if err != nil { return err } From fd7992add56e7997db996e496ceb57f7b942a1ea Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Jan 2022 17:20:47 +0800 Subject: [PATCH 02/11] change: report event for initializing plugin --- plugin/common/init.go | 41 ++++++++++++++--------------------------- runner.go | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/plugin/common/init.go b/plugin/common/init.go index 0ff13a0d..931668f8 100644 --- a/plugin/common/init.go +++ b/plugin/common/init.go @@ -10,7 +10,6 @@ import ( "github.com/rs/zerolog/log" - "github.com/httprunner/hrp/internal/ga" pluginHost "github.com/httprunner/hrp/plugin/host" pluginShared "github.com/httprunner/hrp/plugin/shared" ) @@ -30,31 +29,19 @@ type Plugin interface { Quit() error // quit plugin } -// goPlugin implements golang official plugin -type goPlugin struct { +// GoPlugin implements golang official plugin +type GoPlugin struct { *plugin.Plugin cachedFunctions map[string]reflect.Value // cache loaded functions to improve performance } -func (p *goPlugin) Init(path string) error { +func (p *GoPlugin) Init(path string) error { if runtime.GOOS == "windows" { log.Warn().Msg("go plugin does not support windows") return fmt.Errorf("go plugin does not support windows") } var err error - // report event for loading go plugin - defer func() { - event := ga.EventTracking{ - Category: "LoadGoPlugin", - Action: "plugin.Open", - } - if err != nil { - event.Value = 1 // failed - } - go ga.SendEvent(event) - }() - p.Plugin, err = plugin.Open(path) if err != nil { log.Error().Err(err).Str("path", path).Msg("load go plugin failed") @@ -66,7 +53,7 @@ func (p *goPlugin) Init(path string) error { return nil } -func (p *goPlugin) Has(funcName string) bool { +func (p *GoPlugin) Has(funcName string) bool { fn, ok := p.cachedFunctions[funcName] if ok { return fn.IsValid() @@ -89,7 +76,7 @@ func (p *goPlugin) Has(funcName string) bool { return true } -func (p *goPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { +func (p *GoPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { if !p.Has(funcName) { return nil, fmt.Errorf("function %s not found", funcName) } @@ -97,18 +84,18 @@ func (p *goPlugin) Call(funcName string, args ...interface{}) (interface{}, erro return CallFunc(fn, args...) } -func (p *goPlugin) Quit() error { +func (p *GoPlugin) Quit() error { // no need to quit for go plugin return nil } -// hashicorpPlugin implements hashicorp/go-plugin -type hashicorpPlugin struct { +// HashicorpPlugin implements hashicorp/go-plugin +type HashicorpPlugin struct { pluginShared.FuncCaller cachedFunctions map[string]bool // cache loaded functions to improve performance } -func (p *hashicorpPlugin) Init(path string) error { +func (p *HashicorpPlugin) Init(path string) error { f, err := pluginHost.Init(path) if err != nil { @@ -122,7 +109,7 @@ func (p *hashicorpPlugin) Init(path string) error { return nil } -func (p *hashicorpPlugin) Has(funcName string) bool { +func (p *HashicorpPlugin) Has(funcName string) bool { flag, ok := p.cachedFunctions[funcName] if ok { return flag @@ -144,11 +131,11 @@ func (p *hashicorpPlugin) Has(funcName string) bool { return false } -func (p *hashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { +func (p *HashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { return p.FuncCaller.Call(funcName, args...) } -func (p *hashicorpPlugin) Quit() error { +func (p *HashicorpPlugin) Quit() error { // kill hashicorp plugin process pluginHost.Quit() return nil @@ -165,7 +152,7 @@ func Init(path string) (Plugin, error) { pluginPath, err := locateFile(path, hashicorpGoPluginFile) if err == nil { // found hashicorp go plugin file - plugin = &hashicorpPlugin{} + plugin = &HashicorpPlugin{} err = plugin.Init(pluginPath) return plugin, err } @@ -174,7 +161,7 @@ func Init(path string) (Plugin, error) { pluginPath, err = locateFile(path, goPluginFile) if err == nil { // found go plugin file - plugin = &goPlugin{} + plugin = &GoPlugin{} err = plugin.Init(pluginPath) return plugin, err } diff --git a/runner.go b/runner.go index b0e9b6d4..fb2bedb4 100644 --- a/runner.go +++ b/runner.go @@ -506,6 +506,27 @@ func (r *caseRunner) parseConfig(config IConfig) error { // init plugin var err error + defer func() { + if r.parser.plugin == nil { + return + } + var pluginType string + if _, ok := r.parser.plugin.(*common.GoPlugin); ok { + pluginType = "go" + } else { + pluginType = "hashicorp" + } + + // report event for initializing plugin + event := ga.EventTracking{ + Category: "InitPlugin", + Action: fmt.Sprintf("Init %s plugin", pluginType), + } + if err != nil { + event.Value = 1 // failed + } + go ga.SendEvent(event) + }() r.parser.plugin, err = common.Init(cfg.Path) if err != nil { return err From ede35216e2ac1eb891557933bb510cb9d116f96d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Jan 2022 17:32:32 +0800 Subject: [PATCH 03/11] feat: add github.com/httprunner/hrp/plugin sub-module --- go.mod | 5 ++- plugin/go.mod | 10 +++++ plugin/go.sum | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 plugin/go.mod create mode 100644 plugin/go.sum diff --git a/go.mod b/go.mod index e0e88167..758c54bf 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/getsentry/sentry-go v0.11.0 github.com/google/uuid v1.3.0 - github.com/hashicorp/go-hclog v1.1.0 - github.com/hashicorp/go-plugin v1.4.3 + github.com/httprunner/hrp/plugin v0.0.0-00010101000000-000000000000 github.com/jinzhu/copier v0.3.2 github.com/jmespath/go-jmespath v0.4.0 github.com/maja42/goval v1.2.1 @@ -21,3 +20,5 @@ require ( golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) + +replace github.com/httprunner/hrp/plugin => ./plugin diff --git a/plugin/go.mod b/plugin/go.mod new file mode 100644 index 00000000..f5af4505 --- /dev/null +++ b/plugin/go.mod @@ -0,0 +1,10 @@ +module github.com/httprunner/hrp/plugin + +go 1.16 + +require ( + github.com/hashicorp/go-hclog v1.1.0 + github.com/hashicorp/go-plugin v1.4.3 + github.com/rs/zerolog v1.26.1 + github.com/stretchr/testify v1.7.0 +) diff --git a/plugin/go.sum b/plugin/go.sum new file mode 100644 index 00000000..0e146002 --- /dev/null +++ b/plugin/go.sum @@ -0,0 +1,118 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw= +github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From b84f33eee14d37ab4c83c2804ed701a7ecaf5a28 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Jan 2022 19:52:46 +0800 Subject: [PATCH 04/11] docs: add plugin docs --- README.md | 5 +- plugin/README.md | 190 +++++++++++++++++++++++++++++++++++++++++++++++ plugin/plugin.go | 3 + 3 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 plugin/README.md diff --git a/README.md b/README.md index 6ddd349c..4dcc78d1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - [x] Testcases can be described in multiple formats, `YAML`/`JSON`/`Golang`, and they are interchangeable. - [x] With [`HAR`][HAR] support, you can use Charles/Fiddler/Chrome/etc as a script recording generator. - [x] Supports `variables`/`extract`/`validate`/`hooks` mechanisms to create extremely complex test scenarios. -- [ ] Built-in integration of rich functions, and you can also use [`go plugin`][plugin] to create and call custom functions. +- [x] Built-in integration of rich functions, and you can also use [hashicorp/plugin] or [go plugin] to create and call custom functions. - [x] Inherit all powerful features of [`Boomer`][Boomer] and [`locust`][locust], you can run `load test` without extra work. - [x] Using it as a `CLI tool` or a `library` are both supported. @@ -272,7 +272,8 @@ func TestCaseDemo(t *testing.T) { [jmespath]: https://jmespath.org/ [allure]: https://docs.qameta.io/allure/ [HAR]: http://httparchive.org/ -[plugin]: https://pkg.go.dev/plugin +[hashicorp/plugin]: https://github.com/hashicorp/go-plugin +[go plugin]: https://pkg.go.dev/plugin [demo.json]: https://github.com/httprunner/hrp/blob/main/examples/demo.json [examples]: https://github.com/httprunner/hrp/blob/main/examples/ [CHANGELOG]: docs/CHANGELOG.md \ No newline at end of file diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 00000000..b83e2c10 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,190 @@ +# plugin + +When you need to do some dynamic calculations or custom logic processing in testcases, you need to use the plugin function mechanism. + +HttpRunner+ supports both [hashicorp/plugin] and [go plugin] to create and call custom functions. + +## hashicorp/plugin + +It is recommended to use [hashicorp/plugin] in most cases. + +### create plugin functions + +Firstly, you need to define your plugin functions. The functions can be very flexible, only the following restrictions should be complied with. + +- function should return at most one value and one error. +- `Register()` and `Serve()` must be called to register plugin functions and start a plugin server process in `main()`. + +Here is some plugin functions as example. + +```go +package main + +import ( + "fmt" + + "github.com/httprunner/hrp/plugin" +) + +func SumTwoInt(a, b int) int { + return a + b +} + +func SumInts(args ...int) int { + var sum int + for _, arg := range args { + sum += arg + } + return sum +} + +func Sum(args ...interface{}) (interface{}, error) { + var sum float64 + for _, arg := range args { + switch v := arg.(type) { + case int: + sum += float64(v) + case float64: + sum += v + default: + return nil, fmt.Errorf("unexpected type: %T", arg) + } + } + return sum, nil +} + +func main() { + plugin.Register("sum_ints", SumInts) + plugin.Register("sum_two_int", SumTwoInt) + plugin.Register("sum", Sum) + plugin.Serve() +} +``` + +You can get more examples at [examples/plugin/] + +### build plugin + +Secondly, you can build your hashicorp plugin to binary file `debugtalk.bin`. The name of `debugtalk.bin` is by convention and should not be changed. + +```bash +$ go build -o examples/debugtalk.bin examples/plugin/hashicorp.go examples/plugin/debugtalk.go +``` + +It is recommended to place the `debugtalk.bin` file in your project root folder, or you can put it in the parent folder of the target testcase file. HttpRunner+ will search `debugtalk.bin` upward recursively until current working directory or system root dir. + +### use plugin functions + +Then, you can call your defined plugin function in your `YAML/JSON` testcase at any position. + +```json +{ + "name": "get with params", + "variables": { + "a": "${sum_two_int(1,6)}", + "b": "${sum_ints(1,2,3)}", + "c": "${sum(1, 2.3, 4)}", + }, + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$c", + "foo2": "${max($a, $b)}" + }, + "headers": { + "User-Agent": "HttpRunnerPlus" + } + } +} +``` + +## go plugin + +The golang official plugin is only supported on Linux, FreeBSD, and macOS. And this solution also has many drawbacks. + +### create plugin functions + +Firstly, you need to define your plugin functions. The functions can be very flexible, only the following restrictions should be complied with. + +- plugin package name must be `main`. +- function names must be capitalized. +- function should return at most one value and one error. + +Here is some plugin functions as example. + +```go +package main + +func SumTwoInt(a, b int) int { + return a + b +} + +func SumInts(args ...int) int { + var sum int + for _, arg := range args { + sum += arg + } + return sum +} + +func Sum(args ...interface{}) (interface{}, error) { + var sum float64 + for _, arg := range args { + switch v := arg.(type) { + case int: + sum += float64(v) + case float64: + sum += v + default: + return nil, fmt.Errorf("unexpected type: %T", arg) + } + } + return sum, nil +} +``` + +You can get more examples at [examples/plugin/debugtalk.go] + +### build plugin + +Then you can build your go plugin with `-buildmode=plugin` flag to binary file `debugtalk.so`. The name of `debugtalk.so` is by convention and should not be changed. + +```bash +$ go build -buildmode=plugin -o=examples/debugtalk.so examples/plugin/debugtalk.go +``` + +It is recommended to place the `debugtalk.so` file in your project root folder, or you can put it in the parent folder of the target testcase file. HttpRunner+ will search `debugtalk.so` upward recursively until current working directory or system root dir. + +### use plugin functions + +Then, you can call your defined plugin function in your `YAML/JSON` testcase at any position. + +```json +{ + "name": "get with params", + "variables": { + "a": "${SumTwoInt(1,6)}", + "b": "${SumInts(1,2,3)}", + "c": "${Sum(1, 2.3, 4)}", + }, + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$c", + "foo2": "${max($a, $b)}" + }, + "headers": { + "User-Agent": "HttpRunnerPlus" + } + } +} +``` + +Notice: you should use the original function name. + +[hashicorp/plugin]: https://github.com/hashicorp/go-plugin +[go plugin]: https://pkg.go.dev/plugin +[examples/plugin/]: ../examples/plugin/ +[examples/plugin/debugtalk.go]: ../examples/plugin/debugtalk.go diff --git a/plugin/plugin.go b/plugin/plugin.go index 6a75dedb..4dc25731 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -42,6 +42,8 @@ func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{} var functions = make(functionsMap) +// Register registers a plugin function. +// Every plugin function must be registered before Serve() is called. func Register(funcName string, fn interface{}) { if _, ok := functions[funcName]; ok { return @@ -49,6 +51,7 @@ func Register(funcName string, fn interface{}) { functions[funcName] = reflect.ValueOf(fn) } +// Serve starts a plugin server process. func Serve() { funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ From d5dff2dcdf86d03a4611a97ec586346a9e548677 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Jan 2022 20:24:42 +0800 Subject: [PATCH 05/11] feat: add scaffold for plugin --- examples/demo.json | 4 +-- examples/demo.yaml | 4 +-- internal/scaffold/demo.go | 54 ++++++++++++++++++++++++++++++++-- internal/scaffold/demo_test.go | 1 + internal/scaffold/main.go | 16 ++++++++++ 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/examples/demo.json b/examples/demo.json index 0f6c9819..198bcbaa 100644 --- a/examples/demo.json +++ b/examples/demo.json @@ -3,9 +3,9 @@ "name": "demo with complex mechanisms", "base_url": "https://postman-echo.com", "variables": { - "a": 12.3, + "a": "${sum(10, 2.3)}", "b": 3.45, - "n": 5, + "n": "${sum_ints(1, 2, 2)}", "varFoo1": "${gen_random_string($n)}", "varFoo2": "${max($a, $b)}" } diff --git a/examples/demo.yaml b/examples/demo.yaml index a0cee432..ed6131dc 100644 --- a/examples/demo.yaml +++ b/examples/demo.yaml @@ -2,9 +2,9 @@ config: name: demo with complex mechanisms base_url: https://postman-echo.com variables: - a: 12.3 + a: ${sum(10, 2.3)} b: 3.45 - "n": 5 + "n": ${sum_ints(1, 2, 2)} varFoo1: ${gen_random_string($n)} varFoo2: ${max($a, $b)} teststeps: diff --git a/internal/scaffold/demo.go b/internal/scaffold/demo.go index 83753467..7ea63b5a 100644 --- a/internal/scaffold/demo.go +++ b/internal/scaffold/demo.go @@ -6,8 +6,8 @@ var demoTestCase = &hrp.TestCase{ Config: hrp.NewConfig("demo with complex mechanisms"). SetBaseURL("https://postman-echo.com"). WithVariables(map[string]interface{}{ // global level variables - "n": 5, - "a": 12.3, + "n": "${sum_ints(1, 2, 2)}", + "a": "${sum(10, 2.3)}", "b": 3.45, "varFoo1": "${gen_random_string($n)}", "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function @@ -56,6 +56,50 @@ var demoTestCase = &hrp.TestCase{ }, } +// debugtalk.go +var demoPlugin = `package main + +import ( + "fmt" + + "github.com/httprunner/hrp/plugin" +) + +func SumTwoInt(a, b int) int { + return a + b +} + +func SumInts(args ...int) int { + var sum int + for _, arg := range args { + sum += arg + } + return sum +} + +func Sum(args ...interface{}) (interface{}, error) { + var sum float64 + for _, arg := range args { + switch v := arg.(type) { + case int: + sum += float64(v) + case float64: + sum += v + default: + return nil, fmt.Errorf("unexpected type: %T", arg) + } + } + return sum, nil +} + +func main() { + plugin.Register("sum_ints", SumInts) + plugin.Register("sum_two_int", SumTwoInt) + plugin.Register("sum", Sum) + plugin.Serve() +} +` + // .gitignore var demoIgnoreContent = `.env reports/* @@ -64,9 +108,13 @@ reports/* .idea/ .DS_Store output/ + +# plugin +debugtalk.bin +debugtalk.so ` // .env var demoEnvContent = `USERNAME=debugtalk -"PASSWORD=123456 +PASSWORD=123456 ` diff --git a/internal/scaffold/demo_test.go b/internal/scaffold/demo_test.go index 870ef7bd..a78d3dcf 100644 --- a/internal/scaffold/demo_test.go +++ b/internal/scaffold/demo_test.go @@ -25,6 +25,7 @@ func TestGenDemoTestCase(t *testing.T) { } func Example_demo() { + demoTestCase.Config.ToStruct().Path = "../../examples/debugtalk.bin" err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase) fmt.Println(err) // Output: diff --git a/internal/scaffold/main.go b/internal/scaffold/main.go index b9ca26d2..419626a1 100644 --- a/internal/scaffold/main.go +++ b/internal/scaffold/main.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "path" "github.com/httprunner/hrp/internal/ga" @@ -36,6 +37,9 @@ func CreateScaffold(projectName string) error { if err := createFolder(path.Join(projectName, "testcases")); err != nil { return err } + if err := createFolder(path.Join(projectName, "plugin")); err != nil { + return err + } if err := createFolder(path.Join(projectName, "reports")); err != nil { return err } @@ -53,6 +57,18 @@ func CreateScaffold(projectName string) error { return err } + // create debugtalk.go + pluginFile := path.Join(projectName, "plugin", "debugtalk.go") + if err := createFile(pluginFile, demoPlugin); err != nil { + return err + } + cmd := exec.Command("go", "build", + "-o", path.Join(projectName, "debugtalk.bin"), + pluginFile) + if err := cmd.Run(); err != nil { + log.Error().Err(err).Msg("build plugin debugtalk.bin failed") + } + // create .gitignore if err := createFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil { return err From 9a1d02c7b2716e56321273cc91b559d7947bed17 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Jan 2022 17:18:16 +0800 Subject: [PATCH 06/11] fix: unittests for plugin --- boomer.go | 27 ++++++++++- boomer_test.go | 3 ++ docs/cmd/hrp.md | 2 +- docs/cmd/hrp_boom.md | 2 +- docs/cmd/hrp_har2case.md | 2 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_startproject.md | 2 +- internal/scaffold/demo_test.go | 28 +++++++++++ plugin/common/hashicorp_plugin_test.go | 12 ++--- runner.go | 64 ++++++++++++++------------ runner_test.go | 22 +++++++++ 11 files changed, 125 insertions(+), 41 deletions(-) diff --git a/boomer.go b/boomer.go index 3236a007..0989ce40 100644 --- a/boomer.go +++ b/boomer.go @@ -8,6 +8,7 @@ import ( "github.com/httprunner/hrp/internal/boomer" "github.com/httprunner/hrp/internal/ga" + "github.com/httprunner/hrp/plugin/common" ) func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer { @@ -20,7 +21,8 @@ func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer { type HRPBoomer struct { *boomer.Boomer - debug bool + plugins []common.Plugin // each task has its own plugin process + debug bool } // SetDebug configures whether to log HTTP request and response content. @@ -57,14 +59,29 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) { b.Boomer.Run(taskSlice...) } +func (b *HRPBoomer) Quit() { + for _, plugin := range b.plugins { + plugin.Quit() + } + b.Boomer.Quit() +} + func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { hrpRunner := NewRunner(nil).SetDebug(b.debug) config := testcase.Config.ToStruct() + + // each testcase has its own plugin process + plugin, _ := initPlugin(config.Path) + if plugin != nil { + b.plugins = append(b.plugins, plugin) + } + return &boomer.Task{ Name: config.Name, Weight: config.Weight, Fn: func() { runner := hrpRunner.newCaseRunner(testcase) + runner.parser.plugin = plugin testcaseSuccess := true // flag whole testcase result var transactionSuccess = true // flag current transaction result @@ -74,6 +91,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { // copy config to avoid data racing if err := copier.Copy(caseConfig, cfg); err != nil { log.Error().Err(err).Msg("copy config data failed") + return } // iterate through all parameter iterators and update case variables for _, it := range caseConfig.ParametersSetting.Iterators { @@ -81,6 +99,13 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { caseConfig.Variables = mergeVariables(it.Next(), caseConfig.Variables) } } + + config := runner.TestCase.Config + if err := runner.parseConfig(config); err != nil { + log.Error().Err(err).Msg("parse config failed") + return + } + startTime := time.Now() for index, step := range testcase.TestSteps { stepData, err := runner.runStep(index, caseConfig) diff --git a/boomer_test.go b/boomer_test.go index b49e1069..ab61e62e 100644 --- a/boomer_test.go +++ b/boomer_test.go @@ -6,6 +6,9 @@ import ( ) func TestBoomerStandaloneRun(t *testing.T) { + buildHashicorpPlugin() + defer removeHashicorpPlugin() + testcase1 := &TestCase{ Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"), TestSteps: []IStep{ diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 14de4bca..0a64f28f 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -33,4 +33,4 @@ Copyright 2021 debugtalk * [hrp run](hrp_run.md) - run API test * [hrp startproject](hrp_startproject.md) - create a scaffold project -###### Auto generated by spf13/cobra on 17-Jan-2022 +###### Auto generated by spf13/cobra on 18-Jan-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 9b274446..162ae0e3 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -39,4 +39,4 @@ hrp boom [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 17-Jan-2022 +###### Auto generated by spf13/cobra on 18-Jan-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index 2fe2e4ae..2d575c70 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -23,4 +23,4 @@ hrp har2case $har_path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 17-Jan-2022 +###### Auto generated by spf13/cobra on 18-Jan-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 2abd2c76..5c1f1c40 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -31,4 +31,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 17-Jan-2022 +###### Auto generated by spf13/cobra on 18-Jan-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index 40b7807b..96485c09 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -16,4 +16,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 17-Jan-2022 +###### Auto generated by spf13/cobra on 18-Jan-2022 diff --git a/internal/scaffold/demo_test.go b/internal/scaffold/demo_test.go index a78d3dcf..9f301c4a 100644 --- a/internal/scaffold/demo_test.go +++ b/internal/scaffold/demo_test.go @@ -2,8 +2,12 @@ package scaffold import ( "fmt" + "os" + "os/exec" "testing" + "github.com/rs/zerolog/log" + "github.com/httprunner/hrp" ) @@ -12,6 +16,21 @@ var ( demoTestCaseYAMLPath = "../../examples/demo.yaml" ) +func buildHashicorpPlugin() { + log.Info().Msg("[init] build hashicorp go plugin") + cmd := exec.Command("go", "build", + "-o", "../../examples/debugtalk.bin", + "../../examples/plugin/hashicorp.go", "../../examples/plugin/debugtalk.go") + if err := cmd.Run(); err != nil { + panic(err) + } +} + +func removeHashicorpPlugin() { + log.Info().Msg("[teardown] remove hashicorp plugin") + os.Remove("../../examples/debugtalk.bin") +} + func TestGenDemoTestCase(t *testing.T) { tCase, _ := demoTestCase.ToTCase() err := tCase.Dump2JSON(demoTestCaseJSONPath) @@ -25,6 +44,9 @@ func TestGenDemoTestCase(t *testing.T) { } func Example_demo() { + buildHashicorpPlugin() + defer removeHashicorpPlugin() + demoTestCase.Config.ToStruct().Path = "../../examples/debugtalk.bin" err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase) fmt.Println(err) @@ -33,6 +55,9 @@ func Example_demo() { } func Example_jsonDemo() { + buildHashicorpPlugin() + defer removeHashicorpPlugin() + testCase := &hrp.TestCasePath{Path: demoTestCaseJSONPath} err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase) fmt.Println(err) @@ -41,6 +66,9 @@ func Example_jsonDemo() { } func Example_yamlDemo() { + buildHashicorpPlugin() + defer removeHashicorpPlugin() + testCase := &hrp.TestCasePath{Path: demoTestCaseYAMLPath} err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase) fmt.Println(err) diff --git a/plugin/common/hashicorp_plugin_test.go b/plugin/common/hashicorp_plugin_test.go index 06713926..a0c6a806 100644 --- a/plugin/common/hashicorp_plugin_test.go +++ b/plugin/common/hashicorp_plugin_test.go @@ -1,18 +1,18 @@ package common import ( - "fmt" "os" "os/exec" "testing" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" ) func buildHashicorpPlugin() { - fmt.Println("[init] build hashicorp go plugin") + log.Info().Msg("[init] build hashicorp go plugin") cmd := exec.Command("go", "build", - "-o=debugtalk.bin", + "-o", "../../examples/debugtalk.bin", "../../examples/plugin/hashicorp.go", "../../examples/plugin/debugtalk.go") if err := cmd.Run(); err != nil { panic(err) @@ -20,15 +20,15 @@ func buildHashicorpPlugin() { } func removeHashicorpPlugin() { - fmt.Println("[teardown] remove hashicorp plugin") - os.Remove("debugtalk.bin") + log.Info().Msg("[teardown] remove hashicorp plugin") + os.Remove("../../examples/debugtalk.bin") } func TestInitHashicorpPlugin(t *testing.T) { buildHashicorpPlugin() defer removeHashicorpPlugin() - plugin, err := Init("debugtalk.bin") + plugin, err := Init("../../examples/debugtalk.bin") if err != nil { t.Fatal(err) } diff --git a/runner.go b/runner.go index fb2bedb4..f8f20899 100644 --- a/runner.go +++ b/runner.go @@ -156,12 +156,19 @@ func (r *caseRunner) reset() *caseRunner { } func (r *caseRunner) run() error { + config := r.TestCase.Config + + // init plugin + var err error + if r.parser.plugin, err = initPlugin(config.ToStruct().Path); err != nil { + return err + } defer func() { if r.parser.plugin != nil { r.parser.plugin.Quit() } }() - config := r.TestCase.Config + if err := r.parseConfig(config); err != nil { return err } @@ -182,6 +189,33 @@ func (r *caseRunner) run() error { return nil } +func initPlugin(path string) (plugin common.Plugin, err error) { + defer func() { + if plugin == nil { + return + } + + var pluginType string + if _, ok := plugin.(*common.GoPlugin); ok { + pluginType = "go" + } else { + pluginType = "hashicorp" + } + + // report event for initializing plugin + event := ga.EventTracking{ + Category: "InitPlugin", + Action: fmt.Sprintf("Init %s plugin", pluginType), + } + if err != nil { + event.Value = 1 // failed + } + go ga.SendEvent(event) + }() + plugin, err = common.Init(path) + return +} + func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) { step := r.TestCase.TestSteps[index] @@ -504,34 +538,6 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err func (r *caseRunner) parseConfig(config IConfig) error { cfg := config.ToStruct() - // init plugin - var err error - defer func() { - if r.parser.plugin == nil { - return - } - var pluginType string - if _, ok := r.parser.plugin.(*common.GoPlugin); ok { - pluginType = "go" - } else { - pluginType = "hashicorp" - } - - // report event for initializing plugin - event := ga.EventTracking{ - Category: "InitPlugin", - Action: fmt.Sprintf("Init %s plugin", pluginType), - } - if err != nil { - event.Value = 1 // failed - } - go ga.SendEvent(event) - }() - r.parser.plugin, err = common.Init(cfg.Path) - if err != nil { - return err - } - // parse config variables parsedVariables, err := r.parser.parseVariables(cfg.Variables) if err != nil { diff --git a/runner_test.go b/runner_test.go index 686d67e1..2fb64fcf 100644 --- a/runner_test.go +++ b/runner_test.go @@ -1,10 +1,32 @@ package hrp import ( + "os" + "os/exec" "testing" + + "github.com/rs/zerolog/log" ) +func buildHashicorpPlugin() { + log.Info().Msg("[init] build hashicorp go plugin") + cmd := exec.Command("go", "build", + "-o", "examples/debugtalk.bin", + "examples/plugin/hashicorp.go", "examples/plugin/debugtalk.go") + if err := cmd.Run(); err != nil { + panic(err) + } +} + +func removeHashicorpPlugin() { + log.Info().Msg("[teardown] remove hashicorp plugin") + os.Remove("examples/debugtalk.bin") +} + func TestHttpRunner(t *testing.T) { + buildHashicorpPlugin() + defer removeHashicorpPlugin() + testcase1 := &TestCase{ Config: NewConfig("TestCase1"). SetBaseURL("http://httpbin.org"), From 142bc13c948491fa17641605034ee1f9e0b73258 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Jan 2022 17:33:26 +0800 Subject: [PATCH 07/11] feat: catch Interrupt and SIGTERM signals to ensure plugin quitted --- plugin/common/init.go | 1 + runner.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/plugin/common/init.go b/plugin/common/init.go index 931668f8..399d38da 100644 --- a/plugin/common/init.go +++ b/plugin/common/init.go @@ -137,6 +137,7 @@ func (p *HashicorpPlugin) Call(funcName string, args ...interface{}) (interface{ func (p *HashicorpPlugin) Quit() error { // kill hashicorp plugin process + log.Warn().Msg("quit hashicorp plugin process") pluginHost.Quit() return nil } diff --git a/runner.go b/runner.go index f8f20899..a28f28a2 100644 --- a/runner.go +++ b/runner.go @@ -9,8 +9,11 @@ import ( "net/http" "net/http/httputil" "net/url" + "os" + "os/signal" "strconv" "strings" + "syscall" "testing" "time" @@ -213,6 +216,18 @@ func initPlugin(path string) (plugin common.Plugin, err error) { go ga.SendEvent(event) }() plugin, err = common.Init(path) + + // catch Interrupt and SIGTERM signals to ensure plugin quitted + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + if plugin != nil { + plugin.Quit() + } + os.Exit(1) + }() + return } From 7ef04674dce0175e3caaf97791aba7bacf21138b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Jan 2022 20:21:43 +0800 Subject: [PATCH 08/11] change: clean code --- plugin/common/init.go | 2 +- runner.go | 45 +++++++++++++++++++------------------------ 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/plugin/common/init.go b/plugin/common/init.go index 399d38da..aa27856e 100644 --- a/plugin/common/init.go +++ b/plugin/common/init.go @@ -148,7 +148,7 @@ func Init(path string) (Plugin, error) { } var plugin Plugin - // priority: hashicorp plugin > go plugin > builtin functions + // priority: hashicorp plugin > go plugin // locate hashicorp plugin file pluginPath, err := locateFile(path, hashicorpGoPluginFile) if err == nil { diff --git a/runner.go b/runner.go index a28f28a2..8e687b48 100644 --- a/runner.go +++ b/runner.go @@ -193,41 +193,36 @@ func (r *caseRunner) run() error { } func initPlugin(path string) (plugin common.Plugin, err error) { - defer func() { - if plugin == nil { - return - } - - var pluginType string - if _, ok := plugin.(*common.GoPlugin); ok { - pluginType = "go" - } else { - pluginType = "hashicorp" - } - - // report event for initializing plugin - event := ga.EventTracking{ - Category: "InitPlugin", - Action: fmt.Sprintf("Init %s plugin", pluginType), - } - if err != nil { - event.Value = 1 // failed - } - go ga.SendEvent(event) - }() plugin, err = common.Init(path) + if plugin == nil { + return + } // catch Interrupt and SIGTERM signals to ensure plugin quitted c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - if plugin != nil { - plugin.Quit() - } + plugin.Quit() os.Exit(1) }() + // report event for initializing plugin + var pluginType string + if _, ok := plugin.(*common.GoPlugin); ok { + pluginType = "go" + } else { + pluginType = "hashicorp" + } + event := ga.EventTracking{ + Category: "InitPlugin", + Action: fmt.Sprintf("Init %s plugin", pluginType), + } + if err != nil { + event.Value = 1 // failed + } + go ga.SendEvent(event) + return } From c10bf8574d5bfe1852bb172aa27839ecfecb5d88 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Jan 2022 20:30:24 +0800 Subject: [PATCH 09/11] fix: data race --- boomer.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/boomer.go b/boomer.go index 0989ce40..056f130a 100644 --- a/boomer.go +++ b/boomer.go @@ -1,6 +1,7 @@ package hrp import ( + "sync" "time" "github.com/jinzhu/copier" @@ -13,16 +14,18 @@ import ( func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer { b := &HRPBoomer{ - Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate), - debug: false, + Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate), + pluginsMutex: new(sync.RWMutex), + debug: false, } return b } type HRPBoomer struct { *boomer.Boomer - plugins []common.Plugin // each task has its own plugin process - debug bool + plugins []common.Plugin // each task has its own plugin process + pluginsMutex *sync.RWMutex // avoid data race + debug bool } // SetDebug configures whether to log HTTP request and response content. @@ -60,7 +63,10 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) { } func (b *HRPBoomer) Quit() { - for _, plugin := range b.plugins { + b.pluginsMutex.Lock() + plugins := b.plugins + b.pluginsMutex.Unlock() + for _, plugin := range plugins { plugin.Quit() } b.Boomer.Quit() @@ -73,7 +79,9 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { // each testcase has its own plugin process plugin, _ := initPlugin(config.Path) if plugin != nil { + b.pluginsMutex.Lock() b.plugins = append(b.plugins, plugin) + b.pluginsMutex.Unlock() } return &boomer.Task{ From 53ee5200c8547cdf725abd3e1d6a5bab1a37701b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 19 Jan 2022 14:52:30 +0800 Subject: [PATCH 10/11] change: exit 0 when quit hashicorp process --- runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runner.go b/runner.go index 8e687b48..ab9e8f6d 100644 --- a/runner.go +++ b/runner.go @@ -204,7 +204,7 @@ func initPlugin(path string) (plugin common.Plugin, err error) { go func() { <-c plugin.Quit() - os.Exit(1) + os.Exit(0) }() // report event for initializing plugin From 6fa8fd52d2e9d4c9337f69b1025a6483ef35dde5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 19 Jan 2022 15:03:04 +0800 Subject: [PATCH 11/11] change: update docs --- docs/CHANGELOG.md | 5 ++++- internal/boomer/stats.go | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ebe15942..39453cc7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,11 @@ # Release History -## v0.5.2 (2022-01-16) +## v0.5.2 (2022-01-19) - feat: support creating and calling custom functions with [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin) +- feat: add scaffold demo with hashicorp plugin +- feat: report events for initializing plugin +- fix: log failures when the assertion failed ## v0.5.1 (2022-01-13) diff --git a/internal/boomer/stats.go b/internal/boomer/stats.go index 57af9654..fbf7b287 100644 --- a/internal/boomer/stats.go +++ b/internal/boomer/stats.go @@ -66,7 +66,7 @@ func (s *requestStats) logTransaction(name string, success bool, responseTime in s.transactionPassed++ } else { s.transactionFailed++ - s.get(name, "transaction").logError("") + s.get(name, "transaction").logFailures() } s.get(name, "transaction").log(responseTime, contentLength) } @@ -77,8 +77,8 @@ func (s *requestStats) logRequest(method, name string, responseTime int64, conte } func (s *requestStats) logError(method, name, err string) { - s.total.logError(err) - s.get(name, method).logError(err) + s.total.logFailures() + s.get(name, method).logFailures() // store error in errors map key := genMD5(method, name, err) @@ -264,7 +264,7 @@ func (s *statsEntry) logResponseTime(responseTime int64) { } } -func (s *statsEntry) logError(err string) { +func (s *statsEntry) logFailures() { s.NumFailures++ key := time.Now().Unix() _, ok := s.NumFailPerSec[key]