diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index c9499319..db7329f8 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -30,6 +30,7 @@ Copyright 2017 debugtalk ### SEE ALSO * [hrp boom](hrp_boom.md) - run load test with boomer +* [hrp build](hrp_build.md) - build plugin for testing * [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases * [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files * [hrp pytest](hrp_pytest.md) - run API test with pytest @@ -37,4 +38,4 @@ Copyright 2017 debugtalk * [hrp startproject](hrp_startproject.md) - create a scaffold project * [hrp wiki](hrp_wiki.md) - visit https://httprunner.com -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 429a0ed3..6f30b821 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -42,4 +42,4 @@ hrp boom [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md new file mode 100644 index 00000000..f3b222fe --- /dev/null +++ b/docs/cmd/hrp_build.md @@ -0,0 +1,31 @@ +## hrp build + +build plugin for testing + +### Synopsis + +build python/go plugin for testing + +``` +hrp build $path ... [flags] +``` + +### Examples + +``` + $ hrp build plugin/debugtalk.go + $ hrp build plugin/debugtalk.py +``` + +### Options + +``` + -h, --help help for build + -o, --output string funplugin product output path, default: cwd +``` + +### SEE ALSO + +* [hrp](hrp.md) - Next-Generation API Testing Solution. + +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 3083456c..c52a5348 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -22,4 +22,4 @@ hrp convert $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index 592c5281..2d4cd832 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -24,4 +24,4 @@ hrp har2case $har_path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index 711c8bac..87c1e906 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -16,4 +16,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 63da347e..21a42988 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -35,4 +35,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index e55c5429..45c1fdb3 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -21,4 +21,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md index 2eecbdd0..16fdf14d 100644 --- a/docs/cmd/hrp_wiki.md +++ b/docs/cmd/hrp_wiki.md @@ -16,4 +16,4 @@ hrp wiki [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 27-May-2022 +###### Auto generated by spf13/cobra on 28-May-2022 diff --git a/examples/demo-with-go-plugin/plugin/go.mod b/examples/demo-with-go-plugin/plugin/go.mod index 08a135d0..a36c1c27 100644 --- a/examples/demo-with-go-plugin/plugin/go.mod +++ b/examples/demo-with-go-plugin/plugin/go.mod @@ -2,4 +2,4 @@ module plugin go 1.16 -require github.com/httprunner/funplugin v0.4.6 // indirect +require github.com/httprunner/funplugin v0.4.7 // indirect diff --git a/examples/demo-with-go-plugin/plugin/go.sum b/examples/demo-with-go-plugin/plugin/go.sum index 59ea6478..bae78b47 100644 --- a/examples/demo-with-go-plugin/plugin/go.sum +++ b/examples/demo-with-go-plugin/plugin/go.sum @@ -58,8 +58,8 @@ github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v 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/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo= -github.com/httprunner/funplugin v0.4.6/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= +github.com/httprunner/funplugin v0.4.7 h1:bmk84BL8oPGE/rgxCuHgPcwJtBnwDzm/ocmFY/cKcos= +github.com/httprunner/funplugin v0.4.7/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index 2b2fcb6b..6e7a483a 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,6 +1,6 @@ { "project_name": "demo-with-go-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-with-go-plugin", - "create_time": "2022-05-27T11:34:23.903959+08:00", + "create_time": "2022-05-28T02:00:18.084185+08:00", "hrp_version": "v4.1.0-beta" } \ No newline at end of file diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index 555bccd7..9d8cce0e 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,6 +1,6 @@ { "project_name": "demo-with-py-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-with-py-plugin", - "create_time": "2022-05-27T11:34:31.852589+08:00", + "create_time": "2022-05-28T02:00:28.517914+08:00", "hrp_version": "v4.1.0-beta" } \ No newline at end of file diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json index 72c78cbf..e622b2a7 100644 --- a/examples/demo-without-plugin/proj.json +++ b/examples/demo-without-plugin/proj.json @@ -1,6 +1,6 @@ { "project_name": "demo-without-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-without-plugin", - "create_time": "2022-05-27T11:34:32.548637+08:00", + "create_time": "2022-05-28T02:00:29.191678+08:00", "hrp_version": "v4.1.0-beta" } \ No newline at end of file diff --git a/go.mod b/go.mod index 857c9444..080c56a0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-openapi/spec v0.20.6 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.1 - github.com/httprunner/funplugin v0.4.6 + github.com/httprunner/funplugin v0.4.7 github.com/jinzhu/copier v0.3.2 github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 diff --git a/go.sum b/go.sum index 4651a6e7..921c4995 100644 --- a/go.sum +++ b/go.sum @@ -253,8 +253,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 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/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo= -github.com/httprunner/funplugin v0.4.6/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= +github.com/httprunner/funplugin v0.4.7 h1:bmk84BL8oPGE/rgxCuHgPcwJtBnwDzm/ocmFY/cKcos= +github.com/httprunner/funplugin v0.4.7/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= diff --git a/hrp/cmd/build.go b/hrp/cmd/build.go new file mode 100644 index 00000000..da174bbc --- /dev/null +++ b/hrp/cmd/build.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/build" +) + +var buildCmd = &cobra.Command{ + Use: "build $path ...", + Short: "build plugin for testing", + Long: `build python/go plugin for testing`, + Example: ` $ hrp build plugin/debugtalk.go + $ hrp build plugin/debugtalk.py`, + Args: cobra.ExactArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return build.Run(args[0], output) + }, +} + +var output string + +func init() { + rootCmd.AddCommand(buildCmd) + + buildCmd.Flags().StringVarP(&output, "output", "o", "", "funplugin product output path, default: cwd") +} diff --git a/hrp/internal/build/main.go b/hrp/internal/build/main.go new file mode 100644 index 00000000..2c9cd28b --- /dev/null +++ b/hrp/internal/build/main.go @@ -0,0 +1,297 @@ +package build + +import ( + "bufio" + _ "embed" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/httprunner/funplugin/shared" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +const ( + funppy = `import funppy` + fungo = `"github.com/httprunner/funplugin/fungo"` + regexPythonFunctionName = `def ([a-zA-Z_]\w*)\(.*\)` + regexGoImports = `import \(([\s\S]*?)\)` + regexGoImport = `import (\"[\s\S]*\")` + regexGoFunctionName = `func ([A-Z][a-zA-Z_]\w*)\(.*\)` + regexGoFunctionContent = `func [\s\S]*?\n}` +) + +//go:embed templates/debugtalkPythonTemplate +var pyTemplate string + +//go:embed templates/debugtalkGoTemplate +var goTemplate string + +type TemplateContent struct { + Fun string // funplugin package + Regexps *Regexps // match import/function + Imports []string // python/go import + FromImports []string // python from...import... + Functions []string // python/go function + FunctionNames []string // function name set by user +} + +type Regexps struct { + Import *regexp.Regexp + Imports *regexp.Regexp + FunctionName *regexp.Regexp + FunctionContent *regexp.Regexp // including function define and body +} + +func (t *TemplateContent) parseGoContent(path string) error { + log.Info().Msg(fmt.Sprintf("start to parse %v", path)) + + content, err := os.ReadFile(path) + if err != nil { + log.Error().Err(err).Msg("failed to read file") + return err + } + originalContent := string(content) + + // parse imports + 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") { + t.Imports = append(t.Imports, strings.TrimSpace(elem)) + } + } + // parse import + importSlice = t.Regexps.Import.FindAllStringSubmatch(originalContent, -1) + if len(importSlice) != 0 { + for _, elem := range importSlice { + t.Imports = append(t.Imports, strings.TrimSpace(elem[1])) + } + } + // import fungo package + if !builtin.Contains(t.Imports, fungo) { + t.Imports = append(t.Imports, t.Fun) + } + + // parse function name + functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(originalContent, -1) + for _, elem := range functionNameSlice { + name := strings.Trim(elem[1], " ") + if name == "main" { + continue + } + t.FunctionNames = append(t.FunctionNames, name) + } + + // parse function content + functionContentSlice := t.Regexps.FunctionContent.FindAllStringSubmatch(originalContent, -1) + for _, f := range functionContentSlice { + if strings.Contains(f[0], "func main") { + continue + } + t.Functions = append(t.Functions, strings.Trim(f[0], "\n")) + } + return nil +} + +func (t *TemplateContent) parsePyContent(path string) error { + file, err := os.Open(path) + if err != nil { + fmt.Printf("Error: %s\n", err) + return err + } + defer file.Close() + + r := bufio.NewReader(file) + + // record content excluding import and main + content := "" + + // parse python content line by line + for { + l, _, err := r.ReadLine() + if err == io.EOF { + break + } + line := string(l) + + if strings.HasPrefix(line, "import") { + t.Imports = append(t.Imports, strings.Trim(line, " ")) + } else if strings.HasPrefix(line, "from") { + t.FromImports = append(t.FromImports, strings.Trim(line, " ")) + } else { + // no parse content at under of `if __name__ == "__main__"` + if strings.HasPrefix(line, "if __name__") { + break + } + if strings.HasPrefix(line, "def") { + functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(line, -1) + if len(functionNameSlice) == 0 { + continue + } + t.FunctionNames = append(t.FunctionNames, functionNameSlice[0][1]) + } + content += line + "\n" + } + } + // function content + t.Functions = append(t.Functions, strings.Trim(content, "\n")) + + // import funppy + if !builtin.Contains(t.Imports, t.Fun) { + t.Imports = append(t.Imports, t.Fun) + } + return nil +} + +func (t *TemplateContent) genDebugTalk(path string, templ string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + log.Error().Err(err).Msg("open file failed") + return err + } + defer file.Close() + writer := bufio.NewWriter(file) + tmpl := template.Must(template.New("debugtalk").Parse(templ)) + err = tmpl.Execute(writer, t) + if err != nil { + log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed") + return err + } + err = writer.Flush() + if err == nil { + log.Info().Str("path", path).Msg("generate debugtalk success") + } else { + log.Error().Str("path", path).Msg("generate debugtalk failed") + } + return err +} + +// buildGo builds debugtalk.go to debugtalk.bin +func buildGo(path string, output string) error { + templateContent := &TemplateContent{ + Fun: fungo, + Regexps: &Regexps{ + Import: regexp.MustCompile(regexGoImport), + Imports: regexp.MustCompile(regexGoImports), + FunctionName: regexp.MustCompile(regexGoFunctionName), + FunctionContent: regexp.MustCompile(regexGoFunctionContent), + }, + } + + // create temp dir for building + tempDir, err := ioutil.TempDir("", "hrp_build") + if err != nil { + return err + } + + // check go sdk in tempDir + if err := builtin.ExecCommandInDir(exec.Command("go", "version"), tempDir); err != nil { + return errors.Wrap(err, "go sdk not installed") + } + + // create pluginDir + pluginDir := filepath.Join(tempDir, "plugin") + if err := builtin.CreateFolder(pluginDir); err != nil { + return err + } + // parse debugtalk.go in pluginDir + err = templateContent.parseGoContent(path) + if err != nil { + return err + } + // generate debugtalk.go in pluginDir + err = templateContent.genDebugTalk(filepath.Join(pluginDir, "debugtalk.go"), goTemplate) + if err != nil { + return err + } + + // create go mod + if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "plugin"), pluginDir); err != nil { + return err + } + + // download plugin dependency + // funplugin version should be locked + funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version) + if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil { + return err + } + + if output == "" { + dir, _ := os.Getwd() + output = filepath.Join(dir, "debugtalk.bin") + } else if builtin.IsFolderPathExists(output) { + output = filepath.Join(output, "debugtalk.bin") + } + outputPath, err := filepath.Abs(output) + if err != nil { + return err + } + + // build plugin debugtalk.bin + if err := builtin.ExecCommandInDir(exec.Command("go", "build", "-o", outputPath, "debugtalk.go"), pluginDir); err != nil { + return err + } + log.Info().Msg(fmt.Sprintf("build %s to %s successfully", path, outputPath)) + return nil +} + +// buildPy completes funppy information in debugtalk.py +func buildPy(path string, output string) error { + templateContent := &TemplateContent{ + Fun: funppy, + Regexps: &Regexps{ + FunctionName: regexp.MustCompile(regexPythonFunctionName), + }, + } + err := templateContent.parsePyContent(path) + if err != nil { + return err + } + + // generate debugtalk.py + if output == "" { + dir, _ := os.Getwd() + output = filepath.Join(dir, "debugtalk_gen.py") + } else if builtin.IsFolderPathExists(output) { + output = filepath.Join(output, "debugtalk_gen.py") + } + err = templateContent.genDebugTalk(output, pyTemplate) + if err != nil { + return err + } + + // ensure funppy in .env + _, err = builtin.EnsurePython3Venv("funppy") + if err != nil { + return err + } + + return nil +} + +func Run(arg string, output string) (err error) { + ext := filepath.Ext(arg) + switch ext { + case ".py": + err = buildPy(arg, output) + case ".go": + err = buildGo(arg, output) + default: + return errors.New("type error, expected .py or .go") + } + if err != nil { + log.Error().Err(err).Msg(fmt.Sprintf("failed to build %s", arg)) + os.Exit(1) + } + return nil +} diff --git a/hrp/internal/build/main_test.go b/hrp/internal/build/main_test.go new file mode 100644 index 00000000..41eb1489 --- /dev/null +++ b/hrp/internal/build/main_test.go @@ -0,0 +1,42 @@ +package build + +import ( + "regexp" + "testing" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + + "github.com/stretchr/testify/assert" +) + +func TestRun(t *testing.T) { + err := Run("plugin/debugtalk.go", "./debugtalk_gen.bin") + if !assert.Nil(t, err) { + t.Fatal() + } + + err = Run("plugin/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() + } +} diff --git a/hrp/internal/build/plugin/debugtalk.go b/hrp/internal/build/plugin/debugtalk.go new file mode 100644 index 00000000..eb189472 --- /dev/null +++ b/hrp/internal/build/plugin/debugtalk.go @@ -0,0 +1,44 @@ +package noplugin + +import ( + "fmt" +) + +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 GetVersion() string { + return "v0.4" +} diff --git a/hrp/internal/build/plugin/debugtalk.py b/hrp/internal/build/plugin/debugtalk.py new file mode 100644 index 00000000..9f4c52bc --- /dev/null +++ b/hrp/internal/build/plugin/debugtalk.py @@ -0,0 +1,57 @@ +import logging +import time +from typing import List + + +def get_version(): + return "v0.4" + + +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}" diff --git a/hrp/internal/build/templates/debugtalkGoTemplate b/hrp/internal/build/templates/debugtalkGoTemplate new file mode 100644 index 00000000..d6d0a95e --- /dev/null +++ b/hrp/internal/build/templates/debugtalkGoTemplate @@ -0,0 +1,17 @@ +package main + +import ( +{{- range $import := .Imports }} + {{ $import -}} +{{ end }} +) + +{{ range $function := .Functions }} +{{ $function }} +{{ end }} +func main() { +{{- range $idx, $functionName := .FunctionNames }} + fungo.Register("{{ $functionName }}", {{ $functionName }}) +{{- end }} + fungo.Serve() +} diff --git a/hrp/internal/build/templates/debugtalkPythonTemplate b/hrp/internal/build/templates/debugtalkPythonTemplate new file mode 100644 index 00000000..4da17d12 --- /dev/null +++ b/hrp/internal/build/templates/debugtalkPythonTemplate @@ -0,0 +1,15 @@ +{{- range $import := .Imports }} +{{- $import}} +{{ end }} +{{ range $fromImport := .FromImports }} +{{- $fromImport}} +{{ end }} +{{ range $function := .Functions }} +{{ $function }} + +{{ end }} +if __name__ == "__main__": +{{- range $functionName := .FunctionNames }} + funppy.register("{{ $functionName }}", {{ $functionName }}) +{{- end }} + funppy.serve() diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index d4920084..09176168 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -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 { diff --git a/hrp/parser.go b/hrp/parser.go index b9693f0d..84482e3a 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -252,8 +252,14 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} // 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...) + if p.plugin != nil { + if p.plugin.Has(funcName) { + return p.plugin.Call(funcName, arguments...) + } + commonName := shared.ConvertCommonName(funcName) + if p.plugin.Has(commonName) { + return p.plugin.Call(commonName, arguments...) + } } // get builtin function diff --git a/hrp/plugin.go b/hrp/plugin.go index f9465a90..eeeda537 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -5,19 +5,22 @@ import ( "os" "os/signal" "path/filepath" + "strings" "syscall" "github.com/httprunner/funplugin" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/build" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) const ( - goPluginFile = "debugtalk.so" // built from go plugin - hashicorpGoPluginFile = "debugtalk.bin" // built from hashicorp go plugin - hashicorpPyPluginFile = "debugtalk.py" // used for hashicorp python plugin - projectInfoFile = "proj.json" // used for ensuring root project + goPluginFile = "debugtalk.so" // built from go plugin + hashicorpGoPluginFile = "debugtalk.bin" // built from hashicorp go plugin + hashicorpPyPluginFile = "debugtalk_gen.py" // used for hashicorp python plugin, automatically generated by HRP + debugtalkPyFile = "debugtalk.py" // write by user + projectInfoFile = "proj.json" // used for ensuring root project ) func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir string, err error) { @@ -32,6 +35,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,13 +80,18 @@ 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.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, debugtalkPyFile) + if err == nil { + return + } + pluginPath, err = locateFile(path, hashicorpPyPluginFile) if err == nil { return diff --git a/hrp/runner_test.go b/hrp/runner_test.go index d03f8d75..5a1327e2 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -8,14 +8,13 @@ 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 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+"plugin/debugtalk.go", templatesDir+"debugtalk.bin") if err != nil { log.Error().Err(err).Msg("build hashicorp go plugin failed") os.Exit(1) @@ -39,7 +38,8 @@ 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) {