refactor: go plugin

This commit is contained in:
debugtalk
2022-01-13 14:30:07 +08:00
parent b58dbaa9db
commit 1c7f2f9834
9 changed files with 166 additions and 100 deletions

View File

@@ -29,8 +29,8 @@ Copyright 2021 debugtalk
### SEE ALSO
* [hrp boom](hrp_boom.md) - run load test with boomer
* [hrp har2case](hrp_har2case.md) - Convert HAR to json/yaml testcase files
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
* [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 8-Jan-2022
###### Auto generated by spf13/cobra on 13-Jan-2022

View File

@@ -38,4 +38,4 @@ hrp boom [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 8-Jan-2022
###### Auto generated by spf13/cobra on 13-Jan-2022

View File

@@ -1,10 +1,10 @@
## hrp har2case
Convert HAR to json/yaml testcase files
convert HAR to json/yaml testcase files
### Synopsis
Convert HAR to json/yaml testcase files
convert HAR to json/yaml testcase files
```
hrp har2case $har_path... [flags]
@@ -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 8-Jan-2022
###### Auto generated by spf13/cobra on 13-Jan-2022

View File

@@ -31,4 +31,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 8-Jan-2022
###### Auto generated by spf13/cobra on 13-Jan-2022

View File

@@ -1,6 +1,6 @@
## hrp startproject
Create a scaffold project
create a scaffold project
```
hrp startproject $project_name [flags]
@@ -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 8-Jan-2022
###### Auto generated by spf13/cobra on 13-Jan-2022

View File

@@ -15,7 +15,7 @@ import (
func TestMain(m *testing.M) {
fmt.Println("[TestMain] 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")
cmd := exec.Command("go", "build", "-buildmode=plugin", "-race", "-o=examples/debugtalk.so", "examples/plugin/debugtalk.go")
if err := cmd.Run(); err != nil {
panic(err)
}
@@ -24,42 +24,42 @@ func TestMain(m *testing.M) {
func TestLocatePlugin(t *testing.T) {
cwd, _ := os.Getwd()
_, err := locatePlugin(cwd)
_, err := locatePlugin(cwd, goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
_, err = locatePlugin("")
_, err = locatePlugin("", goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
startPath := "examples/debugtalk.so"
_, err = locatePlugin(startPath)
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/demo.json"
_, err = locatePlugin(startPath)
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/"
_, err = locatePlugin(startPath)
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/plugin/debugtalk.go"
_, err = locatePlugin(startPath)
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "/abc"
_, err = locatePlugin(startPath)
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
@@ -67,7 +67,7 @@ func TestLocatePlugin(t *testing.T) {
func TestCallPluginFunction(t *testing.T) {
parser := newParser()
parser.loadPlugin("examples/debugtalk.so")
parser.initPlugin("examples/debugtalk.so")
// call function without arguments
result, err := parser.callFunc("Concatenate", 1, "2", 3.14)

View File

@@ -4,9 +4,6 @@ import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"plugin"
"reflect"
"regexp"
"strings"
@@ -21,46 +18,7 @@ func newParser() *parser {
}
type parser struct {
// pluginLoader stores loaded go plugins.
pluginLoader *plugin.Plugin
}
// locatePlugin searches debugtalk.so upward recursively until current
// working directory or system root dir.
func locatePlugin(startPath string) (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
// target plugin file name is always debugtalk.so
pluginPath := filepath.Join(startDir, "debugtalk.so")
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 locatePlugin(parentDir)
plugin hrpPlugin // plugin is used to call functions
}
func buildURL(baseURL, stepURL string) string {

182
plugin.go
View File

@@ -2,6 +2,8 @@ package hrp
import (
"fmt"
"os"
"path/filepath"
"plugin"
"reflect"
"runtime"
@@ -12,28 +14,33 @@ import (
"github.com/httprunner/hrp/internal/ga"
)
func (p *parser) loadPlugin(path string) error {
type pluginFile string
const (
goPluginFile pluginFile = "debugtalk.so" // built from go plugin
hashicorpGoPluginFile pluginFile = "debugtalk" // built from hashicorp go plugin
hashicorpPyPluginFile pluginFile = "debugtalk.py"
)
type hrpPlugin interface {
init(path string) error
lookup(funcName string) (reflect.Value, error) // lookup function
// call(funcName string, args ...interface{}) (interface{}, error)
quit() error
}
// goPlugin implements golang official plugin
type goPlugin struct {
*plugin.Plugin
}
func (p *goPlugin) init(path string) error {
if runtime.GOOS == "windows" {
log.Warn().Msg("go plugin does not support windows")
return nil
}
if path == "" {
return nil
}
// check if loaded before
if p.pluginLoader != nil {
return nil
}
// locate plugin file
pluginPath, err := locatePlugin(path)
if err != nil {
// plugin not found
return nil
return fmt.Errorf("go plugin does not support windows")
}
var err error
// report event for loading go plugin
defer func() {
event := ga.EventTracking{
@@ -46,36 +53,137 @@ func (p *parser) loadPlugin(path string) error {
go ga.SendEvent(event)
}()
// load plugin
plugins, err := plugin.Open(pluginPath)
p.Plugin, err = plugin.Open(path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("load go plugin failed")
return err
}
p.pluginLoader = plugins
log.Info().Str("path", path).Msg("load go plugin success")
return nil
}
func getMappingFunction(funcName string, pluginLoader *plugin.Plugin) (reflect.Value, error) {
func (p *goPlugin) lookup(funcName string) (reflect.Value, error) {
if p.Plugin == nil {
return reflect.Value{}, fmt.Errorf("go plugin is not loaded")
}
sym, err := p.Plugin.Lookup(funcName)
if err != nil {
return reflect.Value{}, fmt.Errorf("function %s is not found", funcName)
}
fn := reflect.ValueOf(sym)
// check function type
if fn.Kind() != reflect.Func {
return reflect.Value{}, fmt.Errorf("function %s is invalid", funcName)
}
return fn, nil
}
func (p *goPlugin) call(funcName string, args ...interface{}) (interface{}, error) {
if p.Plugin == nil {
return nil, fmt.Errorf("go plugin is not loaded")
}
return nil, nil
}
func (p *goPlugin) quit() error {
return nil
}
// hashicorpPlugin implements hashicorp/go-plugin
type hashicorpPlugin struct {
cachedFunctions map[string]reflect.Value
}
func (p *hashicorpPlugin) init(path string) error {
log.Info().Str("path", path).Msg("load hashicorp go plugin success")
return nil
}
func (p *hashicorpPlugin) lookup(funcName string) (reflect.Value, error) {
return reflect.Value{}, nil
}
func (p *hashicorpPlugin) call(funcName string, args ...interface{}) (interface{}, error) {
return nil, nil
}
func (p *hashicorpPlugin) quit() error {
return nil
}
func (p *parser) initPlugin(path string) error {
if path == "" {
return nil
}
// locate go plugin file
pluginPath, err := locatePlugin(path, goPluginFile)
if err == nil {
// found go plugin file
p.plugin = &goPlugin{}
return p.plugin.init(pluginPath)
}
// locate hashicorp plugin file
pluginPath, err = locatePlugin(path, hashicorpGoPluginFile)
if err == nil {
// found hashicorp go plugin file
p.plugin = &hashicorpPlugin{}
return p.plugin.init(pluginPath)
}
// plugin not found
return nil
}
// locatePlugin searches destPluginFile upward recursively until current
// working directory or system root dir.
func locatePlugin(startPath string, destPluginFile 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(destPluginFile))
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 locatePlugin(parentDir, destPluginFile)
}
func (p *parser) getMappingFunction(funcName string) (reflect.Value, error) {
var fn reflect.Value
var err error
defer func() {
// check function type
if err == nil && fn.Kind() != reflect.Func {
// function not valid
err = fmt.Errorf("function %s is invalid", funcName)
return
}
}()
// get function from plugin loader
if pluginLoader != nil {
sym, err := pluginLoader.Lookup(funcName)
// get function from plugin
if p.plugin != nil {
fn, err := p.plugin.lookup(funcName)
if err == nil {
fn = reflect.ValueOf(sym)
return fn, nil
}
}
@@ -93,7 +201,7 @@ func getMappingFunction(funcName string, pluginLoader *plugin.Plugin) (reflect.V
// callFunc calls function with arguments
// only support return at most one result value
func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) {
fn, err := getMappingFunction(funcName, p.pluginLoader)
fn, err := p.getMappingFunction(funcName)
if err != nil {
return nil, err
}

View File

@@ -506,7 +506,7 @@ func (r *caseRunner) parseConfig(config IConfig) error {
cfg.Variables = parsedVariables
// load plugin variables and functions
err = r.parser.loadPlugin(cfg.Path)
err = r.parser.initPlugin(cfg.Path)
if err != nil {
return err
}