feat: support v3 format debugtalk.py when executing hrp run/boom

This commit is contained in:
xucong053
2022-05-27 11:20:21 +08:00
parent 726e566668
commit 5c2be8d548
32 changed files with 1386 additions and 67 deletions

View File

@@ -25,7 +25,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
},
}
testcase2 := &demoTestCaseWithPluginJSONPath
testcase2 := &demoTestCaseWithGoPluginJSONPath
b := NewBoomer(2, 1)
go b.Run(testcase1, testcase2)

View File

@@ -1,46 +0,0 @@
package examples
import (
"fmt"
)
import "os"
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 SetupHookExample(args string) string {
return fmt.Sprintf("step name: %v, setup...", args)
}
func TeardownHookExample(args string) string {
return fmt.Sprintf("step name: %v, teardown...", args)
}
func init() {
_, _ = os.Getwd()
}

View File

@@ -1,53 +0,0 @@
import logging
import time
from typing import List
def sleep(n_secs):
time.sleep(n_secs)
def sum(*args):
result = 0
for arg in args:
result += arg
return result
def sum_ints(*args: List[int]) -> int:
result = 0
for arg in args:
result += arg
return result
def sum_two_int(a: int, b: int) -> int:
return a + b
def sum_two_string(a: str, b: str) -> str:
return a + b
def sum_strings(*args: List[str]) -> str:
result = ""
for arg in args:
result += arg
return result
def concatenate(*args: List[str]) -> str:
result = ""
for arg in args:
result += str(arg)
return result
def setup_hook_example(name):
logging.warning("setup_hook_example")
return f"setup_hook_example: {name}"
def teardown_hook_example(name):
logging.warning("teardown_hook_example")
return f"teardown_hook_example: {name}"

View File

@@ -62,7 +62,7 @@ func (t *TemplateContent) parseGoContent(path string) error {
originalContent := string(content)
// parse imports
importSlice := t.Regexps.Import.FindAllStringSubmatch(originalContent, -1)
importSlice := t.Regexps.Imports.FindAllStringSubmatch(originalContent, -1)
if len(importSlice) != 0 {
imports := strings.Replace(importSlice[0][1], "\t", "", -1)
for _, elem := range strings.Split(imports, "\n") {
@@ -70,7 +70,7 @@ func (t *TemplateContent) parseGoContent(path string) error {
}
}
// parse import
importSlice = t.Regexps.Imports.FindAllStringSubmatch(originalContent, -1)
importSlice = t.Regexps.Import.FindAllStringSubmatch(originalContent, -1)
if len(importSlice) != 0 {
for _, elem := range importSlice {
t.Imports = append(t.Imports, strings.TrimSpace(elem[1]))

View File

@@ -1,29 +1,51 @@
package build
import (
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRun(t *testing.T) {
err := Run("examples/debugtalk_no_funppy.py", "")
err := Run("../../../examples/demo-with-no-fungo/plugin/debugtalk.go", "")
if !assert.Nil(t, err) {
t.Fatal()
}
err = Run("examples/debugtalk_no_fungo.go", "")
err = Run("../../../examples/demo-with-no-funppy/debugtalk.py", "")
if !assert.Nil(t, err) {
t.Fatal()
}
err = Run("examples/debugtalk_no_funppy.py", "./debugtalk.py")
err = Run("../../../examples/demo-with-no-fungo/plugin/debugtalk.go", "./debugtalk_gen.bin")
if !assert.Nil(t, err) {
t.Fatal()
}
err = Run("examples/debugtalk_no_fungo.go", "./debugtalk_gen.bin")
err = Run("../../../examples/demo-with-no-funppy/debugtalk.py", "./debugtalk_gen.py")
if !assert.Nil(t, err) {
t.Fatal()
}
contentBytes, err := builtin.ReadFile("./debugtalk_gen.py")
if !assert.Nil(t, err) {
t.Fatal()
}
content := string(contentBytes)
if !assert.Contains(t, content, "import funppy") {
t.Fatal()
}
if !assert.Contains(t, content, "funppy.register") {
t.Fatal()
}
reg, _ := regexp.Compile(`funppy\.register`)
matchedSlice := reg.FindAllStringSubmatch(content, -1)
if !assert.Len(t, matchedSlice, 10) {
t.Fatal()
}
}

View File

@@ -281,7 +281,7 @@ var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension")
// LoadFile loads file content with file extension and assigns to structObj
func LoadFile(path string, structObj interface{}) (err error) {
log.Info().Str("path", path).Msg("load file")
file, err := readFile(path)
file, err := ReadFile(path)
if err != nil {
return errors.Wrap(err, "read file failed")
}
@@ -335,7 +335,7 @@ func parseEnvContent(file []byte, obj interface{}) error {
func loadFromCSV(path string) []map[string]interface{} {
log.Info().Str("path", path).Msg("load csv file")
file, err := readFile(path)
file, err := ReadFile(path)
if err != nil {
log.Error().Err(err).Msg("read csv file failed")
os.Exit(1)
@@ -361,7 +361,7 @@ func loadFromCSV(path string) []map[string]interface{} {
func loadMessage(path string) []byte {
log.Info().Str("path", path).Msg("load message file")
file, err := readFile(path)
file, err := ReadFile(path)
if err != nil {
log.Error().Err(err).Msg("read message file failed")
os.Exit(1)
@@ -369,7 +369,7 @@ func loadMessage(path string) []byte {
return file
}
func readFile(path string) ([]byte, error) {
func ReadFile(path string) ([]byte, error) {
var err error
path, err = filepath.Abs(path)
if err != nil {

View File

@@ -46,12 +46,12 @@ func GetVersion() string {
}
func main() {
fungo.Register("get_version", GetVersion)
fungo.Register("sum_ints", SumInts)
fungo.Register("sum_two_int", SumTwoInt)
fungo.Register("sum_two", SumTwoInt)
fungo.Register("sum", Sum)
fungo.Register("setup_hook_example", SetupHookExample)
fungo.Register("teardown_hook_example", TeardownHookExample)
fungo.Register("GetVersion", GetVersion)
fungo.Register("SumInts", SumInts)
fungo.Register("SumTwoInt", SumTwoInt)
fungo.Register("SumTwoInt", SumTwoInt)
fungo.Register("Sum", Sum)
fungo.Register("SetupHookExample", SetupHookExample)
fungo.Register("TeardownHookExample", TeardownHookExample)
fungo.Serve()
}

View File

@@ -0,0 +1,33 @@
config:
name: "request methods testcase: reference testcase"
variables:
foo1: testsuite_config_bar1
expect_foo1: testsuite_config_bar1
expect_foo2: config_bar2
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: request with functions
variables:
foo1: testcase_ref_bar1
expect_foo1: testcase_ref_bar1
testcase: testcases/requests.yml
export:
- foo3
-
name: post form data
variables:
foo1: bar1
request:
method: POST
url: /post
headers:
User-Agent: funplugin/${GetVersion()}
Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo3"
validate:
- eq: ["status_code", 200]
- eq: ["body.form.foo1", "bar1"]
- eq: ["body.form.foo2", "bar21"]

View File

@@ -0,0 +1,138 @@
{
"config": {
"name": "request methods testcase with functions",
"variables": {
"foo1": "config_bar1",
"foo2": "config_bar2",
"expect_foo1": "config_bar1",
"expect_foo2": "config_bar2"
},
"base_url": "https://postman-echo.com",
"verify": false,
"export": [
"foo3"
]
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "bar11",
"foo2": "bar21",
"sum_v": "${SumTwoInt(1, 2)}"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"sum_v": "$sum_v"
},
"headers": {
"User-Agent": "funplugin/${GetVersion()}"
}
},
"extract": {
"foo3": "body.args.foo2"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.args.foo1",
"bar11"
]
},
{
"eq": [
"body.args.sum_v",
"3"
]
},
{
"eq": [
"body.args.foo2",
"bar21"
]
}
]
},
{
"name": "post raw text",
"variables": {
"foo1": "bar12",
"foo3": "bar32"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${GetVersion()}",
"Content-Type": "text/plain"
},
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.data",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."
]
}
]
},
{
"name": "post form data",
"variables": {
"foo2": "bar23"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${GetVersion()}",
"Content-Type": "application/x-www-form-urlencoded"
},
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.form.foo1",
"$expect_foo1"
]
},
{
"eq": [
"body.form.foo2",
"bar23"
]
},
{
"eq": [
"body.form.foo3",
"bar21"
]
}
]
}
]
}

View File

@@ -0,0 +1,65 @@
config:
name: "request methods testcase with functions"
variables:
foo1: config_bar1
foo2: config_bar2
expect_foo1: config_bar1
expect_foo2: config_bar2
base_url: "https://postman-echo.com"
verify: False
export: ["foo3"]
teststeps:
-
name: get with params
variables:
foo1: bar11
foo2: bar21
sum_v: "${SumTwoInt(1, 2)}"
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
sum_v: $sum_v
headers:
User-Agent: funplugin/${GetVersion()}
extract:
foo3: "body.args.foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.args.foo1", "bar11"]
- eq: ["body.args.sum_v", "3"]
- eq: ["body.args.foo2", "bar21"]
-
name: post raw text
variables:
foo1: "bar12"
foo3: "bar32"
request:
method: POST
url: /post
headers:
User-Agent: funplugin/${GetVersion()}
Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
validate:
- eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
-
name: post form data
variables:
foo2: bar23
request:
method: POST
url: /post
headers:
User-Agent: funplugin/${GetVersion()}
Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
validate:
- eq: ["status_code", 200]
- eq: ["body.form.foo1", "$expect_foo1"]
- eq: ["body.form.foo2", "bar23"]
- eq: ["body.form.foo3", "bar21"]

View File

@@ -0,0 +1,176 @@
{
"config": {
"name": "demo with complex mechanisms",
"base_url": "https://postman-echo.com",
"variables": {
"a": "${Sum(10, 2.3)}",
"b": 3.45,
"n": "${SumInts(1, 2, 2)}",
"varFoo1": "${gen_random_string($n)}",
"varFoo2": "${max($a, $b)}"
}
},
"teststeps": [
{
"name": "transaction 1 start",
"transaction": {
"name": "tran1",
"type": "start"
}
},
{
"name": "get with params",
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$varFoo1",
"foo2": "$varFoo2"
},
"headers": {
"User-Agent": "HttpRunnerPlus"
}
},
"variables": {
"b": 34.5,
"n": 3,
"name": "get with params",
"varFoo2": "${max($a, $b)}"
},
"setup_hooks": [
"${SetupHookExample($name)}"
],
"teardown_hooks": [
"${TeardownHookExample($name)}"
],
"extract": {
"varFoo1": "body.args.foo1"
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "startswith",
"expect": "application/json"
},
{
"check": "body.args.foo1",
"assert": "length_equals",
"expect": 5,
"msg": "check args foo1"
},
{
"check": "$varFoo1",
"assert": "length_equals",
"expect": 5,
"msg": "check args foo1"
},
{
"check": "body.args.foo2",
"assert": "equals",
"expect": "34.5",
"msg": "check args foo2"
}
]
},
{
"name": "transaction 1 end",
"transaction": {
"name": "tran1",
"type": "end"
}
},
{
"name": "post json data",
"request": {
"method": "POST",
"url": "/post",
"body": {
"foo1": "$varFoo1",
"foo2": "${max($a, $b)}"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
},
{
"check": "body.json.foo1",
"assert": "length_equals",
"expect": 5,
"msg": "check args foo1"
},
{
"check": "body.json.foo2",
"assert": "equals",
"expect": 12.3,
"msg": "check args foo2"
}
]
},
{
"name": "post form data",
"request": {
"method": "POST",
"url": "/post",
"headers": {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
"body": {
"foo1": "$varFoo1",
"foo2": "${max($a, $b)}",
"time": "${get_timestamp()}"
}
},
"extract": {
"varTime": "body.form.time"
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
},
{
"check": "body.form.foo1",
"assert": "length_equals",
"expect": 5,
"msg": "check args foo1"
},
{
"check": "body.form.foo2",
"assert": "equals",
"expect": "12.3",
"msg": "check args foo2"
}
]
},
{
"name": "get with timestamp",
"request": {
"method": "GET",
"url": "/get",
"params": {
"time": "$varTime"
}
},
"validate": [
{
"check": "body.args.time",
"assert": "length_equals",
"expect": 13,
"msg": "check extracted var timestamp"
}
]
}
]
}

View File

@@ -2,9 +2,11 @@ package hrp
import (
"fmt"
"github.com/httprunner/httprunner/v4/hrp/internal/build"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/httprunner/funplugin"
@@ -32,6 +34,21 @@ func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir st
// TODO: move pluginDir to funplugin
pluginDir = filepath.Dir(pluginPath)
// compatible the format of debugtalk.py with v2/v3
ext := filepath.Ext(pluginPath)
if ext == ".py" {
// skip if only debugtalk_gen.py exists
if !strings.HasSuffix(pluginPath, "debugtalk_gen.py") {
genPyPluginPath := filepath.Join(pluginDir, "debugtalk_gen.py")
err = build.Run(pluginPath, genPyPluginPath)
if err != nil {
log.Error().Err(err).Msgf(fmt.Sprintf("failed to build %s", pluginPath))
return
}
pluginPath = genPyPluginPath
}
}
// found plugin file
plugin, err = funplugin.Init(pluginPath, funplugin.WithLogOn(logOn))
if err != nil {
@@ -62,19 +79,19 @@ func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir st
}
func locatePlugin(path string) (pluginPath string, err error) {
// priority: hashicorp plugin (debugtalk.bin > debugtalk_gen.py > debugtalk.py) > go plugin (debugtalk.so)
// priority: hashicorp plugin (debugtalk.bin > debugtalk.py > debugtalk_gen.py) > go plugin (debugtalk.so)
pluginPath, err = locateFile(path, hashicorpGoPluginFile)
if err == nil {
return
}
pluginPath, err = locateFile(path, hashicorpPyPluginFile)
pluginPath, err = locateFile(path, debugtalkPyFile)
if err == nil {
return
}
pluginPath, err = locateFile(path, debugtalkPyFile)
pluginPath, err = locateFile(path, hashicorpPyPluginFile)
if err == nil {
return
}

View File

@@ -8,14 +8,22 @@ import (
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/build"
"github.com/httprunner/httprunner/v4/hrp/internal/scaffold"
)
func buildHashicorpGoPluginWithNoFungo() {
log.Info().Msg("[init] build hashicorp go plugin")
err := build.Run(templatesDir+"noplugin/debugtalk.go", templatesDir+"debugtalk.bin")
if err != nil {
log.Error().Err(err).Msg("build hashicorp go plugin failed")
os.Exit(1)
}
}
func buildHashicorpGoPlugin() {
log.Info().Msg("[init] build hashicorp go plugin")
err := builtin.ExecCommand("go", "build",
"-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go")
err := build.Run(templatesDir+"noplugin/debugtalk.go", templatesDir+"debugtalk.bin")
if err != nil {
log.Error().Err(err).Msg("build hashicorp go plugin failed")
os.Exit(1)
@@ -30,7 +38,7 @@ func removeHashicorpGoPlugin() {
func buildHashicorpPyPlugin() {
log.Info().Msg("[init] prepare hashicorp python plugin")
pluginFile := templatesDir + "debugtalk.py"
err := scaffold.CopyFile("templates/plugin/debugtalk.py", pluginFile)
err := scaffold.CopyFile("templates/noplugin/debugtalk.py", pluginFile)
if err != nil {
log.Error().Err(err).Msg("build hashicorp python plugin failed")
os.Exit(1)
@@ -39,24 +47,14 @@ func buildHashicorpPyPlugin() {
func removeHashicorpPyPlugin() {
log.Info().Msg("[teardown] remove hashicorp python plugin")
os.Remove(templatesDir + "debugtalk.py")
// on v4.1^, running case will generate debugtalk_gen.py used by python plugin
os.Remove(templatesDir + "debugtalk_gen.py")
}
func TestRunCaseWithGoPlugin(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
assertRunTestCases(t)
}
func TestRunCaseWithPythonPlugin(t *testing.T) {
buildHashicorpPyPlugin()
defer removeHashicorpPyPlugin()
assertRunTestCases(t)
}
func assertRunTestCases(t *testing.T) {
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetBaseURL("http://httpbin.org"),
@@ -83,7 +81,7 @@ func assertRunTestCases(t *testing.T) {
},
},
),
NewStep("testcase1-step4").CallRefCase(&demoTestCaseWithPluginJSONPath),
NewStep("testcase1-step4").CallRefCase(&demoTestCaseWithGoPluginJSONPath),
},
}
testcase2 := &TestCase{
@@ -98,6 +96,18 @@ func assertRunTestCases(t *testing.T) {
}
}
func TestRunCaseWithPythonPlugin(t *testing.T) {
buildHashicorpPyPlugin()
defer removeHashicorpPyPlugin()
r := NewRunner(t)
r.SetPluginLogOn()
err := r.Run(&demoTestCaseWithPluginJSONPath)
if err != nil {
t.Fatalf("run testcase error: %v", err)
}
}
func TestRunCaseWithThinkTime(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
@@ -160,15 +170,15 @@ func TestRunCaseWithPluginJSON(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
err := NewRunner(nil).Run(&demoTestCaseWithPluginJSONPath) // hrp.Run(testCase)
err := NewRunner(nil).Run(&demoTestCaseWithGoPluginJSONPath) // hrp.Run(testCase)
if err != nil {
t.Fatal()
}
}
func TestRunCaseWithPluginYAML(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
buildHashicorpPyPlugin()
defer removeHashicorpPyPlugin()
err := NewRunner(nil).Run(&demoTestCaseWithPluginYAMLPath) // hrp.Run(testCase)
if err != nil {

View File

@@ -15,6 +15,7 @@ const (
var (
demoTestCaseWithPluginJSONPath TestCasePath = templatesDir + "testcases/demo_with_funplugin.json"
demoTestCaseWithGoPluginJSONPath TestCasePath = templatesDir + "testcases/demo_go_with_funplugin.json"
demoTestCaseWithPluginYAMLPath TestCasePath = templatesDir + "testcases/demo_with_funplugin.yaml"
demoTestCaseWithoutPluginJSONPath TestCasePath = templatesDir + "testcases/demo_without_funplugin.json"
demoTestCaseWithoutPluginYAMLPath TestCasePath = templatesDir + "testcases/demo_without_funplugin.yaml"