change: remove startproject in python version, move all features to go version

This commit is contained in:
debugtalk
2022-03-26 14:25:17 +08:00
parent 6c17ea3ada
commit 878d009d76
29 changed files with 1152 additions and 893 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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()
}

View File

@@ -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
}

View 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
}

View File

@@ -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
`

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,2 @@
USERNAME=debugtalk
PASSWORD=123456

View File

@@ -0,0 +1,15 @@
.env
reports/
*.so
.vscode/
.idea/
.DS_Store
output/
__pycache__/
*.pyc
.python-version
logs/
# plugin
debugtalk.bin
debugtalk.so

View 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()
}

View File

@@ -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)

View File

@@ -0,0 +1 @@
# NOTICE: Generated By HttpRunner. DO NOT EDIT!

View File

@@ -0,0 +1,33 @@
config:
name: "request methods testcase: reference testcase"
variables:
foo1: testsuite_config_bar1
expect_foo1: testsuite_config_bar1
expect_foo2: config_bar2
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: request with functions
variables:
foo1: testcase_ref_bar1
expect_foo1: testcase_ref_bar1
testcase: testcases/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"]

View File

@@ -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()

View File

@@ -0,0 +1,65 @@
config:
name: "request methods testcase with functions"
variables:
foo1: config_bar1
foo2: config_bar2
expect_foo1: config_bar1
expect_foo2: config_bar2
base_url: "https://postman-echo.com"
verify: False
export: ["foo3"]
teststeps:
-
name: get with params
variables:
foo1: bar11
foo2: bar21
sum_v: "${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"]

View File

@@ -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()

View File

@@ -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"
}
]
}
]
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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{
{

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)