diff --git a/boomer.go b/boomer.go index f67d7248..19ab4f27 100644 --- a/boomer.go +++ b/boomer.go @@ -9,29 +9,21 @@ import ( "github.com/httprunner/hrp/internal/boomer" "github.com/httprunner/hrp/internal/ga" - "github.com/httprunner/hrp/plugin/common" + pluginInternal "github.com/httprunner/hrp/plugin/inner" ) func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer { b := &HRPBoomer{ 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 - pluginsMutex *sync.RWMutex // avoid data race - debug bool -} - -// SetDebug configures whether to log HTTP request and response content. -func (b *HRPBoomer) SetDebug(debug bool) *HRPBoomer { - b.debug = debug - return b + plugins []pluginInternal.IPlugin // each task has its own plugin process + pluginsMutex *sync.RWMutex // avoid data race } // Run starts to run load test for one or multiple testcases. @@ -75,11 +67,11 @@ func (b *HRPBoomer) Quit() { } func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task { - hrpRunner := NewRunner(nil).SetDebug(b.debug) + hrpRunner := NewRunner(nil) config := testcase.Config // each testcase has its own plugin process - plugin, _ := initPlugin(config.Path) + plugin, _ := initPlugin(config.Path, false) if plugin != nil { b.pluginsMutex.Lock() b.plugins = append(b.plugins, plugin) diff --git a/cli/hrp/cmd/run.go b/cli/hrp/cmd/run.go index 0e4ba338..5a967679 100644 --- a/cli/hrp/cmd/run.go +++ b/cli/hrp/cmd/run.go @@ -26,12 +26,17 @@ var runCmd = &cobra.Command{ paths = append(paths, &hrp.TestCasePath{Path: arg}) } runner := hrp.NewRunner(nil). - SetDebug(!silentFlag). SetFailfast(!continueOnFailure). SetSaveTests(saveTests) if genHTMLReport { runner.GenHTMLReport() } + if !requestsLogOff { + runner.SetRequestsLogOn() + } + if pluginLogOn { + runner.SetPluginLogOn() + } if proxyUrl != "" { runner.SetProxyUrl(proxyUrl) } @@ -44,7 +49,8 @@ var runCmd = &cobra.Command{ var ( continueOnFailure bool - silentFlag bool + requestsLogOff bool + pluginLogOn bool proxyUrl string saveTests bool genHTMLReport bool @@ -52,9 +58,10 @@ var ( func init() { rootCmd.AddCommand(runCmd) - runCmd.Flags().BoolVar(&continueOnFailure, "continue-on-failure", false, "continue running next step when failure occurs") - runCmd.Flags().BoolVarP(&silentFlag, "silent", "s", false, "disable logging request & response details") + runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs") + runCmd.Flags().BoolVar(&requestsLogOff, "log-requests-off", false, "turn off request & response details logging") + runCmd.Flags().BoolVar(&pluginLogOn, "log-plugin", false, "turn on plugin logging") runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url") - runCmd.Flags().BoolVar(&saveTests, "save-tests", false, "save tests summary") - runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "r", false, "generate html report") + runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary") + runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report") } diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 04adeda2..e0e66c1d 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 22-Feb-2022 +###### Auto generated by spf13/cobra on 25-Feb-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index ba0861dd..bc44844e 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 22-Feb-2022 +###### Auto generated by spf13/cobra on 25-Feb-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index 686ed7b1..f9423ae4 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 22-Feb-2022 +###### Auto generated by spf13/cobra on 25-Feb-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 0cd5715c..f898999f 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -21,16 +21,17 @@ hrp run $path... [flags] ### Options ``` - --continue-on-failure continue running next step when failure occurs - -r, --gen-html-report generate html report + -c, --continue-on-failure continue running next step when failure occurs + -g, --gen-html-report generate html report -h, --help help for run + --log-plugin turn on plugin logging + --log-requests-off turn off request & response details logging -p, --proxy-url string set proxy url - --save-tests save tests summary - -s, --silent disable logging request & response details + -s, --save-tests save tests summary ``` ### SEE ALSO * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 22-Feb-2022 +###### Auto generated by spf13/cobra on 25-Feb-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index b04a4dac..3f9654cb 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 22-Feb-2022 +###### Auto generated by spf13/cobra on 25-Feb-2022 diff --git a/parser.go b/parser.go index ce2a8fe3..7cfccf55 100644 --- a/parser.go +++ b/parser.go @@ -13,7 +13,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/hrp/internal/builtin" - "github.com/httprunner/hrp/plugin/common" + pluginInternal "github.com/httprunner/hrp/plugin/inner" ) func newParser() *parser { @@ -21,7 +21,7 @@ func newParser() *parser { } type parser struct { - plugin common.Plugin // plugin is used to call functions + plugin pluginInternal.IPlugin // plugin is used to call functions } func buildURL(baseURL, stepURL string) string { @@ -252,7 +252,7 @@ func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{ fn := reflect.ValueOf(function) // call with builtin function - return common.CallFunc(fn, arguments...) + return pluginInternal.CallFunc(fn, arguments...) } // merge two variables mapping, the first variables have higher priority diff --git a/plugin/common/init.go b/plugin/common/init.go deleted file mode 100644 index e8f757bf..00000000 --- a/plugin/common/init.go +++ /dev/null @@ -1,209 +0,0 @@ -package common - -import ( - "fmt" - "os" - "path/filepath" - "plugin" - "reflect" - "runtime" - - "github.com/rs/zerolog/log" - - pluginHost "github.com/httprunner/hrp/plugin/host" - pluginShared "github.com/httprunner/hrp/plugin/shared" -) - -type pluginFile string - -const ( - goPluginFile pluginFile = pluginShared.Name + ".so" // built from go plugin - hashicorpGoPluginFile pluginFile = pluginShared.Name + ".bin" // built from hashicorp go plugin - hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py" -) - -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 -type GoPlugin struct { - *plugin.Plugin - cachedFunctions map[string]reflect.Value // cache loaded functions to improve performance -} - -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 - p.Plugin, err = plugin.Open(path) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("load go plugin failed") - return err - } - - p.cachedFunctions = make(map[string]reflect.Value) - log.Info().Str("path", path).Msg("load go plugin success") - return nil -} - -func (p *GoPlugin) Has(funcName string) bool { - fn, ok := p.cachedFunctions[funcName] - if ok { - return fn.IsValid() - } - - sym, err := p.Plugin.Lookup(funcName) - if err != nil { - p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid - return false - } - fn = reflect.ValueOf(sym) - - // check function type - if fn.Kind() != reflect.Func { - p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid - return false - } - - p.cachedFunctions[funcName] = fn - return true -} - -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 CallFunc(fn, args...) -} - -func (p *GoPlugin) Quit() error { - // no need to quit for go plugin - return nil -} - -// 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 { - - f, err := pluginHost.Init(path) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("load go hashicorp plugin failed") - return err - } - p.FuncCaller = f - - p.cachedFunctions = make(map[string]bool) - log.Info().Str("path", path).Msg("load hashicorp go plugin success") - return nil -} - -func (p *HashicorpPlugin) Has(funcName string) bool { - flag, ok := p.cachedFunctions[funcName] - if ok { - return flag - } - - funcNames, err := p.GetNames() - if err != nil { - return false - } - - for _, name := range funcNames { - if name == funcName { - p.cachedFunctions[funcName] = true // cache as exists - return true - } - } - - p.cachedFunctions[funcName] = false // cache as not exists - return false -} - -func (p *HashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { - return p.FuncCaller.Call(funcName, args...) -} - -func (p *HashicorpPlugin) Quit() error { - // kill hashicorp plugin process - log.Info().Msg("quit hashicorp plugin process") - pluginHost.Quit() - return nil -} - -func Init(path string) (Plugin, error) { - if path == "" { - return nil, nil - } - var plugin Plugin - - // priority: hashicorp plugin > go plugin - // locate hashicorp plugin file - pluginPath, err := locateFile(path, hashicorpGoPluginFile) - if err == nil { - // found hashicorp go plugin file - plugin = &HashicorpPlugin{} - err = plugin.Init(pluginPath) - return plugin, err - } - - // locate go plugin file - pluginPath, err = locateFile(path, goPluginFile) - if err == nil { - // found go plugin file - plugin = &GoPlugin{} - err = plugin.Init(pluginPath) - return plugin, err - } - - // plugin not found - return nil, nil -} - -// locateFile searches destFile upward recursively until current -// working directory or system root dir. -func locateFile(startPath string, destFile pluginFile) (string, error) { - stat, err := os.Stat(startPath) - if os.IsNotExist(err) { - return "", err - } - - var startDir string - if stat.IsDir() { - startDir = startPath - } else { - startDir = filepath.Dir(startPath) - } - startDir, _ = filepath.Abs(startDir) - - // convention over configuration - pluginPath := filepath.Join(startDir, string(destFile)) - if _, err := os.Stat(pluginPath); err == nil { - return pluginPath, nil - } - - // current working directory - cwd, _ := os.Getwd() - if startDir == cwd { - return "", fmt.Errorf("searched to CWD, plugin file not found") - } - - // system root dir - parentDir, _ := filepath.Abs(filepath.Dir(startDir)) - if parentDir == startDir { - return "", fmt.Errorf("searched to system root dir, plugin file not found") - } - - return locateFile(parentDir, destFile) -} diff --git a/plugin/host/host.go b/plugin/host/host.go deleted file mode 100644 index e815c7e5..00000000 --- a/plugin/host/host.go +++ /dev/null @@ -1,50 +0,0 @@ -package host - -import ( - "os" - "os/exec" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-plugin" - - "github.com/httprunner/hrp/plugin/shared" -) - -var client *plugin.Client - -func Init(path string) (shared.FuncCaller, error) { - // launch the plugin process - client = plugin.NewClient(&plugin.ClientConfig{ - HandshakeConfig: shared.HandshakeConfig, - Plugins: map[string]plugin.Plugin{ - shared.Name: &shared.HashicorpPlugin{}, - }, - Cmd: exec.Command(path), - Logger: hclog.New(&hclog.LoggerOptions{ - Name: shared.Name, - Output: os.Stdout, - Level: hclog.Info, - }), - }) - - // Connect via RPC - rpcClient, err := client.Client() - if err != nil { - return nil, err - } - - // Request the plugin - raw, err := rpcClient.Dispense(shared.Name) - if err != nil { - return nil, err - } - - // We should have a Function now! This feels like a normal interface - // implementation but is in fact over an RPC connection. - function := raw.(shared.FuncCaller) - return function, nil -} - -func Quit() { - client.Kill() -} diff --git a/plugin/common/call.go b/plugin/inner/call.go similarity index 99% rename from plugin/common/call.go rename to plugin/inner/call.go index 02238cc9..16ce2a42 100644 --- a/plugin/common/call.go +++ b/plugin/inner/call.go @@ -1,4 +1,4 @@ -package common +package pluginInternal import ( "fmt" diff --git a/plugin/common/call_test.go b/plugin/inner/call_test.go similarity index 99% rename from plugin/common/call_test.go rename to plugin/inner/call_test.go index 03b33ba6..b47633e4 100644 --- a/plugin/common/call_test.go +++ b/plugin/inner/call_test.go @@ -1,4 +1,4 @@ -package common +package pluginInternal import ( "errors" diff --git a/plugin/shared/config.go b/plugin/inner/config.go similarity index 83% rename from plugin/shared/config.go rename to plugin/inner/config.go index 423688b1..b5597fde 100644 --- a/plugin/shared/config.go +++ b/plugin/inner/config.go @@ -1,8 +1,8 @@ -package shared +package pluginInternal import "github.com/hashicorp/go-plugin" -const Name = "debugtalk" +const PluginName = "debugtalk" // handshakeConfigs are used to just do a basic handshake between // a plugin and host. If the handshake fails, a user friendly error is shown. @@ -11,5 +11,5 @@ const Name = "debugtalk" var HandshakeConfig = plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "HttpRunnerPlus", - MagicCookieValue: Name, + MagicCookieValue: PluginName, } diff --git a/plugin/inner/go_plugin.go b/plugin/inner/go_plugin.go new file mode 100644 index 00000000..1fb454e5 --- /dev/null +++ b/plugin/inner/go_plugin.go @@ -0,0 +1,70 @@ +package pluginInternal + +import ( + "fmt" + "plugin" + "reflect" + "runtime" + + "github.com/rs/zerolog/log" +) + +// 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 { + 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 + p.Plugin, err = plugin.Open(path) + if err != nil { + log.Error().Err(err).Str("path", path).Msg("load go plugin failed") + return err + } + + p.cachedFunctions = make(map[string]reflect.Value) + log.Info().Str("path", path).Msg("load go plugin success") + return nil +} + +func (p *GoPlugin) Has(funcName string) bool { + fn, ok := p.cachedFunctions[funcName] + if ok { + return fn.IsValid() + } + + sym, err := p.Plugin.Lookup(funcName) + if err != nil { + p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid + return false + } + fn = reflect.ValueOf(sym) + + // check function type + if fn.Kind() != reflect.Func { + p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid + return false + } + + p.cachedFunctions[funcName] = fn + return true +} + +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 CallFunc(fn, args...) +} + +func (p *GoPlugin) Quit() error { + // no need to quit for go plugin + return nil +} diff --git a/plugin/common/go_plugin_test.go b/plugin/inner/go_plugin_test.go similarity index 96% rename from plugin/common/go_plugin_test.go rename to plugin/inner/go_plugin_test.go index fec89180..20c1569c 100644 --- a/plugin/common/go_plugin_test.go +++ b/plugin/inner/go_plugin_test.go @@ -1,7 +1,7 @@ // +build linux freebsd darwin // go plugin doesn't support windows -package common +package pluginInternal import ( "fmt" @@ -70,7 +70,7 @@ func TestCallPluginFunction(t *testing.T) { buildGoPlugin() defer removeGoPlugin() - plugin, err := Init("debugtalk.so") + plugin, err := Init("debugtalk.so", false) if err != nil { t.Fatal(err) } diff --git a/plugin/inner/hashicorp_plugin.go b/plugin/inner/hashicorp_plugin.go new file mode 100644 index 00000000..246ba0ef --- /dev/null +++ b/plugin/inner/hashicorp_plugin.go @@ -0,0 +1,95 @@ +package pluginInternal + +import ( + "os" + "os/exec" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/rs/zerolog/log" +) + +var client *plugin.Client + +// HashicorpPlugin implements hashicorp/go-plugin +type HashicorpPlugin struct { + logOn bool // turn on plugin log + FuncCaller + cachedFunctions map[string]bool // cache loaded functions to improve performance +} + +func (p *HashicorpPlugin) Init(path string) error { + loggerOptions := &hclog.LoggerOptions{ + Name: PluginName, + Output: os.Stdout, + } + if p.logOn { + loggerOptions.Level = hclog.Debug + } else { + loggerOptions.Level = hclog.Info + } + // launch the plugin process + client = plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: map[string]plugin.Plugin{ + PluginName: &HRPPlugin{}, + }, + Cmd: exec.Command(path), + Logger: hclog.New(loggerOptions), + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + log.Error().Err(err).Msg("connect plugin via RPC failed") + return err + } + + // Request the plugin + raw, err := rpcClient.Dispense(PluginName) + if err != nil { + log.Error().Err(err).Msg("request plugin failed") + return err + } + + // We should have a Function now! This feels like a normal interface + // implementation but is in fact over an RPC connection. + p.FuncCaller = raw.(FuncCaller) + + p.cachedFunctions = make(map[string]bool) + log.Info().Str("path", path).Msg("load hashicorp go plugin success") + return nil +} + +func (p *HashicorpPlugin) Has(funcName string) bool { + flag, ok := p.cachedFunctions[funcName] + if ok { + return flag + } + + funcNames, err := p.GetNames() + if err != nil { + return false + } + + for _, name := range funcNames { + if name == funcName { + p.cachedFunctions[funcName] = true // cache as exists + return true + } + } + + p.cachedFunctions[funcName] = false // cache as not exists + return false +} + +func (p *HashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { + return p.FuncCaller.Call(funcName, args...) +} + +func (p *HashicorpPlugin) Quit() error { + // kill hashicorp plugin process + log.Info().Msg("quit hashicorp plugin process") + client.Kill() + return nil +} diff --git a/plugin/common/hashicorp_plugin_test.go b/plugin/inner/hashicorp_plugin_test.go similarity index 95% rename from plugin/common/hashicorp_plugin_test.go rename to plugin/inner/hashicorp_plugin_test.go index a0c6a806..de6fe7e0 100644 --- a/plugin/common/hashicorp_plugin_test.go +++ b/plugin/inner/hashicorp_plugin_test.go @@ -1,4 +1,4 @@ -package common +package pluginInternal import ( "os" @@ -28,7 +28,7 @@ func TestInitHashicorpPlugin(t *testing.T) { buildHashicorpPlugin() defer removeHashicorpPlugin() - plugin, err := Init("../../examples/debugtalk.bin") + plugin, err := Init("../../examples/debugtalk.bin", false) if err != nil { t.Fatal(err) } diff --git a/plugin/shared/interface.go b/plugin/inner/host.go similarity index 79% rename from plugin/shared/interface.go rename to plugin/inner/host.go index 9f334808..a8422a38 100644 --- a/plugin/shared/interface.go +++ b/plugin/inner/host.go @@ -1,4 +1,4 @@ -package shared +package pluginInternal import ( "encoding/gob" @@ -89,15 +89,22 @@ func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error { return nil } -// HashicorpPlugin implements hashicorp's plugin.Plugin. -type HashicorpPlugin struct { +// HRPPlugin implements hashicorp's plugin.Plugin. +type HRPPlugin struct { Impl FuncCaller } -func (p *HashicorpPlugin) Server(*plugin.MuxBroker) (interface{}, error) { +func (p *HRPPlugin) Server(*plugin.MuxBroker) (interface{}, error) { return &functionRPCServer{Impl: p.Impl}, nil } -func (HashicorpPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { +func (HRPPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { return &functionRPC{client: c}, nil } + +type IPlugin 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 +} diff --git a/plugin/inner/init.go b/plugin/inner/init.go new file mode 100644 index 00000000..b0f0ed3f --- /dev/null +++ b/plugin/inner/init.go @@ -0,0 +1,83 @@ +package pluginInternal + +import ( + "fmt" + "os" + "path/filepath" +) + +type pluginFile string + +const ( + goPluginFile pluginFile = PluginName + ".so" // built from go plugin + hashicorpGoPluginFile pluginFile = PluginName + ".bin" // built from hashicorp go plugin + hashicorpPyPluginFile pluginFile = PluginName + ".py" +) + +func Init(path string, logOn bool) (IPlugin, error) { + if path == "" { + return nil, nil + } + var plugin IPlugin + + // priority: hashicorp plugin > go plugin + // locate hashicorp plugin file + pluginPath, err := locateFile(path, hashicorpGoPluginFile) + if err == nil { + // found hashicorp go plugin file + plugin = &HashicorpPlugin{ + logOn: logOn, + } + err = plugin.Init(pluginPath) + return plugin, err + } + + // locate go plugin file + pluginPath, err = locateFile(path, goPluginFile) + if err == nil { + // found go plugin file + plugin = &GoPlugin{} + err = plugin.Init(pluginPath) + return plugin, err + } + + // plugin not found + return nil, nil +} + +// locateFile searches destFile upward recursively until current +// working directory or system root dir. +func locateFile(startPath string, destFile pluginFile) (string, error) { + stat, err := os.Stat(startPath) + if os.IsNotExist(err) { + return "", err + } + + var startDir string + if stat.IsDir() { + startDir = startPath + } else { + startDir = filepath.Dir(startPath) + } + startDir, _ = filepath.Abs(startDir) + + // convention over configuration + pluginPath := filepath.Join(startDir, string(destFile)) + if _, err := os.Stat(pluginPath); err == nil { + return pluginPath, nil + } + + // current working directory + cwd, _ := os.Getwd() + if startDir == cwd { + return "", fmt.Errorf("searched to CWD, plugin file not found") + } + + // system root dir + parentDir, _ := filepath.Abs(filepath.Dir(startDir)) + if parentDir == startDir { + return "", fmt.Errorf("searched to system root dir, plugin file not found") + } + + return locateFile(parentDir, destFile) +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 4dc25731..14435c3a 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -8,8 +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" + pluginInternal "github.com/httprunner/hrp/plugin/inner" ) // functionsMap stores plugin functions @@ -37,7 +36,7 @@ func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{} return nil, fmt.Errorf("function %s not found", funcName) } - return common.CallFunc(fn, args...) + return pluginInternal.CallFunc(fn, args...) } var functions = make(functionsMap) @@ -55,17 +54,17 @@ func Register(funcName string, fn interface{}) { func Serve() { funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ - Name: pluginShared.Name, + Name: pluginInternal.PluginName, Output: os.Stdout, Level: hclog.Info, }), functions: functions, } var pluginMap = map[string]plugin.Plugin{ - pluginShared.Name: &pluginShared.HashicorpPlugin{Impl: funcPlugin}, + pluginInternal.PluginName: &pluginInternal.HRPPlugin{Impl: funcPlugin}, } plugin.Serve(&plugin.ServeConfig{ - HandshakeConfig: pluginShared.HandshakeConfig, + HandshakeConfig: pluginInternal.HandshakeConfig, Plugins: pluginMap, }) } diff --git a/runner.go b/runner.go index 50ddb2a4..7dd0178b 100644 --- a/runner.go +++ b/runner.go @@ -32,7 +32,7 @@ import ( "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" - "github.com/httprunner/hrp/plugin/common" + pluginInternal "github.com/httprunner/hrp/plugin/inner" ) const ( @@ -43,7 +43,7 @@ const ( // Run starts to run API test with default configs. func Run(testcases ...ITestCase) error { t := &testing.T{} - return NewRunner(t).SetDebug(true).Run(testcases...) + return NewRunner(t).SetRequestsLogOn().Run(testcases...) } // NewRunner constructs a new runner instance. @@ -53,8 +53,7 @@ func NewRunner(t *testing.T) *HRPRunner { } return &HRPRunner{ t: t, - failfast: true, // default to failfast - debug: false, // default to turn off debug + failfast: true, // default to failfast genHTMLReport: false, client: &http.Client{ Transport: &http.Transport{ @@ -68,7 +67,8 @@ func NewRunner(t *testing.T) *HRPRunner { type HRPRunner struct { t *testing.T failfast bool - debug bool + requestsLogOn bool + pluginLogOn bool saveTests bool genHTMLReport bool client *http.Client @@ -81,10 +81,17 @@ func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner { return r } -// SetDebug configures whether to log HTTP request and response content. -func (r *HRPRunner) SetDebug(debug bool) *HRPRunner { - log.Info().Bool("debug", debug).Msg("[init] SetDebug") - r.debug = debug +// SetRequestsLogOn turns on request & response details logging. +func (r *HRPRunner) SetRequestsLogOn() *HRPRunner { + log.Info().Msg("[init] SetRequestsLogOn") + r.requestsLogOn = true + return r +} + +// SetPluginLogOn turns on plugin logging. +func (r *HRPRunner) SetPluginLogOn() *HRPRunner { + log.Info().Msg("[init] SetPluginLogOn") + r.pluginLogOn = true return r } @@ -221,7 +228,7 @@ func (r *caseRunner) run() error { config := r.TestCase.Config // init plugin var err error - if r.parser.plugin, err = initPlugin(config.Path); err != nil { + if r.parser.plugin, err = initPlugin(config.Path, r.hrpRunner.pluginLogOn); err != nil { return err } defer func() { @@ -247,9 +254,7 @@ func (r *caseRunner) run() error { // merge test case if the step is test case summary, ok := stepDataObj.Data.(*testCaseSummary) if ok { - for _, rc := range summary.Records { - r.summary.Records = append(r.summary.Records, rc) - } + r.summary.Records = append(r.summary.Records, summary.Records...) r.summary.Stat.Total += summary.Stat.Total r.summary.Stat.Successes += summary.Stat.Successes r.summary.Stat.Failures += summary.Stat.Failures @@ -277,8 +282,8 @@ func (r *caseRunner) run() error { return nil } -func initPlugin(path string) (plugin common.Plugin, err error) { - plugin, err = common.Init(path) +func initPlugin(path string, logOn bool) (plugin pluginInternal.IPlugin, err error) { + plugin, err = pluginInternal.Init(path, logOn) if plugin == nil { return } @@ -294,7 +299,7 @@ func initPlugin(path string) (plugin common.Plugin, err error) { // report event for initializing plugin var pluginType string - if _, ok := plugin.(*common.GoPlugin); ok { + if _, ok := plugin.(*pluginInternal.GoPlugin); ok { pluginType = "go" } else { pluginType = "hashicorp" @@ -616,14 +621,6 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro } sessionData := newSessionData() - // deal with setup hooks - for _, setupHook := range step.SetupHooks { - _, err = r.parser.parseData(setupHook, step.Variables) - if err != nil { - return stepResult, errors.Wrap(err, "run setup hooks failed") - } - } - // convert request struct to map jsonRequest, _ := json.Marshal(&step.Request) var requestMap map[string]interface{} @@ -764,6 +761,18 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro req.URL = u req.Host = u.Host + // add request object to step variables, could be used in setup hooks + step.Variables["hrp_step_name"] = step.Name + step.Variables["hrp_step_request"] = requestMap + + // deal with setup hooks + for _, setupHook := range step.SetupHooks { + _, err = r.parser.parseData(setupHook, step.Variables) + if err != nil { + return stepResult, errors.Wrap(err, "run setup hooks failed") + } + } + // log & print request if err := r.printRequest(req); err != nil { return stepResult, err @@ -795,6 +804,18 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro err = errors.Wrap(err, "init ResponseObject error") return } + + // add response object to step variables, could be used in teardown hooks + step.Variables["hrp_step_response"] = respObj.respObjMeta + + // deal with teardown hooks + for _, teardownHook := range step.TeardownHooks { + _, err = r.parser.parseData(teardownHook, step.Variables) + if err != nil { + return stepResult, errors.Wrap(err, "run teardown hooks failed") + } + } + sessionData.ReqResps.Request = requestMap sessionData.ReqResps.Response = builtin.FormatResponse(respObj.respObjMeta) @@ -816,18 +837,11 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro stepResult.ContentSize = resp.ContentLength stepResult.Data = sessionData - // deal with teardown hooks - for _, teardownHook := range step.TeardownHooks { - _, err = r.parser.parseData(teardownHook, step.Variables) - if err != nil { - return stepResult, errors.Wrap(err, "run teardown hooks failed") - } - } return stepResult, err } func (r *caseRunner) printRequest(req *http.Request) error { - if !r.hrpRunner.debug { + if !r.hrpRunner.requestsLogOn { return nil } reqContentType := req.Header.Get("Content-Type") @@ -846,7 +860,7 @@ func (r *caseRunner) printRequest(req *http.Request) error { } func (r *caseRunner) printResponse(resp *http.Response) error { - if !r.hrpRunner.debug { + if !r.hrpRunner.requestsLogOn { return nil } fmt.Println("==================== response ===================") @@ -877,7 +891,7 @@ func shouldPrintBody(contentType string) bool { if strings.HasPrefix(contentType, "application/xml") { return true } - if strings.HasPrefix(contentType, "application/www-form-urlencoded") { + if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { return true } return false diff --git a/step_test.go b/step_test.go index b81b48d3..f180983f 100644 --- a/step_test.go +++ b/step_test.go @@ -77,7 +77,7 @@ func TestRunRequestRun(t *testing.T) { Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), TestSteps: []IStep{stepGET, stepPOSTData}, } - runner := NewRunner(t).SetDebug(true).newCaseRunner(testcase) + runner := NewRunner(t).SetRequestsLogOn().newCaseRunner(testcase) if _, err := runner.runStep(0, testcase.Config); err != nil { t.Fatalf("tStep.Run() error: %s", err) }