Merge pull request #124 from httprunner/refactor-plugin

refactor plugin
This commit is contained in:
debugtalk
2022-02-25 23:07:49 +08:00
committed by GitHub
22 changed files with 354 additions and 345 deletions

View File

@@ -9,29 +9,21 @@ import (
"github.com/httprunner/hrp/internal/boomer" "github.com/httprunner/hrp/internal/boomer"
"github.com/httprunner/hrp/internal/ga" "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 { func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
b := &HRPBoomer{ b := &HRPBoomer{
Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate), Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate),
pluginsMutex: new(sync.RWMutex), pluginsMutex: new(sync.RWMutex),
debug: false,
} }
return b return b
} }
type HRPBoomer struct { type HRPBoomer struct {
*boomer.Boomer *boomer.Boomer
plugins []common.Plugin // each task has its own plugin process plugins []pluginInternal.IPlugin // each task has its own plugin process
pluginsMutex *sync.RWMutex // avoid data race 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
} }
// Run starts to run load test for one or multiple testcases. // 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 { func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
hrpRunner := NewRunner(nil).SetDebug(b.debug) hrpRunner := NewRunner(nil)
config := testcase.Config config := testcase.Config
// each testcase has its own plugin process // each testcase has its own plugin process
plugin, _ := initPlugin(config.Path) plugin, _ := initPlugin(config.Path, false)
if plugin != nil { if plugin != nil {
b.pluginsMutex.Lock() b.pluginsMutex.Lock()
b.plugins = append(b.plugins, plugin) b.plugins = append(b.plugins, plugin)

View File

@@ -26,12 +26,17 @@ var runCmd = &cobra.Command{
paths = append(paths, &hrp.TestCasePath{Path: arg}) paths = append(paths, &hrp.TestCasePath{Path: arg})
} }
runner := hrp.NewRunner(nil). runner := hrp.NewRunner(nil).
SetDebug(!silentFlag).
SetFailfast(!continueOnFailure). SetFailfast(!continueOnFailure).
SetSaveTests(saveTests) SetSaveTests(saveTests)
if genHTMLReport { if genHTMLReport {
runner.GenHTMLReport() runner.GenHTMLReport()
} }
if !requestsLogOff {
runner.SetRequestsLogOn()
}
if pluginLogOn {
runner.SetPluginLogOn()
}
if proxyUrl != "" { if proxyUrl != "" {
runner.SetProxyUrl(proxyUrl) runner.SetProxyUrl(proxyUrl)
} }
@@ -44,7 +49,8 @@ var runCmd = &cobra.Command{
var ( var (
continueOnFailure bool continueOnFailure bool
silentFlag bool requestsLogOff bool
pluginLogOn bool
proxyUrl string proxyUrl string
saveTests bool saveTests bool
genHTMLReport bool genHTMLReport bool
@@ -52,9 +58,10 @@ var (
func init() { func init() {
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
runCmd.Flags().BoolVar(&continueOnFailure, "continue-on-failure", false, "continue running next step when failure occurs") runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs")
runCmd.Flags().BoolVarP(&silentFlag, "silent", "s", false, "disable logging request & response details") 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().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
runCmd.Flags().BoolVar(&saveTests, "save-tests", false, "save tests summary") runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "r", false, "generate html report") runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
} }

View File

@@ -33,4 +33,4 @@ Copyright 2021 debugtalk
* [hrp run](hrp_run.md) - run API test * [hrp run](hrp_run.md) - run API test
* [hrp startproject](hrp_startproject.md) - create a scaffold project * [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

View File

@@ -39,4 +39,4 @@ hrp boom [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [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

View File

@@ -23,4 +23,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [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

View File

@@ -21,16 +21,17 @@ hrp run $path... [flags]
### Options ### Options
``` ```
--continue-on-failure continue running next step when failure occurs -c, --continue-on-failure continue running next step when failure occurs
-r, --gen-html-report generate html report -g, --gen-html-report generate html report
-h, --help help for run -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 -p, --proxy-url string set proxy url
--save-tests save tests summary -s, --save-tests save tests summary
-s, --silent disable logging request & response details
``` ```
### SEE ALSO ### SEE ALSO
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [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

View File

@@ -16,4 +16,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [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

View File

@@ -13,7 +13,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/plugin/common" pluginInternal "github.com/httprunner/hrp/plugin/inner"
) )
func newParser() *parser { func newParser() *parser {
@@ -21,7 +21,7 @@ func newParser() *parser {
} }
type parser struct { 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 { func buildURL(baseURL, stepURL string) string {
@@ -252,7 +252,7 @@ func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{
fn := reflect.ValueOf(function) fn := reflect.ValueOf(function)
// call with builtin 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 // merge two variables mapping, the first variables have higher priority

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -1,4 +1,4 @@
package common package pluginInternal
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package common package pluginInternal
import ( import (
"errors" "errors"

View File

@@ -1,8 +1,8 @@
package shared package pluginInternal
import "github.com/hashicorp/go-plugin" import "github.com/hashicorp/go-plugin"
const Name = "debugtalk" const PluginName = "debugtalk"
// handshakeConfigs are used to just do a basic handshake between // handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown. // 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{ var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "HttpRunnerPlus", MagicCookieKey: "HttpRunnerPlus",
MagicCookieValue: Name, MagicCookieValue: PluginName,
} }

70
plugin/inner/go_plugin.go Normal file
View File

@@ -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
}

View File

@@ -1,7 +1,7 @@
// +build linux freebsd darwin // +build linux freebsd darwin
// go plugin doesn't support windows // go plugin doesn't support windows
package common package pluginInternal
import ( import (
"fmt" "fmt"
@@ -70,7 +70,7 @@ func TestCallPluginFunction(t *testing.T) {
buildGoPlugin() buildGoPlugin()
defer removeGoPlugin() defer removeGoPlugin()
plugin, err := Init("debugtalk.so") plugin, err := Init("debugtalk.so", false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
package common package pluginInternal
import ( import (
"os" "os"
@@ -28,7 +28,7 @@ func TestInitHashicorpPlugin(t *testing.T) {
buildHashicorpPlugin() buildHashicorpPlugin()
defer removeHashicorpPlugin() defer removeHashicorpPlugin()
plugin, err := Init("../../examples/debugtalk.bin") plugin, err := Init("../../examples/debugtalk.bin", false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -1,4 +1,4 @@
package shared package pluginInternal
import ( import (
"encoding/gob" "encoding/gob"
@@ -89,15 +89,22 @@ func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error {
return nil return nil
} }
// HashicorpPlugin implements hashicorp's plugin.Plugin. // HRPPlugin implements hashicorp's plugin.Plugin.
type HashicorpPlugin struct { type HRPPlugin struct {
Impl FuncCaller Impl FuncCaller
} }
func (p *HashicorpPlugin) Server(*plugin.MuxBroker) (interface{}, error) { func (p *HRPPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &functionRPCServer{Impl: p.Impl}, nil 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 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
}

83
plugin/inner/init.go Normal file
View File

@@ -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)
}

View File

@@ -8,8 +8,7 @@ import (
hclog "github.com/hashicorp/go-hclog" hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/httprunner/hrp/plugin/common" pluginInternal "github.com/httprunner/hrp/plugin/inner"
pluginShared "github.com/httprunner/hrp/plugin/shared"
) )
// functionsMap stores plugin functions // 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 nil, fmt.Errorf("function %s not found", funcName)
} }
return common.CallFunc(fn, args...) return pluginInternal.CallFunc(fn, args...)
} }
var functions = make(functionsMap) var functions = make(functionsMap)
@@ -55,17 +54,17 @@ func Register(funcName string, fn interface{}) {
func Serve() { func Serve() {
funcPlugin := &functionPlugin{ funcPlugin := &functionPlugin{
logger: hclog.New(&hclog.LoggerOptions{ logger: hclog.New(&hclog.LoggerOptions{
Name: pluginShared.Name, Name: pluginInternal.PluginName,
Output: os.Stdout, Output: os.Stdout,
Level: hclog.Info, Level: hclog.Info,
}), }),
functions: functions, functions: functions,
} }
var pluginMap = map[string]plugin.Plugin{ var pluginMap = map[string]plugin.Plugin{
pluginShared.Name: &pluginShared.HashicorpPlugin{Impl: funcPlugin}, pluginInternal.PluginName: &pluginInternal.HRPPlugin{Impl: funcPlugin},
} }
plugin.Serve(&plugin.ServeConfig{ plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: pluginShared.HandshakeConfig, HandshakeConfig: pluginInternal.HandshakeConfig,
Plugins: pluginMap, Plugins: pluginMap,
}) })
} }

View File

@@ -32,7 +32,7 @@ import (
"github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common" pluginInternal "github.com/httprunner/hrp/plugin/inner"
) )
const ( const (
@@ -43,7 +43,7 @@ const (
// Run starts to run API test with default configs. // Run starts to run API test with default configs.
func Run(testcases ...ITestCase) error { func Run(testcases ...ITestCase) error {
t := &testing.T{} t := &testing.T{}
return NewRunner(t).SetDebug(true).Run(testcases...) return NewRunner(t).SetRequestsLogOn().Run(testcases...)
} }
// NewRunner constructs a new runner instance. // NewRunner constructs a new runner instance.
@@ -53,8 +53,7 @@ func NewRunner(t *testing.T) *HRPRunner {
} }
return &HRPRunner{ return &HRPRunner{
t: t, t: t,
failfast: true, // default to failfast failfast: true, // default to failfast
debug: false, // default to turn off debug
genHTMLReport: false, genHTMLReport: false,
client: &http.Client{ client: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
@@ -68,7 +67,8 @@ func NewRunner(t *testing.T) *HRPRunner {
type HRPRunner struct { type HRPRunner struct {
t *testing.T t *testing.T
failfast bool failfast bool
debug bool requestsLogOn bool
pluginLogOn bool
saveTests bool saveTests bool
genHTMLReport bool genHTMLReport bool
client *http.Client client *http.Client
@@ -81,10 +81,17 @@ func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner {
return r return r
} }
// SetDebug configures whether to log HTTP request and response content. // SetRequestsLogOn turns on request & response details logging.
func (r *HRPRunner) SetDebug(debug bool) *HRPRunner { func (r *HRPRunner) SetRequestsLogOn() *HRPRunner {
log.Info().Bool("debug", debug).Msg("[init] SetDebug") log.Info().Msg("[init] SetRequestsLogOn")
r.debug = debug 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 return r
} }
@@ -221,7 +228,7 @@ func (r *caseRunner) run() error {
config := r.TestCase.Config config := r.TestCase.Config
// init plugin // init plugin
var err error 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 return err
} }
defer func() { defer func() {
@@ -247,9 +254,7 @@ func (r *caseRunner) run() error {
// merge test case if the step is test case // merge test case if the step is test case
summary, ok := stepDataObj.Data.(*testCaseSummary) summary, ok := stepDataObj.Data.(*testCaseSummary)
if ok { if ok {
for _, rc := range summary.Records { r.summary.Records = append(r.summary.Records, summary.Records...)
r.summary.Records = append(r.summary.Records, rc)
}
r.summary.Stat.Total += summary.Stat.Total r.summary.Stat.Total += summary.Stat.Total
r.summary.Stat.Successes += summary.Stat.Successes r.summary.Stat.Successes += summary.Stat.Successes
r.summary.Stat.Failures += summary.Stat.Failures r.summary.Stat.Failures += summary.Stat.Failures
@@ -277,8 +282,8 @@ func (r *caseRunner) run() error {
return nil return nil
} }
func initPlugin(path string) (plugin common.Plugin, err error) { func initPlugin(path string, logOn bool) (plugin pluginInternal.IPlugin, err error) {
plugin, err = common.Init(path) plugin, err = pluginInternal.Init(path, logOn)
if plugin == nil { if plugin == nil {
return return
} }
@@ -294,7 +299,7 @@ func initPlugin(path string) (plugin common.Plugin, err error) {
// report event for initializing plugin // report event for initializing plugin
var pluginType string var pluginType string
if _, ok := plugin.(*common.GoPlugin); ok { if _, ok := plugin.(*pluginInternal.GoPlugin); ok {
pluginType = "go" pluginType = "go"
} else { } else {
pluginType = "hashicorp" pluginType = "hashicorp"
@@ -616,14 +621,6 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
} }
sessionData := newSessionData() 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 // convert request struct to map
jsonRequest, _ := json.Marshal(&step.Request) jsonRequest, _ := json.Marshal(&step.Request)
var requestMap map[string]interface{} var requestMap map[string]interface{}
@@ -764,6 +761,18 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
req.URL = u req.URL = u
req.Host = u.Host 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 // log & print request
if err := r.printRequest(req); err != nil { if err := r.printRequest(req); err != nil {
return stepResult, err return stepResult, err
@@ -795,6 +804,18 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
err = errors.Wrap(err, "init ResponseObject error") err = errors.Wrap(err, "init ResponseObject error")
return 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.Request = requestMap
sessionData.ReqResps.Response = builtin.FormatResponse(respObj.respObjMeta) 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.ContentSize = resp.ContentLength
stepResult.Data = sessionData 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 return stepResult, err
} }
func (r *caseRunner) printRequest(req *http.Request) error { func (r *caseRunner) printRequest(req *http.Request) error {
if !r.hrpRunner.debug { if !r.hrpRunner.requestsLogOn {
return nil return nil
} }
reqContentType := req.Header.Get("Content-Type") 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 { func (r *caseRunner) printResponse(resp *http.Response) error {
if !r.hrpRunner.debug { if !r.hrpRunner.requestsLogOn {
return nil return nil
} }
fmt.Println("==================== response ===================") fmt.Println("==================== response ===================")
@@ -877,7 +891,7 @@ func shouldPrintBody(contentType string) bool {
if strings.HasPrefix(contentType, "application/xml") { if strings.HasPrefix(contentType, "application/xml") {
return true return true
} }
if strings.HasPrefix(contentType, "application/www-form-urlencoded") { if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
return true return true
} }
return false return false

View File

@@ -77,7 +77,7 @@ func TestRunRequestRun(t *testing.T) {
Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), Config: NewConfig("test").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{stepGET, stepPOSTData}, 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 { if _, err := runner.runStep(0, testcase.Config); err != nil {
t.Fatalf("tStep.Run() error: %s", err) t.Fatalf("tStep.Run() error: %s", err)
} }