mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
refactor: go plugin
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
44
parser.go
44
parser.go
@@ -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
182
plugin.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user