diff --git a/README.md b/README.md index 6ddd349c..4dcc78d1 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 00000000..b83e2c10 --- /dev/null +++ b/plugin/README.md @@ -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 diff --git a/plugin/plugin.go b/plugin/plugin.go index 6a75dedb..4dc25731 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -42,6 +42,8 @@ func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{} 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 @@ -49,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{