refactor: plugin structure

This commit is contained in:
debugtalk
2022-01-17 15:18:30 +08:00
parent 24546e98e6
commit 675ded099d
8 changed files with 97 additions and 96 deletions

View File

@@ -11,6 +11,9 @@ import (
"github.com/maja42/goval" "github.com/maja42/goval"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/plugin/common"
) )
func newParser() *parser { func newParser() *parser {
@@ -18,7 +21,7 @@ func newParser() *parser {
} }
type parser struct { 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 { func buildURL(baseURL, stepURL string) string {
@@ -233,6 +236,25 @@ func (p *parser) parseString(raw string, variablesMapping map[string]interface{}
return parsedString, nil 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 // merge two variables mapping, the first variables have higher priority
func mergeVariables(variables, overriddenVariables map[string]interface{}) map[string]interface{} { func mergeVariables(variables, overriddenVariables map[string]interface{}) map[string]interface{} {
if overriddenVariables == nil { if overriddenVariables == nil {

View File

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

View File

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

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 hrp package common
import ( import (
"fmt" "fmt"
@@ -16,7 +16,7 @@ func buildGoPlugin() {
fmt.Println("[setup] build go plugin") fmt.Println("[setup] build go plugin")
// flag -race is necessary in order to be consistent with go test // flag -race is necessary in order to be consistent with go test
cmd := exec.Command("go", "build", "-buildmode=plugin", "-race", 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 { if err := cmd.Run(); err != nil {
panic(err) panic(err)
} }
@@ -24,50 +24,43 @@ func buildGoPlugin() {
func removeGoPlugin() { func removeGoPlugin() {
fmt.Println("[teardown] remove go plugin") fmt.Println("[teardown] remove go plugin")
os.Remove("examples/debugtalk.so") os.Remove("debugtalk.so")
} }
func TestLocatePlugin(t *testing.T) { func TestLocatePlugin(t *testing.T) {
buildGoPlugin() buildGoPlugin()
defer removeGoPlugin() defer removeGoPlugin()
cwd, _ := os.Getwd() _, err := locateFile("../", goPluginFile)
_, err := locatePlugin(cwd, goPluginFile)
if !assert.Error(t, err) { if !assert.Error(t, err) {
t.Fail() t.Fail()
} }
_, err = locatePlugin("", goPluginFile) _, err = locateFile("", goPluginFile)
if !assert.Error(t, err) { if !assert.Error(t, err) {
t.Fail() t.Fail()
} }
startPath := "examples/debugtalk.so" startPath := "debugtalk.so"
_, err = locatePlugin(startPath, goPluginFile) _, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fail() t.Fail()
} }
startPath = "examples/demo.json" startPath = "call.go"
_, err = locatePlugin(startPath, goPluginFile) _, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fail() t.Fail()
} }
startPath = "examples/" startPath = "."
_, err = locatePlugin(startPath, goPluginFile) _, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/plugin/debugtalk.go"
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fail() t.Fail()
} }
startPath = "/abc" startPath = "/abc"
_, err = locatePlugin(startPath, goPluginFile) _, err = locateFile(startPath, goPluginFile)
if !assert.Error(t, err) { if !assert.Error(t, err) {
t.Fail() t.Fail()
} }
@@ -75,17 +68,19 @@ func TestLocatePlugin(t *testing.T) {
func TestCallPluginFunction(t *testing.T) { func TestCallPluginFunction(t *testing.T) {
buildGoPlugin() buildGoPlugin()
removeHashicorpPlugin()
defer removeGoPlugin() defer removeGoPlugin()
parser := newParser() plugin, err := Init("debugtalk.so")
err := parser.initPlugin("examples/debugtalk.so")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !assert.True(t, plugin.Has("Concatenate")) {
t.Fail()
}
// call function without arguments // 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) { if !assert.NoError(t, err) {
t.Fail() t.Fail()
} }

View File

@@ -1,4 +1,4 @@
package hrp package common
import ( import (
"fmt" "fmt"
@@ -6,15 +6,14 @@ import (
"os/exec" "os/exec"
"testing" "testing"
"github.com/httprunner/hrp/plugin/host"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func buildHashicorpPlugin() { func buildHashicorpPlugin() {
fmt.Println("[init] build hashicorp go plugin") fmt.Println("[init] build hashicorp go plugin")
cmd := exec.Command("go", "build", cmd := exec.Command("go", "build",
"-o=examples/debugtalk.bin", "-o=debugtalk.bin",
"examples/plugin/hashicorp.go", "examples/plugin/debugtalk.go") "../../examples/plugin/hashicorp.go", "../../examples/plugin/debugtalk.go")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
panic(err) panic(err)
} }
@@ -22,46 +21,42 @@ func buildHashicorpPlugin() {
func removeHashicorpPlugin() { func removeHashicorpPlugin() {
fmt.Println("[teardown] remove hashicorp plugin") fmt.Println("[teardown] remove hashicorp plugin")
os.Remove("examples/debugtalk.bin") os.Remove("debugtalk.bin")
} }
func TestInitHashicorpPlugin(t *testing.T) { func TestInitHashicorpPlugin(t *testing.T) {
buildHashicorpPlugin() buildHashicorpPlugin()
defer removeHashicorpPlugin() defer removeHashicorpPlugin()
f, err := host.Init("examples/debugtalk.bin") plugin, err := Init("debugtalk.bin")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer host.Quit() defer plugin.Quit()
v1, err := f.GetNames() if !assert.True(t, plugin.Has("sum_ints")) {
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !assert.Contains(t, v1, "sum_ints") { if !assert.True(t, plugin.Has("concatenate")) {
t.Fatal(err)
}
if !assert.Contains(t, v1, "concatenate") {
t.Fatal(err) t.Fatal(err)
} }
var v2 interface{} 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !assert.Equal(t, 10, v2) { if !assert.Equal(t, 10, v2) {
t.Fail() t.Fail()
} }
v2, err = f.Call("sum_two_int", 1, 2) v2, err = plugin.Call("sum_two_int", 1, 2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !assert.Equal(t, 3, v2) { if !assert.Equal(t, 3, v2) {
t.Fail() 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -70,14 +65,14 @@ func TestInitHashicorpPlugin(t *testing.T) {
} }
var v3 interface{} var v3 interface{}
v3, err = f.Call("sum_two_string", "a", "b") v3, err = plugin.Call("sum_two_string", "a", "b")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !assert.Equal(t, "ab", v3) { if !assert.Equal(t, "ab", v3) {
t.Fail() t.Fail()
} }
v3, err = f.Call("sum_strings", "a", "b", "c") v3, err = plugin.Call("sum_strings", "a", "b", "c")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -85,7 +80,7 @@ func TestInitHashicorpPlugin(t *testing.T) {
t.Fail() 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -1,4 +1,4 @@
package hrp package common
import ( import (
"fmt" "fmt"
@@ -10,7 +10,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
pluginHost "github.com/httprunner/hrp/plugin/host" pluginHost "github.com/httprunner/hrp/plugin/host"
pluginShared "github.com/httprunner/hrp/plugin/shared" pluginShared "github.com/httprunner/hrp/plugin/shared"
@@ -24,11 +23,11 @@ const (
hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py" hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py"
) )
type hrpPlugin interface { type Plugin interface {
init(path string) error // init plugin Init(path string) error // init plugin
has(funcName string) bool // check if plugin has function Has(funcName string) bool // check if plugin has function
call(funcName string, args ...interface{}) (interface{}, error) // call function Call(funcName string, args ...interface{}) (interface{}, error) // call function
quit() error // quit plugin Quit() error // quit plugin
} }
// goPlugin implements golang official plugin // goPlugin implements golang official plugin
@@ -37,7 +36,7 @@ type goPlugin struct {
cachedFunctions map[string]reflect.Value // cache loaded functions to improve performance 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" { if runtime.GOOS == "windows" {
log.Warn().Msg("go plugin does not support windows") log.Warn().Msg("go plugin does not support windows")
return fmt.Errorf("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 return nil
} }
func (p *goPlugin) has(funcName string) bool { func (p *goPlugin) Has(funcName string) bool {
fn, ok := p.cachedFunctions[funcName] fn, ok := p.cachedFunctions[funcName]
if ok { if ok {
return fn.IsValid() return fn.IsValid()
@@ -90,12 +89,15 @@ func (p *goPlugin) has(funcName string) bool {
return true 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] 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 // no need to quit for go plugin
return nil return nil
} }
@@ -106,7 +108,7 @@ type hashicorpPlugin struct {
cachedFunctions map[string]bool // cache loaded functions to improve performance 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) f, err := pluginHost.Init(path)
if err != nil { if err != nil {
@@ -120,7 +122,7 @@ func (p *hashicorpPlugin) init(path string) error {
return nil return nil
} }
func (p *hashicorpPlugin) has(funcName string) bool { func (p *hashicorpPlugin) Has(funcName string) bool {
flag, ok := p.cachedFunctions[funcName] flag, ok := p.cachedFunctions[funcName]
if ok { if ok {
return flag return flag
@@ -142,45 +144,48 @@ func (p *hashicorpPlugin) has(funcName string) bool {
return false 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...) return p.FuncCaller.Call(funcName, args...)
} }
func (p *hashicorpPlugin) quit() error { func (p *hashicorpPlugin) Quit() error {
// kill hashicorp plugin process // kill hashicorp plugin process
pluginHost.Quit() pluginHost.Quit()
return nil return nil
} }
func (p *parser) initPlugin(path string) error { func Init(path string) (Plugin, error) {
if path == "" { if path == "" {
return nil return nil, nil
} }
var plugin Plugin
// priority: hashicorp plugin > go plugin > builtin functions // priority: hashicorp plugin > go plugin > builtin functions
// locate hashicorp plugin file // locate hashicorp plugin file
pluginPath, err := locatePlugin(path, hashicorpGoPluginFile) pluginPath, err := locateFile(path, hashicorpGoPluginFile)
if err == nil { if err == nil {
// found hashicorp go plugin file // found hashicorp go plugin file
p.plugin = &hashicorpPlugin{} plugin = &hashicorpPlugin{}
return p.plugin.init(pluginPath) err = plugin.Init(pluginPath)
return plugin, err
} }
// locate go plugin file // locate go plugin file
pluginPath, err = locatePlugin(path, goPluginFile) pluginPath, err = locateFile(path, goPluginFile)
if err == nil { if err == nil {
// found go plugin file // found go plugin file
p.plugin = &goPlugin{} plugin = &goPlugin{}
return p.plugin.init(pluginPath) err = plugin.Init(pluginPath)
return plugin, err
} }
// plugin not found // 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. // 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) stat, err := os.Stat(startPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return "", err return "", err
@@ -195,7 +200,7 @@ func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) {
startDir, _ = filepath.Abs(startDir) startDir, _ = filepath.Abs(startDir)
// convention over configuration // convention over configuration
pluginPath := filepath.Join(startDir, string(destPluginFile)) pluginPath := filepath.Join(startDir, string(destFile))
if _, err := os.Stat(pluginPath); err == nil { if _, err := os.Stat(pluginPath); err == nil {
return pluginPath, 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 "", fmt.Errorf("searched to system root dir, plugin file not found")
} }
return locatePlugin(parentDir, destPluginFile) return locateFile(parentDir, destFile)
}
// 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...)
} }

View File

@@ -8,6 +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"
pluginShared "github.com/httprunner/hrp/plugin/shared" 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 nil, fmt.Errorf("function %s not found", funcName)
} }
return pluginShared.CallFunc(fn, args...) return common.CallFunc(fn, args...)
} }
var functions = make(functionsMap) var functions = make(functionsMap)

View File

@@ -19,6 +19,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common"
) )
// Run starts to run API test with default configs. // Run starts to run API test with default configs.
@@ -157,7 +158,7 @@ func (r *caseRunner) reset() *caseRunner {
func (r *caseRunner) run() error { func (r *caseRunner) run() error {
defer func() { defer func() {
if r.parser.plugin != nil { if r.parser.plugin != nil {
r.parser.plugin.quit() r.parser.plugin.Quit()
} }
}() }()
config := r.TestCase.Config config := r.TestCase.Config
@@ -504,7 +505,8 @@ func (r *caseRunner) parseConfig(config IConfig) error {
cfg := config.ToStruct() cfg := config.ToStruct()
// init plugin // init plugin
err := r.parser.initPlugin(cfg.Path) var err error
r.parser.plugin, err = common.Init(cfg.Path)
if err != nil { if err != nil {
return err return err
} }