From 1c7f2f98343c592a745b34e64f86afc7379db4c5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 13 Jan 2022 14:30:07 +0800 Subject: [PATCH] refactor: go plugin --- docs/cmd/hrp.md | 6 +- docs/cmd/hrp_boom.md | 2 +- docs/cmd/hrp_har2case.md | 6 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_startproject.md | 4 +- plugin_test.go => go_plugin_test.go | 18 +-- parser.go | 44 +------ plugin.go | 182 ++++++++++++++++++++++------ runner.go | 2 +- 9 files changed, 166 insertions(+), 100 deletions(-) rename plugin_test.go => go_plugin_test.go (73%) diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 93352005..8ccee28e 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -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 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 36e4851c..f421c9d9 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -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 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index 79315570..6f57a509 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -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 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 20523b8d..9cce4aaf 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -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 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index c22b9f47..736d31d6 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -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 diff --git a/plugin_test.go b/go_plugin_test.go similarity index 73% rename from plugin_test.go rename to go_plugin_test.go index b811c6e3..a08dd0ce 100644 --- a/plugin_test.go +++ b/go_plugin_test.go @@ -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) diff --git a/parser.go b/parser.go index 49062ae2..b456f680 100644 --- a/parser.go +++ b/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 { diff --git a/plugin.go b/plugin.go index 7748e207..8bc29af9 100644 --- a/plugin.go +++ b/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 } diff --git a/runner.go b/runner.go index c59e1fec..4ba6eaa2 100644 --- a/runner.go +++ b/runner.go @@ -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 }