Merge pull request #59 from httprunner/go-plugin

refactor go plugin

- feat: add github.com/httprunner/hrp/plugin sub-module
- feat: add scaffold for plugin
- feat: catch Interrupt and SIGTERM signals to ensure plugin quitted
- change: report event for initializing plugin
- doc: add plugin docs
- refactor: plugin structure
This commit is contained in:
debugtalk
2022-01-19 15:07:14 +08:00
committed by GitHub
28 changed files with 650 additions and 147 deletions

View File

@@ -16,7 +16,7 @@
- [x] Testcases can be described in multiple formats, `YAML`/`JSON`/`Golang`, and they are interchangeable.
- [x] With [`HAR`][HAR] support, you can use Charles/Fiddler/Chrome/etc as a script recording generator.
- [x] Supports `variables`/`extract`/`validate`/`hooks` mechanisms to create extremely complex test scenarios.
- [ ] Built-in integration of rich functions, and you can also use [`go plugin`][plugin] to create and call custom functions.
- [x] Built-in integration of rich functions, and you can also use [hashicorp/plugin] or [go plugin] to create and call custom functions.
- [x] Inherit all powerful features of [`Boomer`][Boomer] and [`locust`][locust], you can run `load test` without extra work.
- [x] Using it as a `CLI tool` or a `library` are both supported.
@@ -272,7 +272,8 @@ func TestCaseDemo(t *testing.T) {
[jmespath]: https://jmespath.org/
[allure]: https://docs.qameta.io/allure/
[HAR]: http://httparchive.org/
[plugin]: https://pkg.go.dev/plugin
[hashicorp/plugin]: https://github.com/hashicorp/go-plugin
[go plugin]: https://pkg.go.dev/plugin
[demo.json]: https://github.com/httprunner/hrp/blob/main/examples/demo.json
[examples]: https://github.com/httprunner/hrp/blob/main/examples/
[CHANGELOG]: docs/CHANGELOG.md

View File

@@ -1,6 +1,7 @@
package hrp
import (
"sync"
"time"
"github.com/jinzhu/copier"
@@ -8,19 +9,23 @@ import (
"github.com/httprunner/hrp/internal/boomer"
"github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common"
)
func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
b := &HRPBoomer{
Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate),
debug: false,
Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate),
pluginsMutex: new(sync.RWMutex),
debug: false,
}
return b
}
type HRPBoomer struct {
*boomer.Boomer
debug bool
plugins []common.Plugin // each task has its own plugin process
pluginsMutex *sync.RWMutex // avoid data race
debug bool
}
// SetDebug configures whether to log HTTP request and response content.
@@ -57,14 +62,34 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
b.Boomer.Run(taskSlice...)
}
func (b *HRPBoomer) Quit() {
b.pluginsMutex.Lock()
plugins := b.plugins
b.pluginsMutex.Unlock()
for _, plugin := range plugins {
plugin.Quit()
}
b.Boomer.Quit()
}
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
hrpRunner := NewRunner(nil).SetDebug(b.debug)
config := testcase.Config.ToStruct()
// each testcase has its own plugin process
plugin, _ := initPlugin(config.Path)
if plugin != nil {
b.pluginsMutex.Lock()
b.plugins = append(b.plugins, plugin)
b.pluginsMutex.Unlock()
}
return &boomer.Task{
Name: config.Name,
Weight: config.Weight,
Fn: func() {
runner := hrpRunner.newCaseRunner(testcase)
runner.parser.plugin = plugin
testcaseSuccess := true // flag whole testcase result
var transactionSuccess = true // flag current transaction result
@@ -74,6 +99,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
// copy config to avoid data racing
if err := copier.Copy(caseConfig, cfg); err != nil {
log.Error().Err(err).Msg("copy config data failed")
return
}
// iterate through all parameter iterators and update case variables
for _, it := range caseConfig.ParametersSetting.Iterators {
@@ -81,6 +107,13 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
caseConfig.Variables = mergeVariables(it.Next(), caseConfig.Variables)
}
}
config := runner.TestCase.Config
if err := runner.parseConfig(config); err != nil {
log.Error().Err(err).Msg("parse config failed")
return
}
startTime := time.Now()
for index, step := range testcase.TestSteps {
stepData, err := runner.runStep(index, caseConfig)

View File

@@ -6,6 +6,9 @@ import (
)
func TestBoomerStandaloneRun(t *testing.T) {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
testcase1 := &TestCase{
Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"),
TestSteps: []IStep{

View File

@@ -1,8 +1,11 @@
# Release History
## v0.5.2 (2022-01-16)
## v0.5.2 (2022-01-19)
- feat: support creating and calling custom functions with [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin)
- feat: add scaffold demo with hashicorp plugin
- feat: report events for initializing plugin
- fix: log failures when the assertion failed
## v0.5.1 (2022-01-13)

View File

@@ -33,4 +33,4 @@ Copyright 2021 debugtalk
* [hrp run](hrp_run.md) - run API test
* [hrp startproject](hrp_startproject.md) - create a scaffold project
###### Auto generated by spf13/cobra on 17-Jan-2022
###### Auto generated by spf13/cobra on 18-Jan-2022

View File

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

View File

@@ -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 17-Jan-2022
###### Auto generated by spf13/cobra on 18-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 17-Jan-2022
###### Auto generated by spf13/cobra on 18-Jan-2022

View File

@@ -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 17-Jan-2022
###### Auto generated by spf13/cobra on 18-Jan-2022

View File

@@ -3,9 +3,9 @@
"name": "demo with complex mechanisms",
"base_url": "https://postman-echo.com",
"variables": {
"a": 12.3,
"a": "${sum(10, 2.3)}",
"b": 3.45,
"n": 5,
"n": "${sum_ints(1, 2, 2)}",
"varFoo1": "${gen_random_string($n)}",
"varFoo2": "${max($a, $b)}"
}

View File

@@ -2,9 +2,9 @@ config:
name: demo with complex mechanisms
base_url: https://postman-echo.com
variables:
a: 12.3
a: ${sum(10, 2.3)}
b: 3.45
"n": 5
"n": ${sum_ints(1, 2, 2)}
varFoo1: ${gen_random_string($n)}
varFoo2: ${max($a, $b)}
teststeps:

5
go.mod
View File

@@ -6,8 +6,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/getsentry/sentry-go v0.11.0
github.com/google/uuid v1.3.0
github.com/hashicorp/go-hclog v1.1.0
github.com/hashicorp/go-plugin v1.4.3
github.com/httprunner/hrp/plugin v0.0.0-00010101000000-000000000000
github.com/jinzhu/copier v0.3.2
github.com/jmespath/go-jmespath v0.4.0
github.com/maja42/goval v1.2.1
@@ -21,3 +20,5 @@ require (
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
replace github.com/httprunner/hrp/plugin => ./plugin

View File

@@ -66,7 +66,7 @@ func (s *requestStats) logTransaction(name string, success bool, responseTime in
s.transactionPassed++
} else {
s.transactionFailed++
s.get(name, "transaction").logError("")
s.get(name, "transaction").logFailures()
}
s.get(name, "transaction").log(responseTime, contentLength)
}
@@ -77,8 +77,8 @@ func (s *requestStats) logRequest(method, name string, responseTime int64, conte
}
func (s *requestStats) logError(method, name, err string) {
s.total.logError(err)
s.get(name, method).logError(err)
s.total.logFailures()
s.get(name, method).logFailures()
// store error in errors map
key := genMD5(method, name, err)
@@ -264,7 +264,7 @@ func (s *statsEntry) logResponseTime(responseTime int64) {
}
}
func (s *statsEntry) logError(err string) {
func (s *statsEntry) logFailures() {
s.NumFailures++
key := time.Now().Unix()
_, ok := s.NumFailPerSec[key]

View File

@@ -6,8 +6,8 @@ var demoTestCase = &hrp.TestCase{
Config: hrp.NewConfig("demo with complex mechanisms").
SetBaseURL("https://postman-echo.com").
WithVariables(map[string]interface{}{ // global level variables
"n": 5,
"a": 12.3,
"n": "${sum_ints(1, 2, 2)}",
"a": "${sum(10, 2.3)}",
"b": 3.45,
"varFoo1": "${gen_random_string($n)}",
"varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function
@@ -56,6 +56,50 @@ var demoTestCase = &hrp.TestCase{
},
}
// debugtalk.go
var demoPlugin = `package main
import (
"fmt"
"github.com/httprunner/hrp/plugin"
)
func SumTwoInt(a, b int) int {
return a + b
}
func SumInts(args ...int) int {
var sum int
for _, arg := range args {
sum += arg
}
return sum
}
func Sum(args ...interface{}) (interface{}, error) {
var sum float64
for _, arg := range args {
switch v := arg.(type) {
case int:
sum += float64(v)
case float64:
sum += v
default:
return nil, fmt.Errorf("unexpected type: %T", arg)
}
}
return sum, nil
}
func main() {
plugin.Register("sum_ints", SumInts)
plugin.Register("sum_two_int", SumTwoInt)
plugin.Register("sum", Sum)
plugin.Serve()
}
`
// .gitignore
var demoIgnoreContent = `.env
reports/*
@@ -64,9 +108,13 @@ reports/*
.idea/
.DS_Store
output/
# plugin
debugtalk.bin
debugtalk.so
`
// .env
var demoEnvContent = `USERNAME=debugtalk
"PASSWORD=123456
PASSWORD=123456
`

View File

@@ -2,8 +2,12 @@ package scaffold
import (
"fmt"
"os"
"os/exec"
"testing"
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp"
)
@@ -12,6 +16,21 @@ var (
demoTestCaseYAMLPath = "../../examples/demo.yaml"
)
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 TestGenDemoTestCase(t *testing.T) {
tCase, _ := demoTestCase.ToTCase()
err := tCase.Dump2JSON(demoTestCaseJSONPath)
@@ -25,6 +44,10 @@ func TestGenDemoTestCase(t *testing.T) {
}
func Example_demo() {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
demoTestCase.Config.ToStruct().Path = "../../examples/debugtalk.bin"
err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase)
fmt.Println(err)
// Output:
@@ -32,6 +55,9 @@ func Example_demo() {
}
func Example_jsonDemo() {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
testCase := &hrp.TestCasePath{Path: demoTestCaseJSONPath}
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
fmt.Println(err)
@@ -40,6 +66,9 @@ func Example_jsonDemo() {
}
func Example_yamlDemo() {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
testCase := &hrp.TestCasePath{Path: demoTestCaseYAMLPath}
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
fmt.Println(err)

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"github.com/httprunner/hrp/internal/ga"
@@ -36,6 +37,9 @@ func CreateScaffold(projectName string) error {
if err := createFolder(path.Join(projectName, "testcases")); err != nil {
return err
}
if err := createFolder(path.Join(projectName, "plugin")); err != nil {
return err
}
if err := createFolder(path.Join(projectName, "reports")); err != nil {
return err
}
@@ -53,6 +57,18 @@ func CreateScaffold(projectName string) error {
return err
}
// create debugtalk.go
pluginFile := path.Join(projectName, "plugin", "debugtalk.go")
if err := createFile(pluginFile, demoPlugin); err != nil {
return err
}
cmd := exec.Command("go", "build",
"-o", path.Join(projectName, "debugtalk.bin"),
pluginFile)
if err := cmd.Run(); err != nil {
log.Error().Err(err).Msg("build plugin debugtalk.bin failed")
}
// create .gitignore
if err := createFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil {
return err

View File

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

190
plugin/README.md Normal file
View File

@@ -0,0 +1,190 @@
# plugin
When you need to do some dynamic calculations or custom logic processing in testcases, you need to use the plugin function mechanism.
HttpRunner+ supports both [hashicorp/plugin] and [go plugin] to create and call custom functions.
## hashicorp/plugin
It is recommended to use [hashicorp/plugin] in most cases.
### create plugin functions
Firstly, you need to define your plugin functions. The functions can be very flexible, only the following restrictions should be complied with.
- function should return at most one value and one error.
- `Register()` and `Serve()` must be called to register plugin functions and start a plugin server process in `main()`.
Here is some plugin functions as example.
```go
package main
import (
"fmt"
"github.com/httprunner/hrp/plugin"
)
func SumTwoInt(a, b int) int {
return a + b
}
func SumInts(args ...int) int {
var sum int
for _, arg := range args {
sum += arg
}
return sum
}
func Sum(args ...interface{}) (interface{}, error) {
var sum float64
for _, arg := range args {
switch v := arg.(type) {
case int:
sum += float64(v)
case float64:
sum += v
default:
return nil, fmt.Errorf("unexpected type: %T", arg)
}
}
return sum, nil
}
func main() {
plugin.Register("sum_ints", SumInts)
plugin.Register("sum_two_int", SumTwoInt)
plugin.Register("sum", Sum)
plugin.Serve()
}
```
You can get more examples at [examples/plugin/]
### build plugin
Secondly, you can build your hashicorp plugin to binary file `debugtalk.bin`. The name of `debugtalk.bin` is by convention and should not be changed.
```bash
$ go build -o examples/debugtalk.bin examples/plugin/hashicorp.go examples/plugin/debugtalk.go
```
It is recommended to place the `debugtalk.bin` file in your project root folder, or you can put it in the parent folder of the target testcase file. HttpRunner+ will search `debugtalk.bin` upward recursively until current working directory or system root dir.
### use plugin functions
Then, you can call your defined plugin function in your `YAML/JSON` testcase at any position.
```json
{
"name": "get with params",
"variables": {
"a": "${sum_two_int(1,6)}",
"b": "${sum_ints(1,2,3)}",
"c": "${sum(1, 2.3, 4)}",
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$c",
"foo2": "${max($a, $b)}"
},
"headers": {
"User-Agent": "HttpRunnerPlus"
}
}
}
```
## go plugin
The golang official plugin is only supported on Linux, FreeBSD, and macOS. And this solution also has many drawbacks.
### create plugin functions
Firstly, you need to define your plugin functions. The functions can be very flexible, only the following restrictions should be complied with.
- plugin package name must be `main`.
- function names must be capitalized.
- function should return at most one value and one error.
Here is some plugin functions as example.
```go
package main
func SumTwoInt(a, b int) int {
return a + b
}
func SumInts(args ...int) int {
var sum int
for _, arg := range args {
sum += arg
}
return sum
}
func Sum(args ...interface{}) (interface{}, error) {
var sum float64
for _, arg := range args {
switch v := arg.(type) {
case int:
sum += float64(v)
case float64:
sum += v
default:
return nil, fmt.Errorf("unexpected type: %T", arg)
}
}
return sum, nil
}
```
You can get more examples at [examples/plugin/debugtalk.go]
### build plugin
Then you can build your go plugin with `-buildmode=plugin` flag to binary file `debugtalk.so`. The name of `debugtalk.so` is by convention and should not be changed.
```bash
$ go build -buildmode=plugin -o=examples/debugtalk.so examples/plugin/debugtalk.go
```
It is recommended to place the `debugtalk.so` file in your project root folder, or you can put it in the parent folder of the target testcase file. HttpRunner+ will search `debugtalk.so` upward recursively until current working directory or system root dir.
### use plugin functions
Then, you can call your defined plugin function in your `YAML/JSON` testcase at any position.
```json
{
"name": "get with params",
"variables": {
"a": "${SumTwoInt(1,6)}",
"b": "${SumInts(1,2,3)}",
"c": "${Sum(1, 2.3, 4)}",
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$c",
"foo2": "${max($a, $b)}"
},
"headers": {
"User-Agent": "HttpRunnerPlus"
}
}
}
```
Notice: you should use the original function name.
[hashicorp/plugin]: https://github.com/hashicorp/go-plugin
[go plugin]: https://pkg.go.dev/plugin
[examples/plugin/]: ../examples/plugin/
[examples/plugin/debugtalk.go]: ../examples/plugin/debugtalk.go

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
// +build linux freebsd darwin
// go plugin doesn't support windows
package hrp
package common
import (
"fmt"
@@ -16,7 +16,7 @@ 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=examples/debugtalk.so", "examples/plugin/debugtalk.go")
"-o=debugtalk.so", "../../examples/plugin/debugtalk.go")
if err := cmd.Run(); err != nil {
panic(err)
}
@@ -24,50 +24,43 @@ func buildGoPlugin() {
func removeGoPlugin() {
fmt.Println("[teardown] remove go plugin")
os.Remove("examples/debugtalk.so")
os.Remove("debugtalk.so")
}
func TestLocatePlugin(t *testing.T) {
buildGoPlugin()
defer removeGoPlugin()
cwd, _ := os.Getwd()
_, err := locatePlugin(cwd, goPluginFile)
_, err := locateFile("../", goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
_, err = locatePlugin("", goPluginFile)
_, err = locateFile("", goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
startPath := "examples/debugtalk.so"
_, err = locatePlugin(startPath, goPluginFile)
startPath := "debugtalk.so"
_, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/demo.json"
_, err = locatePlugin(startPath, goPluginFile)
startPath = "call.go"
_, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/"
_, err = locatePlugin(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "examples/plugin/debugtalk.go"
_, err = locatePlugin(startPath, goPluginFile)
startPath = "."
_, err = locateFile(startPath, goPluginFile)
if !assert.Nil(t, err) {
t.Fail()
}
startPath = "/abc"
_, err = locatePlugin(startPath, goPluginFile)
_, err = locateFile(startPath, goPluginFile)
if !assert.Error(t, err) {
t.Fail()
}
@@ -75,17 +68,19 @@ func TestLocatePlugin(t *testing.T) {
func TestCallPluginFunction(t *testing.T) {
buildGoPlugin()
removeHashicorpPlugin()
defer removeGoPlugin()
parser := newParser()
err := parser.initPlugin("examples/debugtalk.so")
plugin, err := Init("debugtalk.so")
if err != nil {
t.Fatal(err)
}
if !assert.True(t, plugin.Has("Concatenate")) {
t.Fail()
}
// 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) {
t.Fail()
}

View File

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

View File

@@ -1,4 +1,4 @@
package hrp
package common
import (
"fmt"
@@ -10,8 +10,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga"
pluginHost "github.com/httprunner/hrp/plugin/host"
pluginShared "github.com/httprunner/hrp/plugin/shared"
)
@@ -24,38 +22,26 @@ const (
hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py"
)
type hrpPlugin 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
type Plugin 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
}
// goPlugin implements golang official plugin
type goPlugin struct {
// 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 {
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
// report event for loading go plugin
defer func() {
event := ga.EventTracking{
Category: "LoadGoPlugin",
Action: "plugin.Open",
}
if err != nil {
event.Value = 1 // failed
}
go ga.SendEvent(event)
}()
p.Plugin, err = plugin.Open(path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("load go plugin failed")
@@ -67,7 +53,7 @@ func (p *goPlugin) init(path string) error {
return nil
}
func (p *goPlugin) has(funcName string) bool {
func (p *GoPlugin) Has(funcName string) bool {
fn, ok := p.cachedFunctions[funcName]
if ok {
return fn.IsValid()
@@ -90,23 +76,26 @@ func (p *goPlugin) has(funcName string) bool {
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]
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
return nil
}
// hashicorpPlugin implements hashicorp/go-plugin
type hashicorpPlugin struct {
// HashicorpPlugin implements hashicorp/go-plugin
type HashicorpPlugin struct {
pluginShared.FuncCaller
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)
if err != nil {
@@ -120,7 +109,7 @@ func (p *hashicorpPlugin) init(path string) error {
return nil
}
func (p *hashicorpPlugin) has(funcName string) bool {
func (p *HashicorpPlugin) Has(funcName string) bool {
flag, ok := p.cachedFunctions[funcName]
if ok {
return flag
@@ -142,45 +131,49 @@ func (p *hashicorpPlugin) has(funcName string) bool {
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...)
}
func (p *hashicorpPlugin) quit() error {
func (p *HashicorpPlugin) Quit() error {
// kill hashicorp plugin process
log.Warn().Msg("quit hashicorp plugin process")
pluginHost.Quit()
return nil
}
func (p *parser) initPlugin(path string) error {
func Init(path string) (Plugin, error) {
if path == "" {
return nil
return nil, nil
}
var plugin Plugin
// priority: hashicorp plugin > go plugin > builtin functions
// priority: hashicorp plugin > go plugin
// locate hashicorp plugin file
pluginPath, err := locatePlugin(path, hashicorpGoPluginFile)
pluginPath, err := locateFile(path, hashicorpGoPluginFile)
if err == nil {
// found hashicorp go plugin file
p.plugin = &hashicorpPlugin{}
return p.plugin.init(pluginPath)
plugin = &HashicorpPlugin{}
err = plugin.Init(pluginPath)
return plugin, err
}
// locate go plugin file
pluginPath, err = locatePlugin(path, goPluginFile)
pluginPath, err = locateFile(path, goPluginFile)
if err == nil {
// found go plugin file
p.plugin = &goPlugin{}
return p.plugin.init(pluginPath)
plugin = &GoPlugin{}
err = plugin.Init(pluginPath)
return plugin, err
}
// 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.
func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) {
func locateFile(startPath string, destFile pluginFile) (string, error) {
stat, err := os.Stat(startPath)
if os.IsNotExist(err) {
return "", err
@@ -195,7 +188,7 @@ func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) {
startDir, _ = filepath.Abs(startDir)
// convention over configuration
pluginPath := filepath.Join(startDir, string(destPluginFile))
pluginPath := filepath.Join(startDir, string(destFile))
if _, err := os.Stat(pluginPath); err == nil {
return pluginPath, nil
}
@@ -212,24 +205,5 @@ func locatePlugin(startPath string, destPluginFile pluginFile) (string, error) {
return "", fmt.Errorf("searched to system root dir, plugin file not found")
}
return locatePlugin(parentDir, destPluginFile)
}
// 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...)
return locateFile(parentDir, destFile)
}

10
plugin/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module github.com/httprunner/hrp/plugin
go 1.16
require (
github.com/hashicorp/go-hclog v1.1.0
github.com/hashicorp/go-plugin v1.4.3
github.com/rs/zerolog v1.26.1
github.com/stretchr/testify v1.7.0
)

118
plugin/go.sum Normal file
View File

@@ -0,0 +1,118 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw=
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -8,6 +8,7 @@ import (
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/httprunner/hrp/plugin/common"
pluginShared "github.com/httprunner/hrp/plugin/shared"
)
@@ -36,11 +37,13 @@ func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{}
return nil, fmt.Errorf("function %s not found", funcName)
}
return pluginShared.CallFunc(fn, args...)
return common.CallFunc(fn, args...)
}
var functions = make(functionsMap)
// Register registers a plugin function.
// Every plugin function must be registered before Serve() is called.
func Register(funcName string, fn interface{}) {
if _, ok := functions[funcName]; ok {
return
@@ -48,6 +51,7 @@ func Register(funcName string, fn interface{}) {
functions[funcName] = reflect.ValueOf(fn)
}
// Serve starts a plugin server process.
func Serve() {
funcPlugin := &functionPlugin{
logger: hclog.New(&hclog.LoggerOptions{

View File

@@ -9,8 +9,11 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"testing"
"time"
@@ -19,6 +22,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common"
)
// Run starts to run API test with default configs.
@@ -155,12 +159,19 @@ func (r *caseRunner) reset() *caseRunner {
}
func (r *caseRunner) run() error {
config := r.TestCase.Config
// init plugin
var err error
if r.parser.plugin, err = initPlugin(config.ToStruct().Path); err != nil {
return err
}
defer func() {
if r.parser.plugin != nil {
r.parser.plugin.quit()
r.parser.plugin.Quit()
}
}()
config := r.TestCase.Config
if err := r.parseConfig(config); err != nil {
return err
}
@@ -181,6 +192,40 @@ func (r *caseRunner) run() error {
return nil
}
func initPlugin(path string) (plugin common.Plugin, err error) {
plugin, err = common.Init(path)
if plugin == nil {
return
}
// catch Interrupt and SIGTERM signals to ensure plugin quitted
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
plugin.Quit()
os.Exit(0)
}()
// report event for initializing plugin
var pluginType string
if _, ok := plugin.(*common.GoPlugin); ok {
pluginType = "go"
} else {
pluginType = "hashicorp"
}
event := ga.EventTracking{
Category: "InitPlugin",
Action: fmt.Sprintf("Init %s plugin", pluginType),
}
if err != nil {
event.Value = 1 // failed
}
go ga.SendEvent(event)
return
}
func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
step := r.TestCase.TestSteps[index]
@@ -503,12 +548,6 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
func (r *caseRunner) parseConfig(config IConfig) error {
cfg := config.ToStruct()
// init plugin
err := r.parser.initPlugin(cfg.Path)
if err != nil {
return err
}
// parse config variables
parsedVariables, err := r.parser.parseVariables(cfg.Variables)
if err != nil {

View File

@@ -1,10 +1,32 @@
package hrp
import (
"os"
"os/exec"
"testing"
"github.com/rs/zerolog/log"
)
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 TestHttpRunner(t *testing.T) {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetBaseURL("http://httpbin.org"),