mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-28 20:09:36 +08:00
refactor: move hrp/ to root folder
This commit is contained in:
229
internal/builtin/assertion.go
Normal file
229
internal/builtin/assertion.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected interface{}, msgAndArgs ...interface{}) bool{
|
||||
"eq": EqualValues,
|
||||
"equals": EqualValues,
|
||||
"equal": EqualValues,
|
||||
"lt": assert.Less,
|
||||
"less_than": assert.Less,
|
||||
"le": assert.LessOrEqual,
|
||||
"less_or_equals": assert.LessOrEqual,
|
||||
"gt": assert.Greater,
|
||||
"greater_than": assert.Greater,
|
||||
"ge": assert.GreaterOrEqual,
|
||||
"greater_or_equals": assert.GreaterOrEqual,
|
||||
"ne": NotEqual,
|
||||
"not_equal": NotEqual,
|
||||
"contains": assert.Contains,
|
||||
"type_match": assert.IsType,
|
||||
// custom assertions
|
||||
"startswith": StartsWith,
|
||||
"endswith": EndsWith,
|
||||
"len_eq": EqualLength,
|
||||
"length_equals": EqualLength,
|
||||
"length_equal": EqualLength,
|
||||
"len_lt": LessThanLength,
|
||||
"count_lt": LessThanLength,
|
||||
"length_less_than": LessThanLength,
|
||||
"len_le": LessOrEqualsLength,
|
||||
"count_le": LessOrEqualsLength,
|
||||
"length_less_or_equals": LessOrEqualsLength,
|
||||
"len_gt": GreaterThanLength,
|
||||
"count_gt": GreaterThanLength,
|
||||
"length_greater_than": GreaterThanLength,
|
||||
"len_ge": GreaterOrEqualsLength,
|
||||
"count_ge": GreaterOrEqualsLength,
|
||||
"length_greater_or_equals": GreaterOrEqualsLength,
|
||||
"contained_by": ContainedBy,
|
||||
"str_eq": StringEqual,
|
||||
"string_equals": StringEqual,
|
||||
"equal_fold": EqualFold,
|
||||
"regex_match": RegexMatch,
|
||||
}
|
||||
|
||||
func EqualValues(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.EqualValues(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
func NotEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.NotEqual(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
// StartsWith check if string starts with substring
|
||||
func StartsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasPrefix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
// EndsWith check if string ends with substring
|
||||
func EndsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasSuffix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
func EqualLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l != length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect == %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GreaterThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l <= length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect > %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GreaterOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l < length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect >= %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func LessThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l >= length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect < %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func LessOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l > length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect <= %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ContainedBy assert whether actual element contains expected element
|
||||
func ContainedBy(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.Contains(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
func StringEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
a := fmt.Sprintf("%v", actual)
|
||||
e := fmt.Sprintf("%v", expected)
|
||||
return assert.True(t, a == e, msgAndArgs)
|
||||
}
|
||||
|
||||
func EqualFold(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, msgAndArgs) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, msgAndArgs) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.EqualFold(actualString, expectedString), msgAndArgs)
|
||||
}
|
||||
|
||||
func RegexMatch(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.Regexp(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
func convertInt(value interface{}) (int, error) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v, nil
|
||||
case int8:
|
||||
return int(v), nil
|
||||
case int16:
|
||||
return int(v), nil
|
||||
case int32:
|
||||
return int(v), nil
|
||||
case int64:
|
||||
return int(v), nil
|
||||
case uint:
|
||||
return int(v), nil
|
||||
case uint8:
|
||||
return int(v), nil
|
||||
case uint16:
|
||||
return int(v), nil
|
||||
case uint32:
|
||||
return int(v), nil
|
||||
case uint64:
|
||||
return int(v), nil
|
||||
case float32:
|
||||
return int(v), nil
|
||||
case float64:
|
||||
return int(v), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
|
||||
}
|
||||
}
|
||||
|
||||
// getLen try to get length of object.
|
||||
// return (false, 0) if impossible.
|
||||
func getLen(x interface{}) (ok bool, length int) {
|
||||
v := reflect.ValueOf(x)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ok = false
|
||||
}
|
||||
}()
|
||||
return true, v.Len()
|
||||
}
|
||||
212
internal/builtin/assertion_test.go
Normal file
212
internal/builtin/assertion_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "a"},
|
||||
{"abc", "ab"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StartsWith(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "c"},
|
||||
{"abc", "bc"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EndsWith(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 0},
|
||||
{[]string{}, 0},
|
||||
{map[string]interface{}{}, 0},
|
||||
{"a", 1},
|
||||
{[]string{"a"}, 1},
|
||||
{map[string]interface{}{"a": 123}, 1},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EqualLength(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessThanLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 1},
|
||||
{[]string{}, 1},
|
||||
{map[string]interface{}{}, 1},
|
||||
{"a", 2},
|
||||
{[]string{"a"}, 2},
|
||||
{map[string]interface{}{"a": 123}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessThanLength(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessOrEqualsLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 1},
|
||||
{[]string{}, 1},
|
||||
{map[string]interface{}{"A": 111}, 1},
|
||||
{"a", 1},
|
||||
{[]string{"a"}, 2},
|
||||
{map[string]interface{}{"a": 123}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreaterThanLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"abcd", 3},
|
||||
{[]string{"a", "b", "c"}, 2},
|
||||
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterThanLength(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreaterOrEqualsLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"abcd", 3},
|
||||
{[]string{"w"}, 1},
|
||||
{map[string]interface{}{"A": 111}, 1},
|
||||
{"a", 1},
|
||||
{[]string{"a", "b", "c"}, 2},
|
||||
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainedBy(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"abcd", "abcdefg"},
|
||||
{"a", []string{"a", "b", "c"}},
|
||||
{"A", map[string]interface{}{"A": 111, "B": 222}},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, ContainedBy(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqual(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"abcd", "abcd"},
|
||||
{"0", 0},
|
||||
{"123", 123},
|
||||
// {"123.0", 123.0}, // FIXME
|
||||
{"12.3", 12.3},
|
||||
{"-12.3", -12.3},
|
||||
{"-123", -123},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StringEqual(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualFold(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"abcd", "abcd"},
|
||||
{"abcd", "ABCD"},
|
||||
{"ABcd", "abCD"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EqualFold(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatch(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"it's starting...", regexp.MustCompile("start")},
|
||||
{"it's not starting", "starting$"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, RegexMatch(t, data.raw, data.expected)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
238
internal/builtin/function.go
Normal file
238
internal/builtin/function.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Functions = map[string]interface{}{
|
||||
"get_timestamp": getTimestamp, // call without arguments
|
||||
"sleep": sleep, // call with one argument
|
||||
"gen_random_string": genRandomString, // call with one argument
|
||||
"random_int": rand.Intn, // call with one argument
|
||||
"random_range": random_range, // call with two arguments
|
||||
"max": math.Max, // call with two arguments
|
||||
"md5": MD5, // call with one argument
|
||||
"parameterize": loadFromCSV,
|
||||
"P": loadFromCSV,
|
||||
"split_by_comma": splitByComma, // call with one argument
|
||||
"environ": os.Getenv,
|
||||
"ENV": os.Getenv,
|
||||
"load_ws_message": loadMessage,
|
||||
"multipart_encoder": multipartEncoder,
|
||||
"multipart_content_type": multipartContentType,
|
||||
}
|
||||
|
||||
// upload file path must starts with @, like @\"PATH\" or @PATH
|
||||
var regexUploadFilePath = regexp.MustCompile(`^@(.*)`)
|
||||
|
||||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
|
||||
func escapeQuotes(s string) string {
|
||||
return quoteEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func random_range(a, b float64) float64 {
|
||||
return a + rand.Float64()*(b-a)
|
||||
}
|
||||
|
||||
func getTimestamp() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func sleep(nSecs int) {
|
||||
time.Sleep(time.Duration(nSecs) * time.Second)
|
||||
}
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
func genRandomString(n int) string {
|
||||
lettersLen := len(letters)
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(lettersLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func MD5(str string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
type TFormDataWriter struct {
|
||||
Writer *multipart.Writer
|
||||
Payload *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *TFormDataWriter) writeCustomText(formKey, formValue, formType, formFileName string) error {
|
||||
if w.Writer == nil {
|
||||
return errors.New("form-data writer not initialized")
|
||||
}
|
||||
h := make(textproto.MIMEHeader)
|
||||
// text doesn't have Content-Type by default
|
||||
if formType != "" {
|
||||
h.Set("Content-Type", formType)
|
||||
}
|
||||
// text doesn't have filename in Content-Disposition by default
|
||||
if formFileName == "" {
|
||||
h.Set("Content-Disposition",
|
||||
fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(formKey)))
|
||||
} else {
|
||||
h.Set("Content-Disposition",
|
||||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
||||
escapeQuotes(formKey), escapeQuotes(formFileName)))
|
||||
}
|
||||
part, err := w.Writer.CreatePart(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = part.Write([]byte(formValue))
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *TFormDataWriter) writeCustomFile(formKey, formValue, formType, formFileName string) error {
|
||||
if w.Writer == nil {
|
||||
return errors.New("form-data writer not initialized")
|
||||
}
|
||||
fPath, err := filepath.Abs(formValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if formType == "" {
|
||||
formType = inferFormType(formValue)
|
||||
}
|
||||
if formFileName == "" {
|
||||
formFileName = filepath.Base(formValue)
|
||||
}
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Type", formType)
|
||||
h.Set("Content-Disposition",
|
||||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
||||
escapeQuotes(formKey), escapeQuotes(formFileName)))
|
||||
part, err := w.Writer.CreatePart(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = part.Write(file)
|
||||
return err
|
||||
}
|
||||
|
||||
func inferFormType(formValue string) string {
|
||||
extName := filepath.Ext(formValue)
|
||||
formType := mime.TypeByExtension(extName)
|
||||
if formType == "" {
|
||||
// file without extension name
|
||||
return "application/octet-stream"
|
||||
}
|
||||
if strings.HasPrefix(formType, "text") {
|
||||
// text/... types have the charset parameter set to "utf-8" by default.
|
||||
return strings.TrimSuffix(formType, "; charset=utf-8")
|
||||
}
|
||||
return formType
|
||||
}
|
||||
|
||||
func multipartEncoder(formMap map[string]interface{}) (*TFormDataWriter, error) {
|
||||
payload := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(payload)
|
||||
tFormWriter := &TFormDataWriter{
|
||||
Writer: writer,
|
||||
Payload: payload,
|
||||
}
|
||||
// e.g. formMap: {"file": "@\"$upload_file\";type=text/foo"}
|
||||
for formKey, formData := range formMap {
|
||||
formDataString := fmt.Sprintf("%v", formData)
|
||||
formItems := strings.Split(formDataString, ";")
|
||||
var isFilePath bool
|
||||
var formValue, formType, formFileName string
|
||||
for _, formItem := range formItems {
|
||||
if formItem == "" {
|
||||
continue
|
||||
}
|
||||
equalSignIndex := strings.Index(formItem, "=")
|
||||
// parse form value, e.g. @\"$upload_file\"
|
||||
if equalSignIndex == -1 {
|
||||
matchRes := regexUploadFilePath.FindStringSubmatch(formItem)
|
||||
if len(matchRes) > 1 {
|
||||
// formItem started with @, regarded as File path
|
||||
isFilePath = true
|
||||
formValue = strings.Trim(matchRes[1], "\"")
|
||||
} else {
|
||||
// formItem is not a valid File path, regarded as Text instead
|
||||
formValue = strings.TrimSuffix(strings.TrimPrefix(formItem, "\""), "\"")
|
||||
}
|
||||
continue
|
||||
}
|
||||
// parse form option, e.g. type=text/plain
|
||||
leftPart := strings.TrimSpace(formItem[:equalSignIndex])
|
||||
var rightPart string
|
||||
if equalSignIndex < len(formItem)-1 {
|
||||
rightPart = strings.TrimSpace(formItem[equalSignIndex+1:])
|
||||
}
|
||||
if (strings.ToLower(leftPart) != "type" && strings.ToLower(leftPart) != "filename") || rightPart == "" {
|
||||
formOption := fmt.Sprintf("%s=%s", leftPart, rightPart)
|
||||
log.Warn().Msgf("invalid form option: %v, ignore", formOption)
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(leftPart) == "type" {
|
||||
formType = rightPart
|
||||
}
|
||||
if strings.ToLower(leftPart) == "filename" {
|
||||
formFileName = rightPart
|
||||
}
|
||||
}
|
||||
if isFilePath {
|
||||
if err := tFormWriter.writeCustomFile(formKey, formValue, formType, formFileName); err != nil {
|
||||
log.Error().Err(err).Msgf("failed to write file: %v=@\"%v\", exit", formKey, formValue)
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := tFormWriter.writeCustomText(formKey, formValue, formType, formFileName); err != nil {
|
||||
log.Error().Err(err).Msgf("failed to write text: %v=%v, ignore", formKey, formValue)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to close form-data writer")
|
||||
}
|
||||
return tFormWriter, nil
|
||||
}
|
||||
|
||||
func multipartContentType(w *TFormDataWriter) string {
|
||||
if w.Writer == nil {
|
||||
return ""
|
||||
}
|
||||
return w.Writer.FormDataContentType()
|
||||
}
|
||||
|
||||
func splitByComma(s string) []string {
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
584
internal/builtin/utils.go
Normal file
584
internal/builtin/utils.go
Normal file
@@ -0,0 +1,584 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/csv"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/code"
|
||||
"github.com/httprunner/httprunner/v5/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")
|
||||
|
||||
// init json encoder
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
err = encoder.Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, buffer.Bytes(), 0o644)
|
||||
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(), 0o644)
|
||||
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 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
|
||||
}
|
||||
|
||||
// IsPathExists 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
|
||||
}
|
||||
|
||||
// IsFolderPathExists returns true if path exists and path is folder
|
||||
func IsFolderPathExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// path not exists
|
||||
return false
|
||||
}
|
||||
|
||||
// path exists and is dir
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
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 v := i.(type) {
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case int32:
|
||||
return float64(v), nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
case float32:
|
||||
return float64(v), nil
|
||||
case float64:
|
||||
return v, nil
|
||||
case string:
|
||||
floatVar, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return floatVar, err
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
if ok {
|
||||
return value.Float64()
|
||||
}
|
||||
return 0, errors.New("failed to convert interface to float64")
|
||||
}
|
||||
|
||||
func TypeNormalization(raw interface{}) interface{} {
|
||||
switch v := raw.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
return reflect.ValueOf(v).Int()
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return reflect.ValueOf(v).Uint()
|
||||
case float32, float64:
|
||||
return reflect.ValueOf(v).Float()
|
||||
default:
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
func InterfaceType(raw interface{}) string {
|
||||
if raw == nil {
|
||||
return ""
|
||||
}
|
||||
return reflect.TypeOf(raw).String()
|
||||
}
|
||||
|
||||
func loadFromCSV(path string) []map[string]interface{} {
|
||||
log.Info().Str("path", path).Msg("load csv file")
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read csv file failed")
|
||||
os.Exit(code.GetErrorCode(err))
|
||||
}
|
||||
|
||||
r := csv.NewReader(strings.NewReader(string(file)))
|
||||
content, err := r.ReadAll()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("parse csv file failed")
|
||||
os.Exit(code.GetErrorCode(err))
|
||||
}
|
||||
firstLine := content[0] // parameter names
|
||||
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[firstLine[j]] = content[i][j]
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadMessage(path string) []byte {
|
||||
log.Info().Str("path", path).Msg("load message file")
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read message file failed")
|
||||
os.Exit(code.GetErrorCode(err))
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func GetFileNameWithoutExtension(path string) string {
|
||||
base := filepath.Base(path)
|
||||
ext := filepath.Ext(base)
|
||||
return base[0 : len(base)-len(ext)]
|
||||
}
|
||||
|
||||
func sha256HMAC(key []byte, data []byte) []byte {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(data)
|
||||
return []byte(fmt.Sprintf("%x", mac.Sum(nil)))
|
||||
}
|
||||
|
||||
// ver: auth-v1 or auth-v2
|
||||
func Sign(ver string, ak string, sk string, body []byte) string {
|
||||
expiration := 1800
|
||||
signKeyInfo := fmt.Sprintf("%s/%s/%d/%d", ver, ak, time.Now().Unix(), expiration)
|
||||
signKey := sha256HMAC([]byte(sk), []byte(signKeyInfo))
|
||||
signResult := sha256HMAC(signKey, body)
|
||||
return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult))
|
||||
}
|
||||
|
||||
func GenNameWithTimestamp(tmpl string) string {
|
||||
if !strings.Contains(tmpl, "%d") {
|
||||
tmpl = tmpl + "_%d"
|
||||
}
|
||||
return fmt.Sprintf(tmpl, time.Now().Unix())
|
||||
}
|
||||
|
||||
func IsZeroFloat64(f float64) bool {
|
||||
threshold := 1e-9
|
||||
return math.Abs(f) < threshold
|
||||
}
|
||||
|
||||
func ConvertToFloat64(val interface{}) (float64, error) {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("value", v).
|
||||
Msg("convert string to float64 failed")
|
||||
return 0, err
|
||||
}
|
||||
return f, nil
|
||||
default:
|
||||
log.Error().Interface("value", val).Type("type", val).
|
||||
Msg("convert float64 failed")
|
||||
return 0, errors.New("convert float64 error")
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertToFloat64Slice(val interface{}) ([]float64, error) {
|
||||
if paramsSlice, ok := val.([]float64); ok {
|
||||
return paramsSlice, nil
|
||||
}
|
||||
paramsSlice, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("val is not slice")
|
||||
}
|
||||
|
||||
var err error
|
||||
float64Slice := make([]float64, len(paramsSlice))
|
||||
for i, v := range paramsSlice {
|
||||
float64Slice[i], err = ConvertToFloat64(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return float64Slice, nil
|
||||
}
|
||||
|
||||
func ConvertToStringSlice(val interface{}) ([]string, error) {
|
||||
paramsSlice, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("val is not slice")
|
||||
}
|
||||
|
||||
stringSlice := make([]string, len(paramsSlice))
|
||||
for i, v := range paramsSlice {
|
||||
stringSlice[i], ok = v.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("val is not string slice")
|
||||
}
|
||||
}
|
||||
return stringSlice, nil
|
||||
}
|
||||
|
||||
func GetFreePort() (int, error) {
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "resolve tcp addr failed")
|
||||
}
|
||||
|
||||
l, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "listen tcp addr failed")
|
||||
}
|
||||
defer func() {
|
||||
if err = l.Close(); err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("close addr %s error", l.Addr().String()))
|
||||
}
|
||||
}()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func GetCurrentDay() string {
|
||||
now := time.Now()
|
||||
// 格式化日期为 yyyyMMdd
|
||||
formattedDate := now.Format("20060102")
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
func DownloadFile(filePath string, fileUrl string) error {
|
||||
log.Info().Str("filePath", filePath).Str("url", fileUrl).Msg("download file")
|
||||
parsedURL, err := url.Parse(fileUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 创建一个新的 HTTP 请求
|
||||
req, err := http.NewRequest("GET", fileUrl, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: rename token
|
||||
eapiToken := os.Getenv("EAPI_TOKEN")
|
||||
if eapiToken != "" {
|
||||
if parsedURL.Host != "gtf-eapi-cn.bytedance.com" && parsedURL.Host != "gtf-eapi-cn.bytedance.net" {
|
||||
return errors.New("invalid domain: must be gtf-eapi-cn.bytedance.com")
|
||||
}
|
||||
// 添加自定义头部
|
||||
req.Header.Add("accessKey", "ies.vedem.video")
|
||||
req.Header.Add("token", eapiToken)
|
||||
}
|
||||
|
||||
// 创建一个 HTTP 客户端并发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s, download failed", resp.Status)
|
||||
}
|
||||
|
||||
// 将响应主体写入文件
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(filepath string) bool {
|
||||
_, err := os.Stat(filepath)
|
||||
if os.IsNotExist(err) {
|
||||
return false // 文件不存在
|
||||
}
|
||||
return err == nil // 文件存在,且没有其他错误
|
||||
}
|
||||
|
||||
func DownloadFileByUrl(fileUrl string) (filePath string, err error) {
|
||||
// 使用 UUID 生成唯一文件名
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := md5.Sum([]byte(fileUrl))
|
||||
fileName := fmt.Sprintf("%x", hash)
|
||||
filePath = filepath.Join(cwd, fileName)
|
||||
locker.Lock(filePath)
|
||||
defer locker.Unlock(filePath)
|
||||
if fileExists(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading file to %s from URL %s\n", filePath, fileUrl)
|
||||
|
||||
// Create an HTTP client with default settings.
|
||||
client := &http.Client{}
|
||||
|
||||
// Build the HTTP GET request.
|
||||
req, err := http.NewRequest("GET", fileUrl, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Perform the request.
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the HTTP status code.
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to download file: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Create the output file.
|
||||
outFile, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Copy the response body to the file.
|
||||
_, err = io.Copy(outFile, resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Printf("File downloaded successfully: %s\n", fileName)
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func RunCommand(cmdName string, args ...string) error {
|
||||
cmd := exec.Command(cmdName, args...)
|
||||
log.Info().Str("command", cmd.String()).Msg("exec command")
|
||||
|
||||
// print stderr output
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
stderrStr := stderr.String()
|
||||
log.Error().Err(err).Msg("failed to exec command. msg: " + stderrStr)
|
||||
if stderrStr != "" {
|
||||
err = errors.Wrap(err, stderrStr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
stderrStr := stderr.String()
|
||||
log.Error().Msg("failed to exec command. msg: " + stderrStr)
|
||||
log.Info().Msg("exec command output: " + stdout.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
type LineCallback func(line string) bool
|
||||
|
||||
// RunCommandWithCallback 运行命令并根据回调判断是否成功
|
||||
func RunCommandWithCallback(cmdName string, args []string, callback LineCallback) error {
|
||||
cmd := exec.Command(cmdName, args...)
|
||||
log.Info().Str("command", cmd.String()).Msg("exec command")
|
||||
|
||||
// 使用管道获取标准输出
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get stdout pipe")
|
||||
return err
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to start command")
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建一个用于标识成功的通道
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
// 逐行读取 stdout
|
||||
go func() {
|
||||
stdoutScanner := bufio.NewScanner(stdoutPipe)
|
||||
for stdoutScanner.Scan() {
|
||||
line := stdoutScanner.Text()
|
||||
log.Info().Msg("stdout: " + line)
|
||||
if callback(line) {
|
||||
done <- struct{}{}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待命令执行完成
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
log.Error().Msg("failed to exec command. msg: " + stderr.String())
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置一个1秒的超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
// 超时,判断失败
|
||||
log.Error().Msg("failed to exec command. msg: " + stderr.String())
|
||||
err = errors.New("command execution failed: callback failed while exec command")
|
||||
log.Error().Err(err).Msg("failed to find keyword in time")
|
||||
return err
|
||||
}
|
||||
}
|
||||
38
internal/config/config.go
Normal file
38
internal/config/config.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ResultsDirName = "results"
|
||||
ScreenshotsDirName = "screenshots"
|
||||
ActionLogDireName = "action_log"
|
||||
)
|
||||
|
||||
var (
|
||||
RootDir string
|
||||
ResultsDir string
|
||||
ResultsPath string
|
||||
ScreenShotsPath string
|
||||
StartTime = time.Now()
|
||||
StartTimeStr = StartTime.Format("20060102150405")
|
||||
ActionLogFilePath string
|
||||
DeviceActionLogFilePath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
RootDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ResultsDir = filepath.Join(ResultsDirName, StartTimeStr)
|
||||
ResultsPath = filepath.Join(RootDir, ResultsDir)
|
||||
ScreenShotsPath = filepath.Join(ResultsPath, ScreenshotsDirName)
|
||||
ActionLogFilePath = filepath.Join(ResultsDir, ActionLogDireName)
|
||||
DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
|
||||
}
|
||||
17
internal/json/json.go
Normal file
17
internal/json/json.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// replace with third-party json library to improve performance
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
var (
|
||||
Marshal = json.Marshal
|
||||
MarshalIndent = json.MarshalIndent
|
||||
Unmarshal = json.Unmarshal
|
||||
NewDecoder = json.NewDecoder
|
||||
NewEncoder = json.NewEncoder
|
||||
Get = json.Get
|
||||
)
|
||||
10
internal/pytest/main.go
Normal file
10
internal/pytest/main.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package pytest
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
)
|
||||
|
||||
func RunPytest(args []string) error {
|
||||
args = append([]string{"run"}, args...)
|
||||
return myexec.ExecPython3Command("httprunner", args...)
|
||||
}
|
||||
34
internal/scaffold/examples_test.go
Normal file
34
internal/scaffold/examples_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenDemoExamples(t *testing.T) {
|
||||
dir := "../../../examples/demo-with-go-plugin"
|
||||
err := CreateScaffold(dir, Go, "", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dir = "../../../examples/demo-with-py-plugin"
|
||||
venv := filepath.Join(dir, ".venv")
|
||||
_ = CreateScaffold(dir, Py, venv, true)
|
||||
// FIXME
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
dir = "../../../examples/demo-without-plugin"
|
||||
err = CreateScaffold(dir, Ignore, "", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dir = "../../../examples/demo-empty-project"
|
||||
err = CreateScaffold(dir, Empty, "", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
220
internal/scaffold/main.go
Normal file
220
internal/scaffold/main.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/code"
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
)
|
||||
|
||||
type PluginType string
|
||||
|
||||
const (
|
||||
Empty PluginType = "empty"
|
||||
Ignore PluginType = "ignore"
|
||||
Py PluginType = "py"
|
||||
Go PluginType = "go"
|
||||
)
|
||||
|
||||
type ProjectInfo struct {
|
||||
ProjectName string `json:"project_name,omitempty" yaml:"project_name,omitempty"`
|
||||
CreateTime time.Time `json:"create_time,omitempty" yaml:"create_time,omitempty"`
|
||||
Version string `json:"hrp_version,omitempty" yaml:"hrp_version,omitempty"`
|
||||
}
|
||||
|
||||
//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, venv string, force bool) error {
|
||||
// report GA event
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_startproject", map[string]interface{}{
|
||||
"pluginType": string(pluginType),
|
||||
"force": force,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
log.Info().
|
||||
Str("projectName", projectName).
|
||||
Str("pluginType", string(pluginType)).
|
||||
Bool("force", force).
|
||||
Msg("create new scaffold project")
|
||||
|
||||
// check if projectName exists
|
||||
if _, err := os.Stat(projectName); err == nil {
|
||||
if !force {
|
||||
log.Warn().Str("projectName", projectName).
|
||||
Msg("project name already exists, please specify a new one.")
|
||||
return fmt.Errorf("project name already exists")
|
||||
}
|
||||
|
||||
log.Warn().Str("projectName", projectName).
|
||||
Msg("project name already exists, remove first !!!")
|
||||
os.RemoveAll(projectName)
|
||||
}
|
||||
|
||||
// create project folders
|
||||
if err := builtin.CreateFolder(projectName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, "har")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFile(filepath.Join(projectName, "har", ".keep"), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, "testcases")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFolder(filepath.Join(projectName, config.ResultsDirName)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := builtin.CreateFile(filepath.Join(projectName, config.ResultsDirName, ".keep"), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectInfo := &ProjectInfo{
|
||||
ProjectName: filepath.Base(projectName),
|
||||
CreateTime: time.Now(),
|
||||
Version: version.VERSION,
|
||||
}
|
||||
|
||||
// dump project information to file
|
||||
err := builtin.Dump2JSON(projectInfo, filepath.Join(projectName, "proj.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create .gitignore
|
||||
err = CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// create .env
|
||||
err = CopyFile("templates/env", filepath.Join(projectName, ".env"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create project testcases
|
||||
if pluginType == Empty {
|
||||
// create empty project
|
||||
err := CopyFile("templates/testcases/demo_empty_request.json",
|
||||
filepath.Join(projectName, "testcases", "requests.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if pluginType == Ignore {
|
||||
// create project without funplugin
|
||||
err := CopyFile("templates/testcases/demo_without_funplugin.json",
|
||||
filepath.Join(projectName, "testcases", "requests.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info().Msg("skip creating function plugin")
|
||||
return nil
|
||||
}
|
||||
|
||||
// create project with funplugin
|
||||
err = CopyFile("templates/testcases/demo_with_funplugin.json",
|
||||
filepath.Join(projectName, "testcases", "demo.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyFile("templates/testcases/demo_requests.json",
|
||||
filepath.Join(projectName, "testcases", "requests.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyFile("templates/testcases/demo_requests.yml",
|
||||
filepath.Join(projectName, "testcases", "requests.yml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyFile("templates/testcases/demo_ref_testcase.yml",
|
||||
filepath.Join(projectName, "testcases", "ref_testcase.yml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create debugtalk function plugin
|
||||
switch pluginType {
|
||||
case Py:
|
||||
return createPythonPlugin(projectName, venv)
|
||||
case Go:
|
||||
return createGoPlugin(projectName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createGoPlugin(projectName string) error {
|
||||
log.Info().Msg("start to create hashicorp go plugin")
|
||||
// check go sdk
|
||||
if err := myexec.RunCommand("go", "version"); err != nil {
|
||||
return errors.Wrap(err, "go sdk not installed")
|
||||
}
|
||||
|
||||
// create debugtalk.go
|
||||
pluginDir := filepath.Join(projectName, "plugin")
|
||||
if err := builtin.CreateFolder(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
err := CopyFile("templates/plugin/debugtalk.go",
|
||||
filepath.Join(projectName, "plugin", hrp.PluginGoSourceFile))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "copy debugtalk.go failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createPythonPlugin(projectName, venv string) error {
|
||||
log.Info().Msg("start to create hashicorp python plugin")
|
||||
|
||||
// create debugtalk.py
|
||||
pluginFile := filepath.Join(projectName, hrp.PluginPySourceFile)
|
||||
err := CopyFile("templates/plugin/debugtalk.py", pluginFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "copy file failed")
|
||||
}
|
||||
|
||||
packages := []string{"funppy", "httprunner"}
|
||||
_, err = myexec.EnsurePython3Venv(venv, packages...)
|
||||
if err != nil {
|
||||
return errors.Wrap(code.InvalidPython3Venv, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
34
internal/scaffold/templates/api/get.json
Normal file
34
internal/scaffold/templates/api/get.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "bar1",
|
||||
"foo2": "bar2"
|
||||
},
|
||||
"headers": {
|
||||
"Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/get?foo1=bar1&foo2=bar2",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
internal/scaffold/templates/api/get.yml
Normal file
22
internal/scaffold/templates/api/get.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
name: ""
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: bar1
|
||||
foo2: bar2
|
||||
headers:
|
||||
Postman-Token: ea19464c-ddd4-4724-abe9-5e2b254c2723
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/get?foo1=bar1&foo2=bar2
|
||||
msg: assert response body url
|
||||
45
internal/scaffold/templates/api/post.json
Normal file
45
internal/scaffold/templates/api/post.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Length": "58",
|
||||
"Content-Type": "text/plain",
|
||||
"Postman-Token": "$session_token"
|
||||
},
|
||||
"body": "This is expected to be sent back as part of response body."
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equals",
|
||||
"expect": "This is expected to be sent back as part of response body.",
|
||||
"msg": "assert response body data"
|
||||
},
|
||||
{
|
||||
"check": "body.json",
|
||||
"assert": "equals",
|
||||
"expect": null,
|
||||
"msg": "assert response body json"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/post/",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
||||
30
internal/scaffold/templates/api/post.yml
Normal file
30
internal/scaffold/templates/api/post.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: ""
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
Content-Length: "58"
|
||||
Content-Type: text/plain
|
||||
Postman-Token: $session_token
|
||||
body: This is expected to be sent back as part of response body.
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.data
|
||||
assert: equals
|
||||
expect: This is expected to be sent back as part of response body.
|
||||
msg: assert response body data
|
||||
- check: body.json
|
||||
assert: equals
|
||||
expect: null
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/post/
|
||||
msg: assert response body url
|
||||
45
internal/scaffold/templates/api/put.json
Normal file
45
internal/scaffold/templates/api/put.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "/put",
|
||||
"headers": {
|
||||
"Content-Length": "58",
|
||||
"Content-Type": "text/plain",
|
||||
"Postman-Token": "5d357b2b-0f10-4ded-bc9a-299ebef7a2d5"
|
||||
},
|
||||
"body": "This is expected to be sent back as part of response body."
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equals",
|
||||
"expect": "This is expected to be sent back as part of response body.",
|
||||
"msg": "assert response body data"
|
||||
},
|
||||
{
|
||||
"check": "body.json",
|
||||
"assert": "equals",
|
||||
"expect": null,
|
||||
"msg": "assert response body json"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/put/",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
||||
30
internal/scaffold/templates/api/put.yml
Normal file
30
internal/scaffold/templates/api/put.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: ""
|
||||
request:
|
||||
method: PUT
|
||||
url: /put
|
||||
headers:
|
||||
Content-Length: "58"
|
||||
Content-Type: text/plain
|
||||
Postman-Token: 5d357b2b-0f10-4ded-bc9a-299ebef7a2d5
|
||||
body: This is expected to be sent back as part of response body.
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.data
|
||||
assert: equals
|
||||
expect: This is expected to be sent back as part of response body.
|
||||
msg: assert response body data
|
||||
- check: body.json
|
||||
assert: equals
|
||||
expect: null
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/put/
|
||||
msg: assert response body url
|
||||
3
internal/scaffold/templates/env
Normal file
3
internal/scaffold/templates/env
Normal file
@@ -0,0 +1,3 @@
|
||||
base_url=https://postman-echo.com
|
||||
USERNAME=debugtalk
|
||||
PASSWORD=123456
|
||||
14
internal/scaffold/templates/gitignore
Normal file
14
internal/scaffold/templates/gitignore
Normal file
@@ -0,0 +1,14 @@
|
||||
reports/
|
||||
*.so
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
output/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.python-version
|
||||
logs/
|
||||
|
||||
# plugin
|
||||
debugtalk.bin
|
||||
debugtalk.so
|
||||
24
internal/scaffold/templates/plugin/.debugtalk_gen.py
Normal file
24
internal/scaffold/templates/plugin/.debugtalk_gen.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# NOTE: Generated By hrp v4.3.6, DO NOT EDIT!
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from debugtalk import *
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import funppy
|
||||
|
||||
funppy.register("get_user_agent", get_user_agent)
|
||||
funppy.register("sleep", sleep)
|
||||
funppy.register("sum", sum)
|
||||
funppy.register("sum_ints", sum_ints)
|
||||
funppy.register("sum_two_int", sum_two_int)
|
||||
funppy.register("sum_two_string", sum_two_string)
|
||||
funppy.register("sum_strings", sum_strings)
|
||||
funppy.register("concatenate", concatenate)
|
||||
funppy.register("setup_hook_example", setup_hook_example)
|
||||
funppy.register("teardown_hook_example", teardown_hook_example)
|
||||
funppy.serve()
|
||||
44
internal/scaffold/templates/plugin/debugtalk.go
Normal file
44
internal/scaffold/templates/plugin/debugtalk.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func SumTwoInt(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func SumInts(args ...int) int {
|
||||
var sum int
|
||||
for _, arg := range args {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func Sum(args ...interface{}) (interface{}, error) {
|
||||
var sum float64
|
||||
for _, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case int:
|
||||
sum += float64(v)
|
||||
case float64:
|
||||
sum += v
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type: %T", arg)
|
||||
}
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func SetupHookExample(args string) string {
|
||||
return fmt.Sprintf("step name: %v, setup...", args)
|
||||
}
|
||||
|
||||
func TeardownHookExample(args string) string {
|
||||
return fmt.Sprintf("step name: %v, teardown...", args)
|
||||
}
|
||||
|
||||
func GetUserAgent() string {
|
||||
return "hrp/fungo"
|
||||
}
|
||||
62
internal/scaffold/templates/plugin/debugtalk.py
Normal file
62
internal/scaffold/templates/plugin/debugtalk.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
|
||||
# commented out function will be filtered
|
||||
# def get_headers():
|
||||
# return {"User-Agent": "hrp"}
|
||||
|
||||
|
||||
def get_user_agent():
|
||||
return "hrp/funppy"
|
||||
|
||||
|
||||
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}"
|
||||
13
internal/scaffold/templates/plugin/debugtalkGoTemplate
Normal file
13
internal/scaffold/templates/plugin/debugtalkGoTemplate
Normal file
@@ -0,0 +1,13 @@
|
||||
// NOTE: Generated By hrp {{ .Version }}, DO NOT EDIT!
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
{{- range $functionName := .FunctionNames }}
|
||||
fungo.Register("{{ $functionName }}", {{ $functionName }})
|
||||
{{- end }}
|
||||
fungo.Serve()
|
||||
}
|
||||
16
internal/scaffold/templates/plugin/debugtalkPythonTemplate
Normal file
16
internal/scaffold/templates/plugin/debugtalkPythonTemplate
Normal file
@@ -0,0 +1,16 @@
|
||||
# NOTE: Generated By hrp {{ .Version }}, DO NOT EDIT!
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from debugtalk import *
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import funppy
|
||||
{{- range $functionName := .FunctionNames }}
|
||||
funppy.register("{{ $functionName }}", {{ $functionName }})
|
||||
{{- end }}
|
||||
funppy.serve()
|
||||
16
internal/scaffold/templates/plugin/debugtalk_gen.go
Normal file
16
internal/scaffold/templates/plugin/debugtalk_gen.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// NOTE: Generated By hrp v4.3.6, DO NOT EDIT!
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fungo.Register("SumTwoInt", SumTwoInt)
|
||||
fungo.Register("SumInts", SumInts)
|
||||
fungo.Register("Sum", Sum)
|
||||
fungo.Register("SetupHookExample", SetupHookExample)
|
||||
fungo.Register("TeardownHookExample", TeardownHookExample)
|
||||
fungo.Register("GetUserAgent", GetUserAgent)
|
||||
fungo.Serve()
|
||||
}
|
||||
6
internal/scaffold/templates/pytest.ini
Normal file
6
internal/scaffold/templates/pytest.ini
Normal file
@@ -0,0 +1,6 @@
|
||||
[pytest]
|
||||
addopts = -s
|
||||
# https://docs.pytest.org/en/latest/how-to/output.html
|
||||
junit_logging = all
|
||||
junit_duration_report = total
|
||||
log_cli = False
|
||||
359
internal/scaffold/templates/report/template.html
Normal file
359
internal/scaffold/templates/report/template.html
Normal file
@@ -0,0 +1,359 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestReport</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
color: #333;
|
||||
margin: 0 auto;
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
#summary {
|
||||
width: 960px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#summary th {
|
||||
background-color: skyblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
#summary td {
|
||||
background-color: lightblue;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 960px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details th {
|
||||
background-color: skyblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.details tr .passed {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.details tr .failed {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.details tr .unchecked {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.details td {
|
||||
background-color: lightblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.details .detail {
|
||||
background-color: lightgrey;
|
||||
font-size: smaller;
|
||||
padding: 5px 10px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.details .success {
|
||||
background-color: greenyellow;
|
||||
}
|
||||
|
||||
.details .error {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.details .failure {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
.details .skipped {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 1em;
|
||||
padding: 6px;
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
background-color: #06d85f;
|
||||
border-radius: 20px/50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
a.button {
|
||||
color: gray;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #2cffbd;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transition: opacity 500ms;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.overlay:target {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.popup {
|
||||
margin: 70px auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 50%;
|
||||
position: relative;
|
||||
transition: all 3s ease-in-out;
|
||||
}
|
||||
|
||||
.popup h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-family: Tahoma, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.popup .close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
transition: all 200ms;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popup .close:hover {
|
||||
color: #06d85f;
|
||||
}
|
||||
|
||||
.popup .content {
|
||||
max-height: 80%;
|
||||
overflow: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.popup .separator {
|
||||
color: royalblue
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.box {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>API Test Report</h1>
|
||||
|
||||
<h2>Summary</h2>
|
||||
<table id="summary">
|
||||
<tr>
|
||||
<th>START AT</th>
|
||||
<td colspan="4">{{.Time.StartAt}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DURATION</th>
|
||||
<td colspan="4">{{ .Time.Duration }} seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>PLATFORM</th>
|
||||
<td>HttpRunnerPlus {{ .Platform.HttprunnerVersion }}</td>
|
||||
<td>{{ .Platform.GoVersion }}</td>
|
||||
<td colspan="2">{{ .Platform.Platform }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>STAT</th>
|
||||
<th colspan="2">TESTCASES (success/fail)</th>
|
||||
<th colspan="2">TESTSTEPS (success/fail/error/skip)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>total (details) =></td>
|
||||
<td colspan="2">{{.Stat.TestCases.Total}} ({{.Stat.TestCases.Success}}/{{.Stat.TestCases.Fail}})</td>
|
||||
<td colspan="2">{{.Stat.TestSteps.Total}} ({{.Stat.TestSteps.Successes}}/0/{{.Stat.TestSteps.Failures}}/0)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Details</h2>
|
||||
{{ range $suite_index, $detail := .Details }}
|
||||
<h3>{{.Name}}</h3>
|
||||
<table id="suite_{{$suite_index}}" class="details">
|
||||
<tr>
|
||||
<td>TOTAL: {{.Stat.Total}}</td>
|
||||
<td>SUCCESS: {{.Stat.Successes}}</td>
|
||||
<td>FAILED: 0</td>
|
||||
<td>ERROR: {{.Stat.Failures}}</td>
|
||||
<td>SKIPPED: 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th colspan="2">Name</th>
|
||||
<th>Response Time</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
{{- range $loop_index, $record := .Records }}
|
||||
{{- with $record}}
|
||||
{{- $status := "error"}}
|
||||
{{- if .Success }} {{ $status = "success" }} {{ end }}
|
||||
<tr id="record_{{$suite_index}}_{{$loop_index}}">
|
||||
<th class={{$status}} style="width:5em;">{{$status}}</th>
|
||||
<td colspan="2">{{.Name}}</td>
|
||||
<td style="text-align:center;width:6em;">{{ .Elapsed }} ms</td>
|
||||
<td class="detail">
|
||||
<a class="button" href="#popup_log_{{$suite_index}}_{{$loop_index}}">log</a>
|
||||
<div id="popup_log_{{$suite_index}}_{{$loop_index}}" class="overlay">
|
||||
<div class="popup">
|
||||
<h2>Request and Response data</h2>
|
||||
<a class="close" href="#record_{{$suite_index}}_{{$loop_index}}">×</a>
|
||||
<div class="content">
|
||||
<h3>Name: {{ .Name }}</h3>
|
||||
{{- if .Data}}
|
||||
<h3>Request:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
{{- range $key, $value := .Data.ReqResps.Request}}
|
||||
<tr>
|
||||
<th>{{$key}}</th>
|
||||
<td align="left">
|
||||
{{- if eq $key "headers" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "params" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "cookies" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else }}
|
||||
<pre>{{$value}}</pre>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</div>
|
||||
<h3>Response:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
{{- range $key, $value := .Data.ReqResps.Response}}
|
||||
<tr>
|
||||
<th>{{$key}}</th>
|
||||
<td align="left">
|
||||
{{- if eq $key "headers" }}
|
||||
{{- range $k, $v := $value}}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "cookies" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else }}
|
||||
<pre>{{ $value }}</pre>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Validators:</h3>
|
||||
<div style="overflow: auto">
|
||||
{{- if .Data.Validators }}
|
||||
<table>
|
||||
<tr>
|
||||
<th>check</th>
|
||||
<th>comparator</th>
|
||||
<th>expect value</th>
|
||||
<th>actual value</th>
|
||||
</tr>
|
||||
{{- range $validator := .Data.Validators }}
|
||||
<tr>
|
||||
{{- if eq $validator.CheckResult "pass" }}
|
||||
<td class="passed">
|
||||
{{- else if eq $validator.CheckResult "fail" }}
|
||||
<td class="failed">
|
||||
{{- else if eq $validator.CheckResult "unchecked" }}
|
||||
<td class="unchecked">
|
||||
{{- end }}
|
||||
{{$validator.Check}}
|
||||
</td>
|
||||
<td>{{$validator.Assert}}</td>
|
||||
<td>{{$validator.Expect}}</td>
|
||||
<td>{{$validator.CheckValue}}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
{{- end }}
|
||||
|
||||
<h3>Statistics:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
<tr>
|
||||
<th>content_size(bytes)</th>
|
||||
<td>{{ .ContentSize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>response_time(ms)</th>
|
||||
<td>{{ .Elapsed }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>elapsed(ms)</th>
|
||||
<td>{{ .Elapsed }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Attachments }}
|
||||
<a class="button" href="#popup_attachment_{{$suite_index}}_{{$loop_index}}">traceback</a>
|
||||
<div id="popup_attachment_{{$suite_index}}_{{$loop_index}}" class="overlay">
|
||||
<div class="popup">
|
||||
<h2>Traceback Message</h2>
|
||||
<a class="close" href="#record_{{$suite_index}}_{{$loop_index}}">×</a>
|
||||
<div class="content">
|
||||
<pre>{{ .Attachments }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</table>
|
||||
{{- end }}
|
||||
</body>
|
||||
1
internal/scaffold/templates/testcases/__init__.py
Normal file
1
internal/scaffold/templates/testcases/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "request methods testcase: empty testcase",
|
||||
"variables": null,
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "",
|
||||
"variables": null,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equal",
|
||||
"expect": 200,
|
||||
"msg": "check status_code"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
internal/scaffold/templates/testcases/demo_empty_request.yml
Normal file
13
internal/scaffold/templates/testcases/demo_empty_request.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
config:
|
||||
name: "request methods testcase: empty testcase"
|
||||
variables:
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
- name:
|
||||
variables:
|
||||
request:
|
||||
method: GET
|
||||
url: "https://"
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
76
internal/scaffold/templates/testcases/demo_ref_api.json
Normal file
76
internal/scaffold/templates/testcases/demo_ref_api.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "api test demo",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.3",
|
||||
"device_sn": "TESTCASE_SETUP_XXX",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.6"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "PostmanRuntime/7.28.4"
|
||||
},
|
||||
"verify": false,
|
||||
"export": [
|
||||
"session_token"
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "test api /get",
|
||||
"api": "api/get.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.4",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.7"
|
||||
},
|
||||
"extract": {
|
||||
"session_token": "body.headers.\"postman-token\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test api /post",
|
||||
"api": "api/post.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.5",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.9"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equal",
|
||||
"expect": 200,
|
||||
"msg": "check status_code"
|
||||
},
|
||||
{
|
||||
"check": "body.headers.\"postman-token\"",
|
||||
"assert": "equal",
|
||||
"expect": "ea19464c-ddd4-4724-abe9-5e2b254c2723",
|
||||
"msg": "check body.headers.postman-token"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test api /put",
|
||||
"api": "api/put.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.6",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.10"
|
||||
},
|
||||
"extract": {
|
||||
"session_token": "body.headers.\"postman-token\""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
33
internal/scaffold/templates/testcases/demo_ref_testcase.yml
Normal file
33
internal/scaffold/templates/testcases/demo_ref_testcase.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
config:
|
||||
name: "request methods testcase: reference testcase"
|
||||
variables:
|
||||
foo1: testsuite_config_bar1
|
||||
expect_foo1: testsuite_config_bar1
|
||||
expect_foo2: config_bar2
|
||||
base_url: "https://postman-echo.com"
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
-
|
||||
name: request with functions
|
||||
variables:
|
||||
foo1: testcase_ref_bar1
|
||||
expect_foo1: testcase_ref_bar1
|
||||
testcase: testcases/requests.yml
|
||||
export:
|
||||
- foo3
|
||||
-
|
||||
name: post form data
|
||||
variables:
|
||||
foo1: bar1
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
User-Agent: ${get_user_agent()}
|
||||
Content-Type: "application/x-www-form-urlencoded"
|
||||
body: "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
|
||||
# FROM: testcases/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": "${get_user_agent()}",
|
||||
"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()
|
||||
136
internal/scaffold/templates/testcases/demo_requests.json
Normal file
136
internal/scaffold/templates/testcases/demo_requests.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "request methods testcase with functions",
|
||||
"variables": {
|
||||
"foo1": "config_bar1",
|
||||
"foo2": "config_bar2",
|
||||
"expect_foo1": "config_bar1",
|
||||
"expect_foo2": "config_bar2"
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "${get_user_agent()}"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": false,
|
||||
"export": [
|
||||
"foo3"
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "get with params",
|
||||
"variables": {
|
||||
"foo1": "${ENV(USERNAME)}",
|
||||
"foo2": "bar21",
|
||||
"sum_v": "${sum_two_int(10000000, 20000000)}"
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "$foo1",
|
||||
"foo2": "$foo2",
|
||||
"sum_v": "$sum_v"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"foo3": "body.args.foo2"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equal",
|
||||
"expect": 200,
|
||||
"msg": "check status_code"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo1",
|
||||
"assert": "equal",
|
||||
"expect": "debugtalk",
|
||||
"msg": "check body.args.foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.args.sum_v",
|
||||
"assert": "equal",
|
||||
"expect": "30000000",
|
||||
"msg": "check body.args.sum_v"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo2",
|
||||
"assert": "equal",
|
||||
"expect": "bar21",
|
||||
"msg": "check body.args.foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "post raw text",
|
||||
"variables": {
|
||||
"foo1": "bar12",
|
||||
"foo3": "bar32"
|
||||
},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
"body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equal",
|
||||
"expect": 200,
|
||||
"msg": "check status_code"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equal",
|
||||
"expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
|
||||
"msg": "check body.data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "post form data",
|
||||
"variables": {
|
||||
"foo2": "bar23"
|
||||
},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
"body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equal",
|
||||
"expect": 200,
|
||||
"msg": "check status_code"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo1",
|
||||
"assert": "equal",
|
||||
"expect": "$expect_foo1",
|
||||
"msg": "check body.form.foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo2",
|
||||
"assert": "equal",
|
||||
"expect": "bar23",
|
||||
"msg": "check body.form.foo2"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo3",
|
||||
"assert": "equal",
|
||||
"expect": "bar21",
|
||||
"msg": "check body.form.foo3"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
62
internal/scaffold/templates/testcases/demo_requests.yml
Normal file
62
internal/scaffold/templates/testcases/demo_requests.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
config:
|
||||
name: "request methods testcase with functions"
|
||||
variables:
|
||||
foo1: config_bar1
|
||||
foo2: config_bar2
|
||||
expect_foo1: config_bar1
|
||||
expect_foo2: config_bar2
|
||||
headers:
|
||||
User-Agent: ${get_user_agent()}
|
||||
verify: False
|
||||
export: ["foo3"]
|
||||
|
||||
teststeps:
|
||||
-
|
||||
name: get with params
|
||||
variables:
|
||||
foo1: ${ENV(USERNAME)}
|
||||
foo2: bar21
|
||||
sum_v: "${sum_two_int(10000000, 20000000)}"
|
||||
request:
|
||||
method: GET
|
||||
url: $base_url/get
|
||||
params:
|
||||
foo1: $foo1
|
||||
foo2: $foo2
|
||||
sum_v: $sum_v
|
||||
extract:
|
||||
foo3: "body.args.foo2"
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
- eq: ["body.args.foo1", "debugtalk"]
|
||||
- eq: ["body.args.sum_v", "30000000"]
|
||||
- eq: ["body.args.foo2", "bar21"]
|
||||
-
|
||||
name: post raw text
|
||||
variables:
|
||||
foo1: "bar12"
|
||||
foo3: "bar32"
|
||||
request:
|
||||
method: POST
|
||||
url: $base_url/post
|
||||
headers:
|
||||
Content-Type: "text/plain"
|
||||
body: "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: $base_url/post
|
||||
headers:
|
||||
Content-Type: "application/x-www-form-urlencoded"
|
||||
body: "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"]
|
||||
83
internal/scaffold/templates/testcases/demo_requests_test.py
Normal file
83
internal/scaffold/templates/testcases/demo_requests_test.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# NOTE: Generated By HttpRunner v4.0.0
|
||||
# FROM: testcases/requests.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
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_int(10000000, 20000000)}"}
|
||||
)
|
||||
.get("/get")
|
||||
.with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"})
|
||||
.with_headers(**{"User-Agent": "${get_user_agent()}"})
|
||||
.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", "30000000")
|
||||
.assert_equal("body.args.foo2", "bar21")
|
||||
),
|
||||
Step(
|
||||
RunRequest("post raw text")
|
||||
.with_variables(**{"foo1": "bar12", "foo3": "bar32"})
|
||||
.post("/post")
|
||||
.with_headers(
|
||||
**{
|
||||
"User-Agent": "${get_user_agent()}",
|
||||
"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": "${get_user_agent()}",
|
||||
"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()
|
||||
176
internal/scaffold/templates/testcases/demo_with_funplugin.json
Normal file
176
internal/scaffold/templates/testcases/demo_with_funplugin.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "demo with complex mechanisms",
|
||||
"base_url": "https://postman-echo.com",
|
||||
"variables": {
|
||||
"a": "${sum(10, 2.3)}",
|
||||
"b": 3.45,
|
||||
"n": "${sum_ints(1, 2, 2)}",
|
||||
"varFoo1": "${gen_random_string($n)}",
|
||||
"varFoo2": "${max($a, $b)}"
|
||||
}
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "transaction 1 start",
|
||||
"transaction": {
|
||||
"name": "tran1",
|
||||
"type": "start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "$varFoo2"
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunnerPlus"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"b": 34.5,
|
||||
"n": 3,
|
||||
"name": "get with params",
|
||||
"varFoo2": "${max($a, $b)}"
|
||||
},
|
||||
"setup_hooks": [
|
||||
"${setup_hook_example($name)}"
|
||||
],
|
||||
"teardown_hooks": [
|
||||
"${teardown_hook_example($name)}"
|
||||
],
|
||||
"extract": {
|
||||
"varFoo1": "body.args.foo1"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "startswith",
|
||||
"expect": "application/json"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "$varFoo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo2",
|
||||
"assert": "equals",
|
||||
"expect": "34.5",
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "transaction 1 end",
|
||||
"transaction": {
|
||||
"name": "tran1",
|
||||
"type": "end"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "post json data",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"body": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "${max($a, $b)}"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo2",
|
||||
"assert": "equals",
|
||||
"expect": 12.3,
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "post form data",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
},
|
||||
"body": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "${max($a, $b)}",
|
||||
"time": "${get_timestamp()}"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"varTime": "body.form.time"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo2",
|
||||
"assert": "equals",
|
||||
"expect": "12.3",
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get with timestamp",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"time": "$varTime"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "body.args.time",
|
||||
"assert": "length_equals",
|
||||
"expect": 13,
|
||||
"msg": "check extracted var timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
114
internal/scaffold/templates/testcases/demo_with_funplugin.yaml
Normal file
114
internal/scaffold/templates/testcases/demo_with_funplugin.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
config:
|
||||
name: demo with complex mechanisms
|
||||
base_url: https://postman-echo.com
|
||||
variables:
|
||||
a: ${sum(10, 2.3)}
|
||||
b: 3.45
|
||||
"n": ${sum_ints(1, 2, 2)}
|
||||
varFoo1: ${gen_random_string($n)}
|
||||
varFoo2: ${max($a, $b)}
|
||||
teststeps:
|
||||
- name: transaction 1 start
|
||||
transaction:
|
||||
name: tran1
|
||||
type: start
|
||||
- name: get with params
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: $varFoo1
|
||||
foo2: $varFoo2
|
||||
headers:
|
||||
User-Agent: HttpRunnerPlus
|
||||
variables:
|
||||
b: 34.5
|
||||
"n": 3
|
||||
name: get with params
|
||||
varFoo2: ${max($a, $b)}
|
||||
setup_hooks:
|
||||
- ${setup_hook_example($name)}
|
||||
teardown_hooks:
|
||||
- ${teardown_hook_example($name)}
|
||||
extract:
|
||||
varFoo1: body.args.foo1
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: startswith
|
||||
expect: application/json
|
||||
- check: body.args.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: $varFoo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.args.foo2
|
||||
assert: equals
|
||||
expect: "34.5"
|
||||
msg: check args foo2
|
||||
- name: transaction 1 end
|
||||
transaction:
|
||||
name: tran1
|
||||
type: end
|
||||
- name: post json data
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
body:
|
||||
foo1: $varFoo1
|
||||
foo2: ${max($a, $b)}
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- check: body.json.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.json.foo2
|
||||
assert: equals
|
||||
expect: 12.3
|
||||
msg: check args foo2
|
||||
- name: post form data
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
body:
|
||||
foo1: $varFoo1
|
||||
foo2: ${max($a, $b)}
|
||||
time: ${get_timestamp()}
|
||||
extract:
|
||||
varTime: body.form.time
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- check: body.form.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.form.foo2
|
||||
assert: equals
|
||||
expect: "12.3"
|
||||
msg: check args foo2
|
||||
- name: get with timestamp
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
time: $varTime
|
||||
validate:
|
||||
- check: body.args.time
|
||||
assert: length_equals
|
||||
expect: 13
|
||||
msg: check extracted var timestamp
|
||||
@@ -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
|
||||
211
internal/sdk/ga4.go
Normal file
211
internal/sdk/ga4.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
)
|
||||
|
||||
// Measurement Protocol (Google Analytics 4) docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4
|
||||
// debugging tools: https://ga-dev-tools.google/ga4/event-builder/
|
||||
const (
|
||||
ga4APISecret = "w7lKNQIrQsKNS4ikgMPp0Q"
|
||||
ga4MeasurementID = "G-9KHR3VC2LN"
|
||||
)
|
||||
|
||||
var (
|
||||
ga4Client *GA4Client
|
||||
userID string
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
userID, err = machineid.ProtectedID("hrp")
|
||||
if err != nil {
|
||||
userID = uuid.NewV1().String()
|
||||
}
|
||||
|
||||
// init GA4 client
|
||||
ga4Client = NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
}
|
||||
|
||||
type GA4Client struct {
|
||||
apiSecret string // Measurement Protocol API secret value
|
||||
measurementID string // MEASUREMENT ID, G-XXXXXXXXXX
|
||||
userID string // A unique identifier for a user
|
||||
httpClient *http.Client // http client session
|
||||
debug bool // send events for validation, used for debug
|
||||
}
|
||||
|
||||
// NewGA4Client creates a new GA4Client object with the measurementID and apiSecret.
|
||||
func NewGA4Client(measurementID, apiSecret string, debug ...bool) *GA4Client {
|
||||
dbg := false
|
||||
if len(debug) > 0 {
|
||||
dbg = debug[0]
|
||||
}
|
||||
|
||||
return &GA4Client{
|
||||
measurementID: measurementID,
|
||||
apiSecret: apiSecret,
|
||||
userID: userID,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
debug: dbg,
|
||||
}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
// Required. The name for the event.
|
||||
Name string `json:"name"`
|
||||
// Optional. The parameters for the event.
|
||||
// engagement_time_msec/session_id
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// payload docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag
|
||||
type Payload struct {
|
||||
// Required. Uniquely identifies a user instance of a web client
|
||||
ClientID string `json:"client_id"`
|
||||
// Optional. A unique identifier for a user
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
// Optional. A Unix timestamp (in microseconds) for the time to associate with the event.
|
||||
// This should only be set to record events that happened in the past.
|
||||
// This value can be overridden via user_property or event timestamps.
|
||||
// Events can be backdated up to 3 calendar days based on the property's timezone.
|
||||
TimestampMicros int64 `json:"timestamp_micros,omitempty"`
|
||||
// Optional. The user properties for the measurement.
|
||||
UserProperties map[string]string `json:"user_properties,omitempty"`
|
||||
// Optional. Set to true to indicate these events should not be used for personalized ads.
|
||||
NonPersonalizedAds bool `json:"non_personalized_ads,omitempty"`
|
||||
// Required. An array of event items. Up to 25 events can be sent per request.
|
||||
Events []Event `json:"events"`
|
||||
}
|
||||
|
||||
// validation docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag
|
||||
type ValidationResponse struct {
|
||||
ValidationMessages []ValidationMessage `json:"validationMessages"` // An array of validation messages.
|
||||
}
|
||||
|
||||
type ValidationMessage struct {
|
||||
FieldPath string `json:"fieldPath"` // The path to the field that was invalid.
|
||||
Description string `json:"description"` // A description of the error.
|
||||
ValidationCode ValidationCode `json:"validationCode"` // A ValidationCode that corresponds to the error.
|
||||
}
|
||||
|
||||
type ValidationCode string
|
||||
|
||||
const (
|
||||
VALUE_INVALID ValidationCode = "VALUE_INVALID" // The value provided for a fieldPath was invalid.
|
||||
VALUE_REQUIRED ValidationCode = "VALUE_REQUIRED" // A required value for a fieldPath was not provided.
|
||||
NAME_INVALID ValidationCode = "NAME_INVALID" // The name provided was invalid.
|
||||
NAME_RESERVED ValidationCode = "NAME_RESERVED" // The name provided was one of the reserved names.
|
||||
VALUE_OUT_OF_BOUNDS ValidationCode = "VALUE_OUT_OF_BOUNDS" // The value provided was too large.
|
||||
EXCEEDED_MAX_ENTITIES ValidationCode = "EXCEEDED_MAX_ENTITIES" // There were too many parameters in the request.
|
||||
NAME_DUPLICATED ValidationCode = "NAME_DUPLICATED" // The same name was provided more than once in the request.
|
||||
)
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GA4Client) SendEvent(event Event) error {
|
||||
query := url.Values{}
|
||||
query.Add("api_secret", g.apiSecret)
|
||||
query.Add("measurement_id", g.measurementID)
|
||||
|
||||
var uri string
|
||||
if g.debug {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/debug/mp/collect?%s", query.Encode())
|
||||
} else {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/mp/collect?%s", query.Encode())
|
||||
}
|
||||
|
||||
// append event params
|
||||
if event.Params == nil {
|
||||
event.Params = map[string]interface{}{}
|
||||
}
|
||||
event.Params["os"] = runtime.GOOS
|
||||
event.Params["arch"] = runtime.GOARCH
|
||||
event.Params["go_version"] = runtime.Version()
|
||||
event.Params["hrp_version"] = version.VERSION
|
||||
|
||||
payload := Payload{
|
||||
ClientID: fmt.Sprintf("%d.%d", rand.Int31(), time.Now().Unix()),
|
||||
UserID: g.userID,
|
||||
TimestampMicros: time.Now().UnixMicro(),
|
||||
Events: []Event{event},
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(payload)
|
||||
if g.debug {
|
||||
log.Debug().
|
||||
Str("uri", uri).
|
||||
Interface("payload", payload).
|
||||
Msg("send GA4 event")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshal GA4 request payload failed")
|
||||
}
|
||||
|
||||
body := bytes.NewReader(bs)
|
||||
res, err := g.httpClient.Post(uri, "application/json", body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "request GA4 failed")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return fmt.Errorf("validation response got unexpected status %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if !g.debug {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read GA4 response body failed")
|
||||
}
|
||||
|
||||
validationResponse := ValidationResponse{}
|
||||
err = json.Unmarshal(bs, &validationResponse)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshal GA4 response body failed")
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("statusCode", res.StatusCode).
|
||||
Interface("validationResponse", validationResponse).
|
||||
Msg("get GA4 validation response")
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendGA4Event(name string, params map[string]interface{}) {
|
||||
if os.Getenv("DISABLE_GA") == "true" {
|
||||
// do not send GA4 events in CI environment
|
||||
return
|
||||
}
|
||||
|
||||
event := Event{
|
||||
Name: name,
|
||||
Params: params,
|
||||
}
|
||||
err := ga4Client.SendEvent(event)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("send GA4 event failed")
|
||||
}
|
||||
}
|
||||
15
internal/sdk/ga4_test.go
Normal file
15
internal/sdk/ga4_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGA4(t *testing.T) {
|
||||
ga4Client := NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
|
||||
event := Event{
|
||||
Name: "hrp_debug_event",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
ga4Client.SendEvent(event)
|
||||
}
|
||||
37
internal/sdk/sentry.go
Normal file
37
internal/sdk/sentry.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryDSN = "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// init sentry sdk
|
||||
if os.Getenv("DISABLE_SENTRY") == "true" {
|
||||
return
|
||||
}
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: fmt.Sprintf("httprunner@%s", version.VERSION),
|
||||
AttachStacktrace: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("init sentry sdk failed!")
|
||||
return
|
||||
}
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetUser(sentry.User{
|
||||
ID: userID,
|
||||
})
|
||||
})
|
||||
}
|
||||
1
internal/version/VERSION
Normal file
1
internal/version/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
v5.0.0+2502052133
|
||||
13
internal/version/init.go
Normal file
13
internal/version/init.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed VERSION
|
||||
var VERSION string
|
||||
|
||||
func init() {
|
||||
VERSION = strings.TrimSpace(VERSION)
|
||||
}
|
||||
11
internal/wiki/main.go
Normal file
11
internal/wiki/main.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func OpenWiki() error {
|
||||
log.Info().Msgf("%s https://httprunner.com", openCmd)
|
||||
return myexec.RunCommand(openCmd, "https://httprunner.com")
|
||||
}
|
||||
3
internal/wiki/open_darwin.go
Normal file
3
internal/wiki/open_darwin.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "open"
|
||||
3
internal/wiki/open_linux.go
Normal file
3
internal/wiki/open_linux.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "xdg-open"
|
||||
3
internal/wiki/open_windows.go
Normal file
3
internal/wiki/open_windows.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "explorer"
|
||||
Reference in New Issue
Block a user