mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-17 16:57:35 +08:00
fix: unittest
This commit is contained in:
103
plugin/inner/call.go
Normal file
103
plugin/inner/call.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package pluginInternal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CallFunc calls function with arguments
|
||||
func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) {
|
||||
argumentsValue, err := convertArgs(fn, args...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert arguments failed")
|
||||
return nil, err
|
||||
}
|
||||
return call(fn, argumentsValue)
|
||||
}
|
||||
|
||||
func convertArgs(fn reflect.Value, args ...interface{}) ([]reflect.Value, error) {
|
||||
fnArgsNum := fn.Type().NumIn()
|
||||
|
||||
// function arguments should match exactly if function's last argument is not slice
|
||||
if len(args) != fnArgsNum && (fnArgsNum == 0 || fn.Type().In(fnArgsNum-1).Kind() != reflect.Slice) {
|
||||
return nil, fmt.Errorf("function expect %d arguments, but got %d", fnArgsNum, len(args))
|
||||
}
|
||||
|
||||
argumentsValue := make([]reflect.Value, len(args))
|
||||
for index := 0; index < len(args); index++ {
|
||||
argument := args[index]
|
||||
if argument == nil {
|
||||
argumentsValue[index] = reflect.Zero(fn.Type().In(index))
|
||||
continue
|
||||
}
|
||||
|
||||
argumentValue := reflect.ValueOf(argument)
|
||||
actualArgumentType := reflect.TypeOf(argument)
|
||||
|
||||
var expectArgumentType reflect.Type
|
||||
if (index == fnArgsNum-1 && fn.Type().In(fnArgsNum-1).Kind() == reflect.Slice) || index > fnArgsNum-1 {
|
||||
// last fn argument is slice
|
||||
expectArgumentType = fn.Type().In(fnArgsNum - 1).Elem() // slice element type
|
||||
|
||||
// last argument is also slice, e.g. []int
|
||||
if actualArgumentType.Kind() == reflect.Slice {
|
||||
if actualArgumentType.Elem() != expectArgumentType {
|
||||
err := fmt.Errorf("function argument %d's slice element type is not match, expect %v, actual %v",
|
||||
index, expectArgumentType, actualArgumentType)
|
||||
return nil, err
|
||||
}
|
||||
argumentsValue[index] = argumentValue
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
expectArgumentType = fn.Type().In(index)
|
||||
}
|
||||
|
||||
// type match
|
||||
if expectArgumentType == actualArgumentType {
|
||||
argumentsValue[index] = argumentValue
|
||||
continue
|
||||
}
|
||||
|
||||
// type not match, check if convertible
|
||||
if !actualArgumentType.ConvertibleTo(expectArgumentType) {
|
||||
// function argument type not match and not convertible
|
||||
err := fmt.Errorf("function argument %d's type is neither match nor convertible, expect %v, actual %v",
|
||||
index, expectArgumentType, actualArgumentType)
|
||||
return nil, err
|
||||
}
|
||||
// convert argument to expect type
|
||||
argumentsValue[index] = argumentValue.Convert(expectArgumentType)
|
||||
}
|
||||
return argumentsValue, nil
|
||||
}
|
||||
|
||||
func call(fn reflect.Value, args []reflect.Value) (interface{}, error) {
|
||||
resultValues := fn.Call(args)
|
||||
if resultValues == nil {
|
||||
// no returns
|
||||
return nil, nil
|
||||
} else if len(resultValues) == 2 {
|
||||
// return two arguments: interface{}, error
|
||||
if resultValues[1].Interface() != nil {
|
||||
return resultValues[0].Interface(), resultValues[1].Interface().(error)
|
||||
} else {
|
||||
return resultValues[0].Interface(), nil
|
||||
}
|
||||
} else if len(resultValues) == 1 {
|
||||
// return one argument
|
||||
if err, ok := resultValues[0].Interface().(error); ok {
|
||||
// return error
|
||||
return nil, err
|
||||
} else {
|
||||
// return interface{}
|
||||
return resultValues[0].Interface(), nil
|
||||
}
|
||||
} else {
|
||||
// return more than 2 arguments, unexpected
|
||||
err := fmt.Errorf("function should return at most 2 values")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
208
plugin/inner/call_test.go
Normal file
208
plugin/inner/call_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package pluginInternal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type data struct {
|
||||
f interface{}
|
||||
args []interface{}
|
||||
expVal interface{}
|
||||
expErr error
|
||||
}
|
||||
|
||||
func TestCallFuncBasic(t *testing.T) {
|
||||
params := []data{
|
||||
// zero argument, zero return
|
||||
{f: func() {}, args: []interface{}{}, expVal: nil, expErr: nil},
|
||||
// zero argument, return one value
|
||||
{f: func() int { return 1 }, args: []interface{}{}, expVal: 1, expErr: nil},
|
||||
{f: func() string { return "a" }, args: []interface{}{}, expVal: "a", expErr: nil},
|
||||
{f: func() interface{} { return 1.23 }, args: []interface{}{}, expVal: 1.23, expErr: nil},
|
||||
// zero argument, return error
|
||||
{f: func() error { return errors.New("xxx") }, args: []interface{}{}, expVal: nil, expErr: errors.New("xxx")},
|
||||
// zero argument, return one value and error
|
||||
{f: func() (int, error) { return 1, errors.New("xxx") }, args: []interface{}{}, expVal: 1, expErr: errors.New("xxx")},
|
||||
{f: func() (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{}, expVal: 1.23, expErr: errors.New("xxx")},
|
||||
|
||||
// one argument, zero return
|
||||
{f: func(n int) {}, args: []interface{}{1}, expVal: nil, expErr: nil},
|
||||
// one argument, return one value
|
||||
{f: func(n int) int { return n * n }, args: []interface{}{2}, expVal: 4},
|
||||
{f: func(c string) string { return c + c }, args: []interface{}{"p"}, expVal: "pp"},
|
||||
{f: func(arg interface{}) interface{} { return fmt.Sprintf("%v", arg) }, args: []interface{}{1.23}, expVal: "1.23"},
|
||||
// one argument, return one value and error
|
||||
{f: func(arg interface{}) (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{"a"}, expVal: 1.23, expErr: errors.New("xxx")},
|
||||
|
||||
// two arguments in same type
|
||||
{f: func(a, b int) int { return a * b }, args: []interface{}{2, 3}, expVal: 6},
|
||||
// two arguments in different type
|
||||
{
|
||||
f: func(n int, c string) string {
|
||||
var s string
|
||||
for i := 0; i < n; i++ {
|
||||
s += c
|
||||
}
|
||||
return s
|
||||
},
|
||||
args: []interface{}{3, "p"},
|
||||
expVal: "ppp",
|
||||
},
|
||||
|
||||
// variable arguments list: ...int, ...interface{}
|
||||
{
|
||||
f: func(n ...int) int {
|
||||
var sum int
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{1, 2, 3},
|
||||
expVal: 6,
|
||||
},
|
||||
{
|
||||
f: func(args ...interface{}) (interface{}, error) {
|
||||
var result string
|
||||
for _, arg := range args {
|
||||
result += fmt.Sprintf("%v", arg)
|
||||
}
|
||||
return result, nil
|
||||
},
|
||||
args: []interface{}{1, 2.3, "4.5", "p"},
|
||||
expVal: "12.34.5p",
|
||||
},
|
||||
{
|
||||
f: func(a, b int8, n ...int) int {
|
||||
var sum int
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
sum += int(a) + int(b)
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{1, 2, 3, 4.5},
|
||||
expVal: 10,
|
||||
},
|
||||
{
|
||||
f: func(a, b int8, n ...int) int {
|
||||
sum := int(a) + int(b)
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{1, 2},
|
||||
expVal: 3,
|
||||
},
|
||||
|
||||
{
|
||||
f: func(a []int, n ...int) int {
|
||||
var sum int
|
||||
for _, arg := range a {
|
||||
sum += arg
|
||||
}
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{[]int{1, 2}, 3, 4},
|
||||
expVal: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
fn := reflect.ValueOf(p.f)
|
||||
val, err := CallFunc(fn, p.args...)
|
||||
if !assert.Equal(t, p.expErr, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, p.expVal, val) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCallFuncComplex(t *testing.T) {
|
||||
params := []data{
|
||||
// arguments include slice
|
||||
{
|
||||
f: func(a int, n []int, b int) int {
|
||||
sum := a
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
sum += b
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{1, []int{2, 3}, 4},
|
||||
expVal: 10,
|
||||
},
|
||||
// last argument is slice
|
||||
{
|
||||
f: func(n []int) int {
|
||||
var sum int
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{[]int{1, 2, 3}},
|
||||
expVal: 6,
|
||||
},
|
||||
{
|
||||
f: func(a, b int, n []int) int {
|
||||
sum := a + b
|
||||
for _, arg := range n {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
},
|
||||
args: []interface{}{1, 2, []int{3, 4}},
|
||||
expVal: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
fn := reflect.ValueOf(p.f)
|
||||
val, err := CallFunc(fn, p.args...)
|
||||
if !assert.Equal(t, p.expErr, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, p.expVal, val) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCallFuncAbnormal(t *testing.T) {
|
||||
params := []data{
|
||||
// return more than 2 values
|
||||
{
|
||||
f: func() (int, int, error) { return 1, 2, nil },
|
||||
args: []interface{}{},
|
||||
expVal: nil,
|
||||
expErr: fmt.Errorf("function should return at most 2 values"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
fn := reflect.ValueOf(p.f)
|
||||
val, err := CallFunc(fn, p.args...)
|
||||
if !assert.Equal(t, p.expErr, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, p.expVal, val) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
15
plugin/inner/config.go
Normal file
15
plugin/inner/config.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package pluginInternal
|
||||
|
||||
import "github.com/hashicorp/go-plugin"
|
||||
|
||||
const PluginName = "debugtalk"
|
||||
|
||||
// handshakeConfigs are used to just do a basic handshake between
|
||||
// a plugin and host. If the handshake fails, a user friendly error is shown.
|
||||
// This prevents users from executing bad plugins or executing a plugin
|
||||
// directory. It is a UX feature, not a security feature.
|
||||
var HandshakeConfig = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 1,
|
||||
MagicCookieKey: "HttpRunnerPlus",
|
||||
MagicCookieValue: PluginName,
|
||||
}
|
||||
70
plugin/inner/go_plugin.go
Normal file
70
plugin/inner/go_plugin.go
Normal 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
|
||||
}
|
||||
90
plugin/inner/go_plugin_test.go
Normal file
90
plugin/inner/go_plugin_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// +build linux freebsd darwin
|
||||
// go plugin doesn't support windows
|
||||
|
||||
package pluginInternal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func buildGoPlugin() {
|
||||
fmt.Println("[setup] build go plugin")
|
||||
// flag -race is necessary in order to be consistent with go test
|
||||
cmd := exec.Command("go", "build", "-buildmode=plugin", "-race",
|
||||
"-o=debugtalk.so", "../../examples/plugin/debugtalk.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeGoPlugin() {
|
||||
fmt.Println("[teardown] remove go plugin")
|
||||
os.Remove("debugtalk.so")
|
||||
}
|
||||
|
||||
func TestLocatePlugin(t *testing.T) {
|
||||
buildGoPlugin()
|
||||
defer removeGoPlugin()
|
||||
|
||||
_, err := locateFile("../", goPluginFile)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
_, err = locateFile("", goPluginFile)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
startPath := "debugtalk.so"
|
||||
_, err = locateFile(startPath, goPluginFile)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
startPath = "call.go"
|
||||
_, err = locateFile(startPath, goPluginFile)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
startPath = "."
|
||||
_, err = locateFile(startPath, goPluginFile)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
startPath = "/abc"
|
||||
_, err = locateFile(startPath, goPluginFile)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallPluginFunction(t *testing.T) {
|
||||
buildGoPlugin()
|
||||
defer removeGoPlugin()
|
||||
|
||||
plugin, err := Init("debugtalk.so", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !assert.True(t, plugin.Has("Concatenate")) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// call function without arguments
|
||||
result, err := plugin.Call("Concatenate", "1", 2, "3.14")
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "123.14", result) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
95
plugin/inner/hashicorp_plugin.go
Normal file
95
plugin/inner/hashicorp_plugin.go
Normal 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
|
||||
}
|
||||
90
plugin/inner/hashicorp_plugin_test.go
Normal file
90
plugin/inner/hashicorp_plugin_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package pluginInternal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func buildHashicorpPlugin() {
|
||||
log.Info().Msg("[init] build hashicorp go plugin")
|
||||
cmd := exec.Command("go", "build",
|
||||
"-o", "../../examples/debugtalk.bin",
|
||||
"../../examples/plugin/hashicorp.go", "../../examples/plugin/debugtalk.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeHashicorpPlugin() {
|
||||
log.Info().Msg("[teardown] remove hashicorp plugin")
|
||||
os.Remove("../../examples/debugtalk.bin")
|
||||
}
|
||||
|
||||
func TestInitHashicorpPlugin(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
plugin, err := Init("../../examples/debugtalk.bin", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer plugin.Quit()
|
||||
|
||||
if !assert.True(t, plugin.Has("sum_ints")) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.True(t, plugin.Has("concatenate")) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var v2 interface{}
|
||||
v2, err = plugin.Call("sum_ints", 1, 2, 3, 4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, 10, v2) {
|
||||
t.Fail()
|
||||
}
|
||||
v2, err = plugin.Call("sum_two_int", 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, 3, v2) {
|
||||
t.Fail()
|
||||
}
|
||||
v2, err = plugin.Call("sum", 1, 2, 3.4, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, 11.4, v2) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
var v3 interface{}
|
||||
v3, err = plugin.Call("sum_two_string", "a", "b")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "ab", v3) {
|
||||
t.Fail()
|
||||
}
|
||||
v3, err = plugin.Call("sum_strings", "a", "b", "c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "abc", v3) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
v3, err = plugin.Call("concatenate", "a", 2, "c", 3.4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "a2c3.4", v3) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
110
plugin/inner/host.go
Normal file
110
plugin/inner/host.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package pluginInternal
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(new(funcData))
|
||||
}
|
||||
|
||||
// funcData is used to transfer between plugin and host via RPC.
|
||||
type funcData struct {
|
||||
Name string // function name
|
||||
Args []interface{} // function arguments
|
||||
}
|
||||
|
||||
// FuncCaller is the interface that we're exposing as a plugin.
|
||||
type FuncCaller interface {
|
||||
GetNames() ([]string, error) // get all plugin function names list
|
||||
Call(funcName string, args ...interface{}) (interface{}, error) // call plugin function
|
||||
}
|
||||
|
||||
// functionRPC runs on the host side.
|
||||
type functionRPC struct {
|
||||
client *rpc.Client
|
||||
}
|
||||
|
||||
func (g *functionRPC) GetNames() ([]string, error) {
|
||||
var resp []string
|
||||
err := g.client.Call("Plugin.GetNames", new(interface{}), &resp)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("rpc call GetNames() failed")
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// host -> plugin
|
||||
func (g *functionRPC) Call(funcName string, funcArgs ...interface{}) (interface{}, error) {
|
||||
log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via RPC")
|
||||
f := funcData{
|
||||
Name: funcName,
|
||||
Args: funcArgs,
|
||||
}
|
||||
|
||||
var args interface{} = f
|
||||
var resp interface{}
|
||||
err := g.client.Call("Plugin.Call", &args, &resp)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("funcName", funcName).Interface("funcArgs", funcArgs).
|
||||
Msg("rpc call Call() failed")
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// functionRPCServer runs on the plugin side, executing the user custom function.
|
||||
type functionRPCServer struct {
|
||||
Impl FuncCaller
|
||||
}
|
||||
|
||||
// plugin execution
|
||||
func (s *functionRPCServer) GetNames(args interface{}, resp *[]string) error {
|
||||
log.Info().Interface("args", args).Msg("GetNames called on plugin side")
|
||||
var err error
|
||||
*resp, err = s.Impl.GetNames()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("GetNames execution failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// plugin execution
|
||||
func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error {
|
||||
log.Info().Interface("args", args).Msg("function called on plugin side")
|
||||
f := args.(*funcData)
|
||||
var err error
|
||||
*resp, err = s.Impl.Call(f.Name, f.Args...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Interface("args", args).Msg("function execution failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HRPPlugin implements hashicorp's plugin.Plugin.
|
||||
type HRPPlugin struct {
|
||||
Impl FuncCaller
|
||||
}
|
||||
|
||||
func (p *HRPPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
|
||||
return &functionRPCServer{Impl: p.Impl}, nil
|
||||
}
|
||||
|
||||
func (HRPPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
|
||||
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
83
plugin/inner/init.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user