mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-14 13:07:37 +08:00
change: remove startproject in python version, move all features to go version
This commit is contained in:
6
.github/workflows/hrp-scaffold.yml
vendored
6
.github/workflows/hrp-scaffold.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Run start project
|
||||
run: ./output/hrp startproject demo
|
||||
- name: Run demo tests
|
||||
run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml
|
||||
run: ./output/hrp run demo/testcases/demo_with_funplugin.json demo/testcases/demo_requests.yml demo/testcases/demo_ref_testcase.yml
|
||||
|
||||
scaffold-with-go-plugin:
|
||||
strategy:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- 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
|
||||
run: ./output/hrp run demo/testcases/demo_with_funplugin.json demo/testcases/demo_requests.yml demo/testcases/demo_ref_testcase.yml
|
||||
|
||||
scaffold-without-custom-plugin:
|
||||
strategy:
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
- 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
|
||||
run: ./output/hrp run demo/testcases/demo_without_plugin.json
|
||||
|
||||
1
.github/workflows/smoketest.yml
vendored
1
.github/workflows/smoketest.yml
vendored
@@ -37,7 +37,6 @@ jobs:
|
||||
run: |
|
||||
poetry run hrun -V
|
||||
poetry run httprunner run -h
|
||||
poetry run httprunner startproject -h
|
||||
- name: Run smoketest - postman echo
|
||||
run: |
|
||||
poetry run hrun examples/postman_echo/request_methods
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
**python version**
|
||||
|
||||
- change: remove startproject, move all features to go version, replace with `hrp startproject`
|
||||
- change: remove har2case, move all features to go version, replace with `hrp run`
|
||||
- change: remove locust, you should run load tests with go version, replace with `hrp boom`
|
||||
- change: remove fastapi and uvicorn dependencies
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
func TestBoomerStandaloneRun(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"),
|
||||
@@ -25,7 +25,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
|
||||
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
||||
},
|
||||
}
|
||||
testcase2 := &demoTestCaseJSONPath
|
||||
testcase2 := &demoTestCaseWithPluginJSONPath
|
||||
|
||||
b := NewBoomer(2, 1)
|
||||
go b.Run(testcase1, testcase2)
|
||||
|
||||
@@ -30,6 +30,7 @@ var scaffoldCmd = &cobra.Command{
|
||||
} else {
|
||||
pluginType = scaffold.Py // default
|
||||
}
|
||||
|
||||
err := scaffold.CreateScaffold(args[0], pluginType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create scaffold project failed")
|
||||
|
||||
@@ -8,23 +8,198 @@ import (
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
const templatesDir = "internal/scaffold/templates/"
|
||||
|
||||
var (
|
||||
demoTestCaseWithPluginJSONPath TestCasePath = templatesDir + "testcases/demo_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"
|
||||
)
|
||||
|
||||
var (
|
||||
demoTestCaseJSONPath TestCasePath = "../examples/hrp/demo.json"
|
||||
demoTestCaseYAMLPath TestCasePath = "../examples/hrp/demo.yaml"
|
||||
demoRefAPIYAMLPath TestCasePath = "../examples/hrp/ref_api_test.yaml"
|
||||
demoRefTestCaseJSONPath TestCasePath = "../examples/hrp/ref_testcase_test.json"
|
||||
demoThinkTimeJsonPath TestCasePath = "../examples/hrp/think_time_test.json"
|
||||
demoAPIYAMLPath APIPath = "../examples/hrp/api/put.yml"
|
||||
)
|
||||
|
||||
var demoTestCaseWithPlugin = &TestCase{
|
||||
Config: NewConfig("demo with complex mechanisms").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{ // global level variables
|
||||
"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
|
||||
}),
|
||||
TestSteps: []IStep{
|
||||
NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction
|
||||
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",
|
||||
}).
|
||||
SetupHook("${setup_hook_example($name)}").
|
||||
GET("/get").
|
||||
TeardownHook("${teardown_hook_example($name)}").
|
||||
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
|
||||
NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction
|
||||
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"),
|
||||
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
|
||||
NewStep("get with timestamp").
|
||||
GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}).
|
||||
Validate().
|
||||
AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
var demoTestCaseWithoutPlugin = &TestCase{
|
||||
Config: 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: []IStep{
|
||||
NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction
|
||||
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
|
||||
NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction
|
||||
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"),
|
||||
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
|
||||
NewStep("get with timestamp").
|
||||
GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}).
|
||||
Validate().
|
||||
AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestGenDemoTestCase(t *testing.T) {
|
||||
tCase, _ := demoTestCaseWithPlugin.ToTCase()
|
||||
err := builtin.Dump2JSON(tCase, demoTestCaseWithPluginJSONPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, demoTestCaseWithPluginYAMLPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
tCase, _ = demoTestCaseWithoutPlugin.ToTCase()
|
||||
err = builtin.Dump2JSON(tCase, demoTestCaseWithoutPluginJSONPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, demoTestCaseWithoutPluginYAMLPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonDemoWithPlugin(t *testing.T) {
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
err := NewRunner(nil).Run(&demoTestCaseWithPluginJSONPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestYamlDemoWithPlugin(t *testing.T) {
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
err := NewRunner(nil).Run(&demoTestCaseWithPluginYAMLPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCase(t *testing.T) {
|
||||
tcJSON := &TCase{}
|
||||
tcYAML := &TCase{}
|
||||
err := builtin.LoadFile(demoTestCaseJSONPath.ToString(), tcJSON)
|
||||
err := builtin.LoadFile(demoTestCaseWithPluginJSONPath.ToString(), tcJSON)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
err = builtin.LoadFile(demoTestCaseYAMLPath.ToString(), tcYAML)
|
||||
err = builtin.LoadFile(demoTestCaseWithPluginYAMLPath.ToString(), tcYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
var Functions = map[string]interface{}{
|
||||
@@ -61,235 +46,3 @@ func MD5(str string) string {
|
||||
hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func Dump2JSON(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to json")
|
||||
file, _ := json.MarshalIndent(data, "", " ")
|
||||
err = os.WriteFile(path, file, 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump json path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Dump2YAML(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to yaml")
|
||||
|
||||
// init yaml encoder
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(4)
|
||||
|
||||
// encode
|
||||
err = encoder.Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, buffer.Bytes(), 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump yaml path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatResponse(raw interface{}) interface{} {
|
||||
formattedResponse := make(map[string]interface{})
|
||||
for key, value := range raw.(map[string]interface{}) {
|
||||
// convert value to json
|
||||
if key == "body" {
|
||||
b, _ := json.MarshalIndent(&value, "", " ")
|
||||
value = string(b)
|
||||
}
|
||||
formattedResponse[key] = value
|
||||
}
|
||||
return formattedResponse
|
||||
}
|
||||
|
||||
func ExecCommand(cmd *exec.Cmd, cwd string) error {
|
||||
log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command")
|
||||
cmd.Dir = cwd
|
||||
output, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(output))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("output", out).Msg("exec command failed")
|
||||
} else if len(out) != 0 {
|
||||
log.Info().Str("output", out).Msg("exec command success")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateFolder(folderPath string) error {
|
||||
log.Info().Str("path", folderPath).Msg("create folder")
|
||||
err := os.MkdirAll(folderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create folder failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateFile(filePath string, data string) error {
|
||||
log.Info().Str("path", filePath).Msg("create file")
|
||||
err := os.WriteFile(filePath, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create file failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists, whether path is file or dir
|
||||
func isPathExists(path string) bool {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists and path is file
|
||||
func isFilePathExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// path not exists
|
||||
return false
|
||||
}
|
||||
|
||||
// path exists
|
||||
if info.IsDir() {
|
||||
// path is dir, not file
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EnsureFolderExists(folderPath string) error {
|
||||
if !isPathExists(folderPath) {
|
||||
err := CreateFolder(folderPath)
|
||||
return err
|
||||
} else if isFilePathExists(folderPath) {
|
||||
return fmt.Errorf("path %v should be directory", folderPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRandomNumber(min, max int) int {
|
||||
if min > max {
|
||||
return 0
|
||||
}
|
||||
r := rand.Intn(max - min + 1)
|
||||
return min + r
|
||||
}
|
||||
|
||||
func Interface2Float64(i interface{}) (float64, error) {
|
||||
switch i.(type) {
|
||||
case int:
|
||||
return float64(i.(int)), nil
|
||||
case int32:
|
||||
return float64(i.(int32)), nil
|
||||
case int64:
|
||||
return float64(i.(int64)), nil
|
||||
case float32:
|
||||
return float64(i.(float32)), nil
|
||||
case float64:
|
||||
return i.(float64), nil
|
||||
case string:
|
||||
intVar, err := strconv.Atoi(i.(string))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(intVar), err
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
if ok {
|
||||
return value.Float64()
|
||||
}
|
||||
return 0, errors.New("failed to convert interface to float64")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read file failed")
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".json", ".har":
|
||||
decoder := json.NewDecoder(bytes.NewReader(file))
|
||||
decoder.UseNumber()
|
||||
err = decoder.Decode(structObj)
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(file, structObj)
|
||||
default:
|
||||
err = ErrUnsupportedFileExt
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func loadFromCSV(path string) []map[string]interface{} {
|
||||
log.Info().Str("path", path).Msg("load csv file")
|
||||
file, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r := csv.NewReader(strings.NewReader(string(file)))
|
||||
content, err := r.ReadAll()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("parse csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
var result []map[string]interface{}
|
||||
for i := 1; i < len(content); i++ {
|
||||
row := make(map[string]interface{})
|
||||
for j := 0; j < len(content[i]); j++ {
|
||||
row[content[0][j]] = content[i][j]
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
var err error
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("convert absolute path failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read file failed")
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
252
hrp/internal/builtin/utils.go
Normal file
252
hrp/internal/builtin/utils.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
func Dump2JSON(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to json")
|
||||
file, _ := json.MarshalIndent(data, "", " ")
|
||||
err = os.WriteFile(path, file, 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump json path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Dump2YAML(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to yaml")
|
||||
|
||||
// init yaml encoder
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(4)
|
||||
|
||||
// encode
|
||||
err = encoder.Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, buffer.Bytes(), 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump yaml path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatResponse(raw interface{}) interface{} {
|
||||
formattedResponse := make(map[string]interface{})
|
||||
for key, value := range raw.(map[string]interface{}) {
|
||||
// convert value to json
|
||||
if key == "body" {
|
||||
b, _ := json.MarshalIndent(&value, "", " ")
|
||||
value = string(b)
|
||||
}
|
||||
formattedResponse[key] = value
|
||||
}
|
||||
return formattedResponse
|
||||
}
|
||||
|
||||
func ExecCommand(cmd *exec.Cmd, cwd string) error {
|
||||
log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command")
|
||||
cmd.Dir = cwd
|
||||
output, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(output))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("output", out).Msg("exec command failed")
|
||||
} else if len(out) != 0 {
|
||||
log.Info().Str("output", out).Msg("exec command success")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateFolder(folderPath string) error {
|
||||
log.Info().Str("path", folderPath).Msg("create folder")
|
||||
err := os.MkdirAll(folderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create folder failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateFile(filePath string, data string) error {
|
||||
log.Info().Str("path", filePath).Msg("create file")
|
||||
err := os.WriteFile(filePath, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create file failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists, whether path is file or dir
|
||||
func isPathExists(path string) bool {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists and path is file
|
||||
func isFilePathExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// path not exists
|
||||
return false
|
||||
}
|
||||
|
||||
// path exists
|
||||
if info.IsDir() {
|
||||
// path is dir, not file
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EnsureFolderExists(folderPath string) error {
|
||||
if !isPathExists(folderPath) {
|
||||
err := CreateFolder(folderPath)
|
||||
return err
|
||||
} else if isFilePathExists(folderPath) {
|
||||
return fmt.Errorf("path %v should be directory", folderPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRandomNumber(min, max int) int {
|
||||
if min > max {
|
||||
return 0
|
||||
}
|
||||
r := rand.Intn(max - min + 1)
|
||||
return min + r
|
||||
}
|
||||
|
||||
func Interface2Float64(i interface{}) (float64, error) {
|
||||
switch i.(type) {
|
||||
case int:
|
||||
return float64(i.(int)), nil
|
||||
case int32:
|
||||
return float64(i.(int32)), nil
|
||||
case int64:
|
||||
return float64(i.(int64)), nil
|
||||
case float32:
|
||||
return float64(i.(float32)), nil
|
||||
case float64:
|
||||
return i.(float64), nil
|
||||
case string:
|
||||
intVar, err := strconv.Atoi(i.(string))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(intVar), err
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
if ok {
|
||||
return value.Float64()
|
||||
}
|
||||
return 0, errors.New("failed to convert interface to float64")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read file failed")
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".json", ".har":
|
||||
decoder := json.NewDecoder(bytes.NewReader(file))
|
||||
decoder.UseNumber()
|
||||
err = decoder.Decode(structObj)
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(file, structObj)
|
||||
default:
|
||||
err = ErrUnsupportedFileExt
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func loadFromCSV(path string) []map[string]interface{} {
|
||||
log.Info().Str("path", path).Msg("load csv file")
|
||||
file, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r := csv.NewReader(strings.NewReader(string(file)))
|
||||
content, err := r.ReadAll()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("parse csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
var result []map[string]interface{}
|
||||
for i := 1; i < len(content); i++ {
|
||||
row := make(map[string]interface{})
|
||||
for j := 0; j < len(content[i]); j++ {
|
||||
row[content[0][j]] = content[i][j]
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
var err error
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("convert absolute path failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read file failed")
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package scaffold
|
||||
|
||||
import "github.com/httprunner/httprunner/hrp"
|
||||
|
||||
var demoTestCase = &hrp.TestCase{
|
||||
Config: hrp.NewConfig("demo with complex mechanisms").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{ // global level variables
|
||||
"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
|
||||
}),
|
||||
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",
|
||||
}).
|
||||
SetupHook("${setup_hook_example($name)}").
|
||||
GET("/get").
|
||||
TeardownHook("${teardown_hook_example($name)}").
|
||||
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"),
|
||||
},
|
||||
}
|
||||
|
||||
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 demoGoPlugin = `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
)
|
||||
|
||||
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 main() {
|
||||
fungo.Register("sum_ints", SumInts)
|
||||
fungo.Register("sum_two_int", SumTwoInt)
|
||||
fungo.Register("sum", Sum)
|
||||
fungo.Register("setup_hook_example", SetupHookExample)
|
||||
fungo.Register("teardown_hook_example", TeardownHookExample)
|
||||
fungo.Serve()
|
||||
}
|
||||
`
|
||||
|
||||
// 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.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}"
|
||||
|
||||
|
||||
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/
|
||||
*.so
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
output/
|
||||
|
||||
# plugin
|
||||
debugtalk.bin
|
||||
debugtalk.so
|
||||
`
|
||||
|
||||
// .env
|
||||
var demoEnvContent = `USERNAME=debugtalk
|
||||
PASSWORD=123456
|
||||
`
|
||||
@@ -1,75 +0,0 @@
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
var (
|
||||
demoTestCaseJSONPath hrp.TestCasePath = "../../../examples/hrp/demo.json"
|
||||
demoTestCaseYAMLPath hrp.TestCasePath = "../../../examples/hrp/demo.yaml"
|
||||
)
|
||||
|
||||
func buildHashicorpPlugin() {
|
||||
log.Info().Msg("[init] build hashicorp go plugin")
|
||||
cmd := exec.Command("go", "build",
|
||||
"-o", "../../../examples/hrp/debugtalk.bin",
|
||||
"../../../examples/hrp/plugin/hashicorp.go", "../../../examples/hrp/plugin/debugtalk.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeHashicorpPlugin() {
|
||||
log.Info().Msg("[teardown] remove hashicorp plugin")
|
||||
os.Remove("../../../examples/hrp/debugtalk.bin")
|
||||
}
|
||||
|
||||
func TestGenDemoTestCase(t *testing.T) {
|
||||
tCase, _ := demoTestCase.ToTCase()
|
||||
err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath.ToString())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleDemo(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
demoTestCase.Config.Path = "../../../examples/hrp/debugtalk.bin"
|
||||
err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonDemo(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
err := hrp.NewRunner(nil).Run(&demoTestCaseJSONPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestYamlDemo(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
err := hrp.NewRunner(nil).Run(&demoTestCaseYAMLPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/funplugin/shared"
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
||||
)
|
||||
@@ -23,6 +23,25 @@ const (
|
||||
Go PluginType = "go"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templatesDir embed.FS
|
||||
|
||||
// CopyFile copies a file from templates dir to scaffold project
|
||||
func CopyFile(templateFile, targetFile string) error {
|
||||
log.Info().Str("path", targetFile).Msg("create file")
|
||||
content, err := templatesDir.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "template file not found")
|
||||
}
|
||||
|
||||
err = os.WriteFile(targetFile, content, 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create file failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateScaffold(projectName string, pluginType PluginType) error {
|
||||
// report event
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
@@ -46,48 +65,56 @@ func CreateScaffold(projectName string, pluginType PluginType) error {
|
||||
if err := builtin.CreateFolder(projectName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(path.Join(projectName, "har")); err != nil {
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, "har")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(path.Join(projectName, "testcases")); err != nil {
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, "testcases")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(path.Join(projectName, "reports")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create demo testcases
|
||||
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")
|
||||
return err
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, path.Join(projectName, "testcases", "demo.yaml"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create demo.yml testcase failed")
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, "reports")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create .gitignore
|
||||
if err := builtin.CreateFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil {
|
||||
err := CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// create .env
|
||||
if err := builtin.CreateFile(path.Join(projectName, ".env"), demoEnvContent); err != nil {
|
||||
err = CopyFile("templates/env", filepath.Join(projectName, ".env"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create demo testcases
|
||||
if pluginType == Ignore {
|
||||
err := CopyFile("templates/testcases/demo_without_plugin.json",
|
||||
filepath.Join(projectName, "testcases", "demo_without_plugin.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info().Msg("skip creating function plugin")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = CopyFile("templates/testcases/demo_with_funplugin.json",
|
||||
filepath.Join(projectName, "testcases", "demo_with_funplugin.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyFile("templates/testcases/demo_requests.yml",
|
||||
filepath.Join(projectName, "testcases", "demo_requests.yml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyFile("templates/testcases/demo_ref_testcase.yml",
|
||||
filepath.Join(projectName, "testcases", "demo_ref_testcase.yml"))
|
||||
if 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:
|
||||
@@ -105,12 +132,13 @@ func createGoPlugin(projectName string) error {
|
||||
}
|
||||
|
||||
// create debugtalk.go
|
||||
pluginDir := path.Join(projectName, "plugin")
|
||||
pluginDir := filepath.Join(projectName, "plugin")
|
||||
if err := builtin.CreateFolder(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
pluginFile := path.Join(pluginDir, "debugtalk.go")
|
||||
if err := builtin.CreateFile(pluginFile, demoGoPlugin); err != nil {
|
||||
err := CopyFile("templates/plugin/debugtalk.go",
|
||||
filepath.Join(projectName, "plugin", "debugtalk.go"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -125,7 +153,7 @@ func createGoPlugin(projectName string) error {
|
||||
}
|
||||
|
||||
// build plugin debugtalk.bin
|
||||
if err := builtin.ExecCommand(exec.Command("go", "build", "-o", path.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil {
|
||||
if err := builtin.ExecCommand(exec.Command("go", "build", "-o", filepath.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -136,8 +164,9 @@ 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 {
|
||||
pluginFile := filepath.Join(projectName, "debugtalk.py")
|
||||
err := CopyFile("templates/plugin/debugtalk.py", pluginFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
2
hrp/internal/scaffold/templates/env
Normal file
2
hrp/internal/scaffold/templates/env
Normal file
@@ -0,0 +1,2 @@
|
||||
USERNAME=debugtalk
|
||||
PASSWORD=123456
|
||||
15
hrp/internal/scaffold/templates/gitignore
Normal file
15
hrp/internal/scaffold/templates/gitignore
Normal file
@@ -0,0 +1,15 @@
|
||||
.env
|
||||
reports/
|
||||
*.so
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
output/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.python-version
|
||||
logs/
|
||||
|
||||
# plugin
|
||||
debugtalk.bin
|
||||
debugtalk.so
|
||||
57
hrp/internal/scaffold/templates/plugin/debugtalk.go
Normal file
57
hrp/internal/scaffold/templates/plugin/debugtalk.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
)
|
||||
|
||||
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 "v4.0.0-alpha"
|
||||
}
|
||||
|
||||
func main() {
|
||||
fungo.Register("get_httprunner_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.Serve()
|
||||
}
|
||||
@@ -1,53 +1,71 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import funppy
|
||||
|
||||
|
||||
def get_httprunner_version():
|
||||
return "v4.0.0-alpha"
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
funppy.register("get_httprunner_version", get_httprunner_version)
|
||||
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", 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)
|
||||
1
hrp/internal/scaffold/templates/testcases/__init__.py
Normal file
1
hrp/internal/scaffold/templates/testcases/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
|
||||
@@ -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/demo_requests.yml
|
||||
export:
|
||||
- foo3
|
||||
-
|
||||
name: post form data
|
||||
variables:
|
||||
foo1: bar1
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
User-Agent: HttpRunner/${get_httprunner_version()}
|
||||
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"]
|
||||
@@ -0,0 +1,60 @@
|
||||
# NOTE: Generated By HttpRunner v4.0.0-alpha
|
||||
# FROM: testcases/demo_ref_testcase.yml
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
from testcases.demo_requests_test import TestCaseDemoRequests as DemoRequests
|
||||
|
||||
|
||||
class TestCaseDemoRefTestcase(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("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 = [
|
||||
Step(
|
||||
RunTestCase("request with functions")
|
||||
.with_variables(
|
||||
**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}
|
||||
)
|
||||
.call(DemoRequests)
|
||||
.export(*["foo3"])
|
||||
),
|
||||
Step(
|
||||
RunRequest("post form data")
|
||||
.with_variables(**{"foo1": "bar1"})
|
||||
.post("/post")
|
||||
.with_headers(
|
||||
**{
|
||||
"User-Agent": "HttpRunner/${get_httprunner_version()}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
)
|
||||
.with_data("foo1=$foo1&foo2=$foo3")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.form.foo1", "bar1")
|
||||
.assert_equal("body.form.foo2", "bar21")
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseDemoRefTestcase().test_start()
|
||||
65
hrp/internal/scaffold/templates/testcases/demo_requests.yml
Normal file
65
hrp/internal/scaffold/templates/testcases/demo_requests.yml
Normal 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: "${sum_two(1, 2)}"
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: $foo1
|
||||
foo2: $foo2
|
||||
sum_v: $sum_v
|
||||
headers:
|
||||
User-Agent: HttpRunner/${get_httprunner_version()}
|
||||
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: HttpRunner/${get_httprunner_version()}
|
||||
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: HttpRunner/${get_httprunner_version()}
|
||||
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"]
|
||||
@@ -0,0 +1,83 @@
|
||||
# NOTE: Generated By HttpRunner v4.0.0-alpha
|
||||
# FROM: testcases/demo_requests.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseDemoRequests(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("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 = [
|
||||
Step(
|
||||
RunRequest("get with params")
|
||||
.with_variables(
|
||||
**{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"}
|
||||
)
|
||||
.get("/get")
|
||||
.with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"})
|
||||
.with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"})
|
||||
.extract()
|
||||
.with_jmespath("body.args.foo2", "foo3")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.args.foo1", "bar11")
|
||||
.assert_equal("body.args.sum_v", "3")
|
||||
.assert_equal("body.args.foo2", "bar21")
|
||||
),
|
||||
Step(
|
||||
RunRequest("post raw text")
|
||||
.with_variables(**{"foo1": "bar12", "foo3": "bar32"})
|
||||
.post("/post")
|
||||
.with_headers(
|
||||
**{
|
||||
"User-Agent": "HttpRunner/${get_httprunner_version()}",
|
||||
"Content-Type": "text/plain",
|
||||
}
|
||||
)
|
||||
.with_data(
|
||||
"This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||
)
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal(
|
||||
"body.data",
|
||||
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
|
||||
)
|
||||
),
|
||||
Step(
|
||||
RunRequest("post form data")
|
||||
.with_variables(**{"foo2": "bar23"})
|
||||
.post("/post")
|
||||
.with_headers(
|
||||
**{
|
||||
"User-Agent": "HttpRunner/${get_httprunner_version()}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
)
|
||||
.with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.form.foo1", "$expect_foo1")
|
||||
.assert_equal("body.form.foo2", "bar23")
|
||||
.assert_equal("body.form.foo3", "bar21")
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseDemoRequests().test_start()
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "demo without custom function plugin",
|
||||
"base_url": "https://postman-echo.com",
|
||||
"variables": {
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
"n": 5,
|
||||
"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)}"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
config:
|
||||
name: demo without custom function plugin
|
||||
base_url: https://postman-echo.com
|
||||
variables:
|
||||
a: 12.3
|
||||
b: 3.45
|
||||
"n": 5
|
||||
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)}
|
||||
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
|
||||
@@ -8,32 +8,24 @@ import (
|
||||
|
||||
func TestLocateFile(t *testing.T) {
|
||||
// specify target file path
|
||||
_, err := locateFile("../examples/hrp/plugin/debugtalk.go", "debugtalk.go")
|
||||
_, err := locateFile(templatesDir+"plugin/debugtalk.go", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// specify path with the same dir
|
||||
_, err = locateFile("../examples/hrp/plugin/hashicorp.go", "debugtalk.go")
|
||||
_, err = locateFile(templatesDir+"plugin/debugtalk.py", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// specify target file path dir
|
||||
_, err = locateFile("../examples/hrp/plugin/", "debugtalk.go")
|
||||
_, err = locateFile(templatesDir+"plugin/", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// specify wrong path
|
||||
_, err = locateFile("../examples/hrp", "debugtalk.go")
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
_, err = locateFile("../examples/hrp/demo.json", "debugtalk.go")
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
_, err = locateFile(".", "debugtalk.go")
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
@@ -45,17 +37,17 @@ func TestLocateFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLocatePythonPlugin(t *testing.T) {
|
||||
_, err := locatePlugin("../examples/hrp/debugtalk.py")
|
||||
_, err := locatePlugin(templatesDir + "plugin/debugtalk.py")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocateGoPlugin(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
_, err := locatePlugin("../examples/hrp/debugtalk.bin")
|
||||
_, err := locatePlugin(templatesDir + "debugtalk.bin")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
@@ -8,31 +8,49 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/scaffold"
|
||||
)
|
||||
|
||||
func buildHashicorpPlugin() {
|
||||
func buildHashicorpGoPlugin() {
|
||||
log.Info().Msg("[init] build hashicorp go plugin")
|
||||
cmd := exec.Command("go", "build",
|
||||
"-o", "../examples/hrp/debugtalk.bin",
|
||||
"../examples/hrp/plugin/hashicorp.go", "../examples/hrp/plugin/debugtalk.go")
|
||||
"-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeHashicorpPlugin() {
|
||||
log.Info().Msg("[teardown] remove hashicorp plugin")
|
||||
os.Remove("../examples/hrp/debugtalk.bin")
|
||||
func removeHashicorpGoPlugin() {
|
||||
log.Info().Msg("[teardown] remove hashicorp go plugin")
|
||||
os.Remove(templatesDir + "debugtalk.bin")
|
||||
}
|
||||
|
||||
func buildHashicorpPyPlugin() {
|
||||
log.Info().Msg("[init] prepare hashicorp python plugin")
|
||||
pluginFile := templatesDir + "debugtalk.py"
|
||||
err := scaffold.CopyFile("templates/plugin/debugtalk.py", pluginFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeHashicorpPyPlugin() {
|
||||
log.Info().Msg("[teardown] remove hashicorp python plugin")
|
||||
os.Remove(templatesDir + "debugtalk.py")
|
||||
}
|
||||
|
||||
func TestHttpRunnerWithGoPlugin(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
assertRunTestCases(t)
|
||||
}
|
||||
|
||||
func TestHttpRunnerWithPythonPlugin(t *testing.T) {
|
||||
buildHashicorpPyPlugin()
|
||||
defer removeHashicorpPyPlugin()
|
||||
|
||||
assertRunTestCases(t)
|
||||
}
|
||||
|
||||
@@ -64,7 +82,7 @@ func assertRunTestCases(t *testing.T) {
|
||||
},
|
||||
),
|
||||
NewStep("TestCase4").CallRefCase(&demoRefAPIYAMLPath),
|
||||
NewStep("TestCase5").CallRefCase(&demoTestCaseJSONPath),
|
||||
NewStep("TestCase5").CallRefCase(&demoTestCaseWithPluginJSONPath),
|
||||
},
|
||||
}
|
||||
testcase2 := &TestCase{
|
||||
@@ -153,8 +171,8 @@ func TestInitRendezvous(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestThinkTime(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
testcases := []*TestCase{
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ from loguru import logger
|
||||
from httprunner import __description__, __version__
|
||||
from httprunner.compat import ensure_cli_args
|
||||
from httprunner.make import init_make_parser, main_make
|
||||
from httprunner.scaffold import init_parser_scaffold, main_scaffold
|
||||
from httprunner.utils import ga_client, init_sentry_sdk
|
||||
|
||||
init_sentry_sdk()
|
||||
@@ -65,7 +64,6 @@ def main():
|
||||
|
||||
subparsers = parser.add_subparsers(help="sub-command help")
|
||||
sub_parser_run = init_parser_run(subparsers)
|
||||
sub_parser_scaffold = init_parser_scaffold(subparsers)
|
||||
sub_parser_make = init_make_parser(subparsers)
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
@@ -80,9 +78,6 @@ def main():
|
||||
elif sys.argv[1] in ["-h", "--help"]:
|
||||
# httprunner -h
|
||||
parser.print_help()
|
||||
elif sys.argv[1] == "startproject":
|
||||
# httprunner startproject
|
||||
sub_parser_scaffold.print_help()
|
||||
elif sys.argv[1] == "run":
|
||||
# httprunner run
|
||||
pytest.main(["-h"])
|
||||
@@ -109,8 +104,6 @@ def main():
|
||||
|
||||
if sys.argv[1] == "run":
|
||||
sys.exit(main_run(extra_args))
|
||||
elif sys.argv[1] == "startproject":
|
||||
main_scaffold(args)
|
||||
elif sys.argv[1] == "make":
|
||||
main_make(args.testcase_path)
|
||||
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import os.path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from httprunner.utils import ga_client
|
||||
|
||||
|
||||
def init_parser_scaffold(subparsers):
|
||||
sub_parser_scaffold = subparsers.add_parser(
|
||||
"startproject", help="Create a new project with template structure."
|
||||
)
|
||||
sub_parser_scaffold.add_argument(
|
||||
"project_name", type=str, nargs="?", help="Specify new project name."
|
||||
)
|
||||
return sub_parser_scaffold
|
||||
|
||||
|
||||
def create_scaffold(project_name):
|
||||
""" create scaffold with specified project name.
|
||||
"""
|
||||
|
||||
def show_tree(prj_name):
|
||||
try:
|
||||
print(f"\n$ tree {prj_name} -a")
|
||||
subprocess.run(["tree", prj_name, "-a"])
|
||||
print("")
|
||||
except FileNotFoundError:
|
||||
logger.warning("tree command not exists, ignore.")
|
||||
|
||||
if os.path.isdir(project_name):
|
||||
logger.warning(
|
||||
f"Project folder {project_name} exists, please specify a new project name."
|
||||
)
|
||||
show_tree(project_name)
|
||||
return 1
|
||||
elif os.path.isfile(project_name):
|
||||
logger.warning(
|
||||
f"Project name {project_name} conflicts with existed file, please specify a new one."
|
||||
)
|
||||
return 1
|
||||
|
||||
logger.info(f"Create new project: {project_name}")
|
||||
print(f"Project Root Dir: {os.path.join(os.getcwd(), project_name)}\n")
|
||||
|
||||
def create_folder(path):
|
||||
os.makedirs(path)
|
||||
msg = f"created folder: {path}"
|
||||
print(msg)
|
||||
|
||||
def create_file(path, file_content=""):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(file_content)
|
||||
msg = f"created file: {path}"
|
||||
print(msg)
|
||||
|
||||
demo_testcase_request_content = """
|
||||
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: "${sum_two(1, 2)}"
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: $foo1
|
||||
foo2: $foo2
|
||||
sum_v: $sum_v
|
||||
headers:
|
||||
User-Agent: HttpRunner/${get_httprunner_version()}
|
||||
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: HttpRunner/${get_httprunner_version()}
|
||||
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: HttpRunner/${get_httprunner_version()}
|
||||
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"]
|
||||
"""
|
||||
demo_testcase_with_ref_content = """
|
||||
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/demo_testcase_request.yml
|
||||
export:
|
||||
- foo3
|
||||
-
|
||||
name: post form data
|
||||
variables:
|
||||
foo1: bar1
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
User-Agent: HttpRunner/${get_httprunner_version()}
|
||||
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"]
|
||||
"""
|
||||
ignore_content = "\n".join(
|
||||
[".env", "reports/*", "__pycache__/*", "*.pyc", ".python-version", "logs/*"]
|
||||
)
|
||||
demo_debugtalk_content = """import time
|
||||
|
||||
from httprunner import __version__
|
||||
|
||||
|
||||
def get_httprunner_version():
|
||||
return __version__
|
||||
|
||||
|
||||
def sum_two(m, n):
|
||||
return m + n
|
||||
|
||||
|
||||
def sleep(n_secs):
|
||||
time.sleep(n_secs)
|
||||
"""
|
||||
demo_env_content = "\n".join(["USERNAME=leolee", "PASSWORD=123456"])
|
||||
|
||||
create_folder(project_name)
|
||||
create_folder(os.path.join(project_name, "har"))
|
||||
create_folder(os.path.join(project_name, "testcases"))
|
||||
create_folder(os.path.join(project_name, "reports"))
|
||||
|
||||
create_file(
|
||||
os.path.join(project_name, "testcases", "demo_testcase_request.yml"),
|
||||
demo_testcase_request_content,
|
||||
)
|
||||
create_file(
|
||||
os.path.join(project_name, "testcases", "demo_testcase_ref.yml"),
|
||||
demo_testcase_with_ref_content,
|
||||
)
|
||||
create_file(os.path.join(project_name, "debugtalk.py"), demo_debugtalk_content)
|
||||
create_file(os.path.join(project_name, ".env"), demo_env_content)
|
||||
create_file(os.path.join(project_name, ".gitignore"), ignore_content)
|
||||
|
||||
show_tree(project_name)
|
||||
return 0
|
||||
|
||||
|
||||
def main_scaffold(args):
|
||||
ga_client.track_event("Scaffold", "startproject")
|
||||
sys.exit(create_scaffold(args.project_name))
|
||||
@@ -1,29 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
import platform
|
||||
|
||||
from httprunner.scaffold import create_scaffold
|
||||
|
||||
|
||||
class TestScaffold(unittest.TestCase):
|
||||
def test_create_scaffold(self):
|
||||
project_name = "projectABC"
|
||||
create_scaffold(project_name)
|
||||
self.assertTrue(os.path.isdir(os.path.join(project_name, "har")))
|
||||
self.assertTrue(os.path.isdir(os.path.join(project_name, "testcases")))
|
||||
self.assertTrue(os.path.isdir(os.path.join(project_name, "reports")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(project_name, "debugtalk.py")))
|
||||
self.assertTrue(os.path.isfile(os.path.join(project_name, ".env")))
|
||||
|
||||
# run demo testcases
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
subprocess.check_call(["hrun", project_name], shell=True)
|
||||
else:
|
||||
subprocess.check_call(["hrun", project_name])
|
||||
except subprocess.SubprocessError:
|
||||
raise
|
||||
finally:
|
||||
shutil.rmtree(project_name)
|
||||
Reference in New Issue
Block a user