diff --git a/.github/workflows/scaffold.yml b/.github/workflows/scaffold.yml index bca29c55..eb764d53 100644 --- a/.github/workflows/scaffold.yml +++ b/.github/workflows/scaffold.yml @@ -1,11 +1,12 @@ name: Run scaffold on: + push: pull_request: types: [synchronize] jobs: - scaffold: + scaffold-with-python-plugin: strategy: fail-fast: false matrix: @@ -26,3 +27,47 @@ jobs: run: ./output/hrp startproject demo - name: Run demo tests run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml + + scaffold-with-go-plugin: + strategy: + fail-fast: false + matrix: + go-version: + - 1.17.x + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Build hrp binary + run: make build + - name: Run start project + run: ./output/hrp startproject demo --go + - name: Run demo tests + run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml + + scaffold-without-custom-plugin: + strategy: + fail-fast: false + matrix: + go-version: + - 1.17.x + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Build hrp binary + run: make build + - name: Run start project + run: ./output/hrp startproject demo --ignore-plugin + - name: Run demo tests + run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 029ef69b..47a940e8 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -15,6 +15,7 @@ jobs: go-version: - 1.16.x - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -22,6 +23,8 @@ jobs: uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} + - name: Install Python plugin dependencies + run: python3 -m pip install funppy - name: Checkout code uses: actions/checkout@v2 - name: Run coverage diff --git a/cli/hrp/cmd/har2case.go b/cli/hrp/cmd/har2case.go index 30f8f618..87cc5707 100644 --- a/cli/hrp/cmd/har2case.go +++ b/cli/hrp/cmd/har2case.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -22,7 +23,7 @@ var har2caseCmd = &cobra.Command{ for _, arg := range args { // must choose one if !genYAMLFlag && !genJSONFlag { - return errors.New("please select to-json flag or to-yaml flag.") + return errors.New("please select convert format type") } var outputPath string var err error @@ -37,7 +38,7 @@ var har2caseCmd = &cobra.Command{ if genYAMLFlag { outputPath, err = har.GenYAML() } else { - outputPath, err = har.GenJSON() + outputPath, err = har.GenJSON() // default } if err != nil { return err diff --git a/cli/hrp/cmd/scaffold.go b/cli/hrp/cmd/scaffold.go index 36a78f37..4f3a1c81 100644 --- a/cli/hrp/cmd/scaffold.go +++ b/cli/hrp/cmd/scaffold.go @@ -1,8 +1,10 @@ package cmd import ( + "errors" "os" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/httprunner/hrp/internal/scaffold" @@ -15,14 +17,37 @@ var scaffoldCmd = &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { - err := scaffold.CreateScaffold(args[0]) + RunE: func(cmd *cobra.Command, args []string) error { + if !ignorePlugin && !genPythonPlugin && !genGoPlugin { + return errors.New("please select function plugin type") + } + + var pluginType scaffold.PluginType + if ignorePlugin { + pluginType = scaffold.Ignore + } else if genGoPlugin { + pluginType = scaffold.Go + } else { + pluginType = scaffold.Py // default + } + err := scaffold.CreateScaffold(args[0], pluginType) if err != nil { + log.Error().Err(err).Msg("create scaffold project failed") os.Exit(1) } + return nil }, } +var ( + ignorePlugin bool + genPythonPlugin bool + genGoPlugin bool +) + func init() { rootCmd.AddCommand(scaffoldCmd) + scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin") + scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin") + scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin") } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 043e1123..95cff809 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## v0.8.0 (2022-03-18) + +- feat: create scaffold with python plugin + ## v0.7.0 (2022-03-15) - feat: support API layer for testcase #94 diff --git a/internal/scaffold/demo.go b/internal/scaffold/demo.go index 146e7f09..90dde3f4 100644 --- a/internal/scaffold/demo.go +++ b/internal/scaffold/demo.go @@ -66,8 +66,70 @@ var demoTestCase = &hrp.TestCase{ }, } +var demoTestCaseWithoutPlugin = &hrp.TestCase{ + Config: hrp.NewConfig("demo without custom function plugin"). + SetBaseURL("https://postman-echo.com"). + WithVariables(map[string]interface{}{ // global level variables + "n": 5, + "a": 12.3, + "b": 3.45, + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function + }), + TestSteps: []hrp.IStep{ + hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction + hrp.NewStep("get with params"). + WithVariables(map[string]interface{}{ // step level variables + "n": 3, // inherit config level variables if not set in step level, a/varFoo1 + "b": 34.5, // override config level variable if existed, n/b/varFoo2 + "varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again + "name": "get with params", + }). + GET("/get"). + WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers + Extract(). + WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath + Validate(). + AssertEqual("status_code", 200, "check response status code"). // validate response status code + AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header + AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath + AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step + AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string + hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction + hrp.NewStep("post json data"). + POST("/post"). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + }). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.json.foo1", 5, "check args foo1"). + AssertEqual("body.json.foo2", 12.3, "check args foo2"), + hrp.NewStep("post form data"). + POST("/post"). + WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + "time": "${get_timestamp()}", + }). + Extract(). + WithJmesPath("body.form.time", "varTime"). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.form.foo1", 5, "check args foo1"). + AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string + hrp.NewStep("get with timestamp"). + GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). + Validate(). + AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), + }, +} + // debugtalk.go -var demoPlugin = `package main +var demoGoPlugin = `package main import ( "fmt" @@ -120,9 +182,67 @@ func main() { } ` +// debugtalk.py +var demoPyPlugin = `import logging +from typing import List + +import funppy + + +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.warn("setup_hook_example") + return f"setup_hook_example: {name}" + +def teardown_hook_example(name): + logging.warn("teardown_hook_example") + return f"teardown_hook_example: {name}" + + +if __name__ == '__main__': + funppy.register("sum", sum) + funppy.register("sum_ints", sum_ints) + funppy.register("concatenate", concatenate) + funppy.register("sum_two_int", sum_two_int) + funppy.register("sum_two_string", sum_two_string) + funppy.register("sum_strings", sum_strings) + funppy.register("setup_hook_example", setup_hook_example) + funppy.register("teardown_hook_example", teardown_hook_example) + funppy.serve() +` + // .gitignore var demoIgnoreContent = `.env -reports/* +reports/ *.so .vscode/ .idea/ diff --git a/internal/scaffold/main.go b/internal/scaffold/main.go index 777dede7..549ab5c6 100644 --- a/internal/scaffold/main.go +++ b/internal/scaffold/main.go @@ -6,12 +6,24 @@ import ( "os/exec" "path" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/funplugin/shared" + "github.com/httprunner/hrp" "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" - "github.com/rs/zerolog/log" ) -func CreateScaffold(projectName string) error { +type PluginType uint + +const ( + Ignore PluginType = iota + Py + Go +) + +func CreateScaffold(projectName string, pluginType PluginType) error { // report event ga.SendEvent(ga.EventTracking{ Category: "Scaffold", @@ -37,16 +49,17 @@ func CreateScaffold(projectName string) error { if err := builtin.CreateFolder(path.Join(projectName, "testcases")); err != nil { return err } - pluginDir := path.Join(projectName, "plugin") - if err := builtin.CreateFolder(pluginDir); err != nil { - return err - } if err := builtin.CreateFolder(path.Join(projectName, "reports")); err != nil { return err } // create demo testcases - tCase, _ := demoTestCase.ToTCase() + var tCase *hrp.TCase + if pluginType == Ignore { + tCase, _ = demoTestCaseWithoutPlugin.ToTCase() + } else { + tCase, _ = demoTestCase.ToTCase() + } err := builtin.Dump2JSON(tCase, path.Join(projectName, "testcases", "demo.json")) if err != nil { log.Error().Err(err).Msg("create demo.json testcase failed") @@ -58,9 +71,43 @@ func CreateScaffold(projectName string) error { return err } + // create .gitignore + if err := builtin.CreateFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil { + return err + } + // create .env + if err := builtin.CreateFile(path.Join(projectName, ".env"), demoEnvContent); err != nil { + return err + } + + // create debugtalk function plugin + switch pluginType { + case Ignore: + log.Info().Msg("skip creating function plugin") + return nil + case Py: + return createPythonPlugin(projectName) + case Go: + return createGoPlugin(projectName) + } + + return nil +} + +func createGoPlugin(projectName string) error { + log.Info().Msg("start to create hashicorp go plugin") + // check go sdk + if err := builtin.ExecCommand(exec.Command("go", "version"), projectName); err != nil { + return errors.Wrap(err, "go sdk not installed") + } + // create debugtalk.go + pluginDir := path.Join(projectName, "plugin") + if err := builtin.CreateFolder(pluginDir); err != nil { + return err + } pluginFile := path.Join(pluginDir, "debugtalk.go") - if err := builtin.CreateFile(pluginFile, demoPlugin); err != nil { + if err := builtin.CreateFile(pluginFile, demoGoPlugin); err != nil { return err } @@ -79,12 +126,20 @@ func CreateScaffold(projectName string) error { return err } - // create .gitignore - if err := builtin.CreateFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil { + return nil +} + +func createPythonPlugin(projectName string) error { + log.Info().Msg("start to create hashicorp python plugin") + + // create debugtalk.py + pluginFile := path.Join(projectName, "debugtalk.py") + if err := builtin.CreateFile(pluginFile, demoPyPlugin); err != nil { return err } - // create .env - if err := builtin.CreateFile(path.Join(projectName, ".env"), demoEnvContent); err != nil { + + // create python venv + if _, err := shared.PreparePython3Venv(pluginFile); err != nil { return err }