mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-28 11:01:30 +08:00
refactor: move hrp/ to root folder
This commit is contained in:
144
hrp/README.md
144
hrp/README.md
@@ -1,144 +0,0 @@
|
||||
# 代码阅读指南(golang 部分)
|
||||
|
||||
## 核心数据结构
|
||||
|
||||
HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤的集合。
|
||||
|
||||
```go
|
||||
type TestCase struct {
|
||||
Config IConfig `json:"config" yaml:"config"`
|
||||
TestSteps []IStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
type IConfig interface {
|
||||
Get() *TConfig
|
||||
}
|
||||
```
|
||||
|
||||
其中,测试步骤 `IStep` 采用了 `go interface` 的设计理念,支持进行任意拓展;步骤内容统一在 `Run` 方法中进行实现。
|
||||
|
||||
```go
|
||||
type IStep interface {
|
||||
Name() string
|
||||
Type() StepType
|
||||
Config() *StepConfig
|
||||
Run(*SessionRunner) (*StepResult, error)
|
||||
}
|
||||
```
|
||||
|
||||
我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 hrp 已支持的步骤类型包括:
|
||||
|
||||
- [request](step_request.go):发起单次 HTTP 请求
|
||||
- [api](step_api.go):引用执行其它 API 文件
|
||||
- [testcase](step_testcase.go):引用执行其它测试用例文件
|
||||
- [thinktime](step_thinktime.go):思考时间,按照配置的逻辑进行等待
|
||||
- [transaction](step_transaction.go):事务机制,用于压测
|
||||
- [rendezvous](step_rendezvous.go):集合点机制,用于压测
|
||||
- [mobile_UI](step_mobile_ui.go):移动端 UI 自动化
|
||||
- [shell](step_shell.go):执行 shell 命令
|
||||
|
||||
基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。
|
||||
|
||||
## 运行主流程
|
||||
|
||||
### 整体控制器 HRPRunner
|
||||
|
||||
执行接口测试时,会初始化一个 `HRPRunner`,用于控制测试的执行策略。
|
||||
|
||||
```go
|
||||
type HRPRunner struct {
|
||||
t *testing.T
|
||||
failfast bool
|
||||
requestsLogOn bool
|
||||
pluginLogOn bool
|
||||
saveTests bool
|
||||
genHTMLReport bool
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (r *HRPRunner) Run(testcases ...ITestCase) error
|
||||
func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error)
|
||||
```
|
||||
|
||||
重点关注两个方法:
|
||||
|
||||
- Run:测试执行的主入口,支持运行一个或多个测试用例
|
||||
- NewCaseRunner:针对给定的测试用例初始化一个 CaseRunner
|
||||
|
||||
### 用例执行器 CaseRunner
|
||||
|
||||
针对每个测试用例,采用 CaseRunner 存储其公共信息,包括 plugin/parser
|
||||
|
||||
```go
|
||||
type CaseRunner struct {
|
||||
TestCase // each testcase init its own CaseRunner
|
||||
|
||||
hrpRunner *HRPRunner // all case runners share one HRPRunner
|
||||
parser *Parser // each CaseRunner init its own Parser
|
||||
|
||||
parametersIterator *ParametersIterator
|
||||
}
|
||||
|
||||
func (r *CaseRunner) NewSession() *SessionRunner
|
||||
```
|
||||
|
||||
重点关注一个方法:
|
||||
|
||||
- NewSession:测试用例的每一次执行对应一个 SessionRunner
|
||||
|
||||
### SessionRunner
|
||||
|
||||
测试用例的具体执行都由 `SessionRunner` 完成,每个 session 实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。
|
||||
|
||||
```go
|
||||
type SessionRunner struct {
|
||||
caseRunner *CaseRunner // all session runners share one CaseRunner
|
||||
|
||||
sessionVariables map[string]interface{} // testcase execution session variables
|
||||
summary *TestCaseSummary // record test case summary
|
||||
}
|
||||
|
||||
func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCaseSummary, err error)
|
||||
```
|
||||
|
||||
重点关注一个方法:
|
||||
|
||||
- Start:启动执行用例,依次执行所有测试步骤
|
||||
|
||||
```go
|
||||
func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCaseSummary, err error) {
|
||||
...
|
||||
r.resetSession()
|
||||
|
||||
r.InitWithParameters(givenVars)
|
||||
|
||||
defer func() {
|
||||
summary = r.summary
|
||||
}
|
||||
|
||||
// run step in sequential order
|
||||
for _, step := range r.testCase.TestSteps {
|
||||
// parse step
|
||||
err = r.parseStepStruct(step)
|
||||
|
||||
// run step
|
||||
stepResult, err := step.Run(r)
|
||||
|
||||
// update summary
|
||||
r.summary.Records = append(r.summary.Records, stepResult)
|
||||
|
||||
// update extracted variables
|
||||
for k, v := range stepResult.ExportVars {
|
||||
r.sessionVariables[k] = v
|
||||
}
|
||||
|
||||
// check if failfast
|
||||
if err != nil && r.caseRunner.hrpRunner.failfast {
|
||||
return errors.Wrap(err, "abort running due to failfast setting")
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
在主流程中,SessionRunner 并不需要关注 step 的具体类型,统一都是调用 `step.Run(r)`,具体实现逻辑都在对应 step 的 `Run(*SessionRunner)` 方法中。
|
||||
258
hrp/build.go
258
hrp/build.go
@@ -1,258 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/version"
|
||||
)
|
||||
|
||||
//go:embed internal/scaffold/templates/plugin/debugtalkPythonTemplate
|
||||
var pyTemplate string
|
||||
|
||||
//go:embed internal/scaffold/templates/plugin/debugtalkGoTemplate
|
||||
var goTemplate string
|
||||
|
||||
// regex for finding all function names
|
||||
type regexFunctions struct {
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
regexPyFunctionName = regexFunctions{regexp.MustCompile(`(?m)^def ([a-zA-Z_]\w*)\(.*\)`)}
|
||||
regexGoFunctionName = regexFunctions{regexp.MustCompile(`(?m)^func ([a-zA-Z_]\w*)\(.*\)`)}
|
||||
)
|
||||
|
||||
func (r *regexFunctions) findAllFunctionNames(content string) ([]string, error) {
|
||||
var functionNames []string
|
||||
// find all function names
|
||||
functionNameSlice := r.FindAllStringSubmatch(content, -1)
|
||||
for _, elem := range functionNameSlice {
|
||||
name := strings.Trim(elem[1], " ")
|
||||
functionNames = append(functionNames, name)
|
||||
}
|
||||
|
||||
var filteredFunctionNames []string
|
||||
if r == ®exPyFunctionName {
|
||||
// filter private functions
|
||||
for _, name := range functionNames {
|
||||
if strings.HasPrefix(name, "__") {
|
||||
continue
|
||||
}
|
||||
filteredFunctionNames = append(filteredFunctionNames, name)
|
||||
}
|
||||
} else if r == ®exGoFunctionName {
|
||||
// filter main and init function
|
||||
for _, name := range functionNames {
|
||||
if name == "main" {
|
||||
log.Warn().Msg("plugin debugtalk.go should not define main() function !!!")
|
||||
return nil, errors.New("debugtalk.go should not contain main() function")
|
||||
}
|
||||
if name == "init" {
|
||||
continue
|
||||
}
|
||||
filteredFunctionNames = append(filteredFunctionNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Strs("functionNames", filteredFunctionNames).Msg("find all function names")
|
||||
return filteredFunctionNames, nil
|
||||
}
|
||||
|
||||
type pluginTemplate struct {
|
||||
path string // file path
|
||||
Version string // hrp version
|
||||
FunctionNames []string // function names
|
||||
}
|
||||
|
||||
func (pt *pluginTemplate) generate(tmpl, output string) error {
|
||||
file, err := os.Create(output)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("open output file failed")
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
err = template.Must(template.New("debugtalk").Parse(tmpl)).Execute(writer, pt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("execute template parsing failed")
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.Flush()
|
||||
if err == nil {
|
||||
log.Info().Str("output", output).Msg("generate debugtalk success")
|
||||
} else {
|
||||
log.Error().Str("output", output).Msg("generate debugtalk failed")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (pt *pluginTemplate) generatePy(output string) error {
|
||||
// specify output file path
|
||||
if output == "" {
|
||||
output = filepath.Join(config.RootDir, PluginPySourceGenFile)
|
||||
} else if builtin.IsFolderPathExists(output) {
|
||||
output = filepath.Join(output, PluginPySourceGenFile)
|
||||
}
|
||||
|
||||
// generate .debugtalk_gen.py
|
||||
err := pt.generate(pyTemplate, output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Str("output", output).Str("plugin", pt.path).Msg("build python plugin successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *pluginTemplate) generateGo(output string) error {
|
||||
pluginDir := filepath.Dir(pt.path)
|
||||
err := pt.generate(goTemplate, filepath.Join(pluginDir, PluginGoSourceGenFile))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "generate hashicorp plugin failed")
|
||||
}
|
||||
|
||||
// check go sdk in tempDir
|
||||
if err := myexec.RunCommand("go", "version"); err != nil {
|
||||
return errors.Wrap(err, "go sdk not installed")
|
||||
}
|
||||
|
||||
if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) {
|
||||
// create go mod
|
||||
if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "init", "main"), pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// download plugin dependency
|
||||
// funplugin version should be locked
|
||||
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", fungo.Version)
|
||||
if err := myexec.ExecCommandInDir(myexec.Command("go", "get", funplugin), pluginDir); err != nil {
|
||||
return errors.Wrap(err, "go get funplugin failed")
|
||||
}
|
||||
}
|
||||
|
||||
// add missing and remove unused modules
|
||||
if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "tidy"), pluginDir); err != nil {
|
||||
return errors.Wrap(err, "go mod tidy failed")
|
||||
}
|
||||
|
||||
// specify output file path
|
||||
if output == "" {
|
||||
output = filepath.Join(config.RootDir, PluginHashicorpGoBuiltFile)
|
||||
} else if builtin.IsFolderPathExists(output) {
|
||||
output = filepath.Join(output, PluginHashicorpGoBuiltFile)
|
||||
}
|
||||
outputPath, _ := filepath.Abs(output)
|
||||
|
||||
// build go plugin to debugtalk.bin
|
||||
cmd := myexec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path))
|
||||
if err := myexec.ExecCommandInDir(cmd, pluginDir); err != nil {
|
||||
return errors.Wrap(err, "go build plugin failed")
|
||||
}
|
||||
log.Info().Str("output", outputPath).Str("plugin", pt.path).Msg("build go plugin successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGo builds debugtalk.go to debugtalk.bin
|
||||
func buildGo(path string, output string) error {
|
||||
log.Info().Str("path", path).Str("output", output).Msg("start to build go plugin")
|
||||
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{
|
||||
"pluginType": "go",
|
||||
})
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to read file")
|
||||
return errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
functionNames, err := regexGoFunctionName.findAllFunctionNames(string(content))
|
||||
if err != nil {
|
||||
return errors.Wrap(code.InvalidPluginFile, err.Error())
|
||||
}
|
||||
|
||||
templateContent := &pluginTemplate{
|
||||
path: path,
|
||||
Version: version.VERSION,
|
||||
FunctionNames: functionNames,
|
||||
}
|
||||
err = templateContent.generateGo(output)
|
||||
if err != nil {
|
||||
return errors.Wrap(code.BuildGoPluginFailed, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildPy completes funppy information in debugtalk.py
|
||||
func buildPy(path string, output string) error {
|
||||
log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin")
|
||||
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{
|
||||
"pluginType": "python",
|
||||
})
|
||||
|
||||
// check the syntax of debugtalk.py
|
||||
err := myexec.ExecPython3Command("py_compile", path)
|
||||
if err != nil {
|
||||
return errors.Wrap(code.InvalidPluginFile,
|
||||
fmt.Sprintf("python plugin syntax invalid: %s", err.Error()))
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to read file")
|
||||
return errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
functionNames, err := regexPyFunctionName.findAllFunctionNames(string(content))
|
||||
if err != nil {
|
||||
return errors.Wrap(code.InvalidPluginFile, err.Error())
|
||||
}
|
||||
|
||||
templateContent := &pluginTemplate{
|
||||
path: path,
|
||||
Version: version.VERSION,
|
||||
FunctionNames: functionNames,
|
||||
}
|
||||
err = templateContent.generatePy(output)
|
||||
if err != nil {
|
||||
return errors.Wrap(code.BuildPyPluginFailed, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildPlugin(path string, output string) (err error) {
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".py":
|
||||
err = buildPy(path, output)
|
||||
case ".go":
|
||||
err = buildGo(path, output)
|
||||
default:
|
||||
return errors.Wrap(code.UnsupportedFileExtension,
|
||||
"type error, expected .py or .go")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("build plugin failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
err := BuildPlugin(tmpl("plugin/debugtalk.go"), "./debugtalk.bin")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
genDebugTalkPyPath := filepath.Join(tmpl("plugin/"), PluginPySourceGenFile)
|
||||
err = BuildPlugin(tmpl("plugin/debugtalk.py"), genDebugTalkPyPath)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
contentBytes, err := readFile(genDebugTalkPyPath)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
content := string(contentBytes)
|
||||
if !assert.Contains(t, content, "import funppy") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Contains(t, content, "funppy.register") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
reg, _ := regexp.Compile(`funppy\.register`)
|
||||
matchedSlice := reg.FindAllStringSubmatch(content, -1)
|
||||
if !assert.Len(t, matchedSlice, 10) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAllPythonFunctionNames(t *testing.T) {
|
||||
content := `
|
||||
def test_1(): # exported function
|
||||
pass
|
||||
|
||||
def _test_2(): # exported function
|
||||
pass
|
||||
|
||||
def __test_3(): # private function
|
||||
pass
|
||||
|
||||
# def test_4(): # commented out function
|
||||
# pass
|
||||
|
||||
def Test5(): # exported function
|
||||
pass
|
||||
`
|
||||
names, err := regexPyFunctionName.findAllFunctionNames(content)
|
||||
if !assert.Nil(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "test_1") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "Test5") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "_test_2") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.NotContains(t, names, "__test_3") {
|
||||
t.FailNow()
|
||||
}
|
||||
// commented out function
|
||||
if !assert.NotContains(t, names, "test_4") {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAllGoFunctionNames(t *testing.T) {
|
||||
content := `
|
||||
func Test1() { // exported function
|
||||
return
|
||||
}
|
||||
|
||||
func testFunc2() { // exported function
|
||||
return
|
||||
}
|
||||
|
||||
func init() { // private function
|
||||
return
|
||||
}
|
||||
|
||||
func _Test3() { // exported function
|
||||
return
|
||||
}
|
||||
|
||||
// func Test4() { // commented out function
|
||||
// return
|
||||
// }
|
||||
`
|
||||
names, err := regexGoFunctionName.findAllFunctionNames(content)
|
||||
if !assert.Nil(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "Test1") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "testFunc2") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.NotContains(t, names, "init") {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, names, "_Test3") {
|
||||
t.FailNow()
|
||||
}
|
||||
// commented out function
|
||||
if !assert.NotContains(t, names, "Test4") {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAllGoFunctionNamesAbnormal(t *testing.T) {
|
||||
content := `
|
||||
func init() { // private function
|
||||
return
|
||||
}
|
||||
|
||||
func main() { // should not define main() function
|
||||
return
|
||||
}
|
||||
`
|
||||
_, err := regexGoFunctionName.findAllFunctionNames(content)
|
||||
if !assert.NotNil(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package adb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func format(data map[string]string) string {
|
||||
result, _ := json.MarshalIndent(data, "", "\t")
|
||||
return string(result)
|
||||
}
|
||||
|
||||
var listAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "List all Android devices",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
deviceList, err := uixt.GetAndroidDevices(serial)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for _, d := range deviceList {
|
||||
if isDetail {
|
||||
fmt.Println(format(d.DeviceInfo()))
|
||||
} else {
|
||||
if usb, err := d.Usb(); err != nil {
|
||||
fmt.Println(d.Serial())
|
||||
} else {
|
||||
fmt.Println(d.Serial(), usb)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
serial string
|
||||
isDetail bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
listAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial")
|
||||
listAndroidDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail")
|
||||
androidRootCmd.AddCommand(listAndroidDevicesCmd)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package adb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/gadb"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var androidRootCmd = &cobra.Command{
|
||||
Use: "adb",
|
||||
Short: "simple utils for android device management",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
func getDevice(serial string) (*gadb.Device, error) {
|
||||
devices, err := uixt.GetAndroidDevices(serial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(devices) > 1 {
|
||||
return nil, fmt.Errorf("found multiple attached devices, please specify android serial")
|
||||
}
|
||||
return devices[0], nil
|
||||
}
|
||||
|
||||
func Init(rootCmd *cobra.Command) {
|
||||
rootCmd.AddCommand(androidRootCmd)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package adb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var (
|
||||
replace bool
|
||||
downgrade bool
|
||||
grant bool
|
||||
)
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install [flags] PACKAGE",
|
||||
Short: "push package to the device and install them automatically",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
_, err = getDevice(serial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
driverExt, err := device.NewDriver()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = driverExt.Install(args[0],
|
||||
uixt.WithReinstall(replace),
|
||||
uixt.WithDowngrade(downgrade),
|
||||
uixt.WithGrantPermission(grant),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("success")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
installCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial")
|
||||
installCmd.Flags().BoolVarP(&replace, "replace", "r", false, "replace existing application")
|
||||
installCmd.Flags().BoolVarP(&downgrade, "downgrade", "d", false, "allow version code downgrade (debuggable packages only)")
|
||||
installCmd.Flags().BoolVarP(&grant, "grant", "g", false, "grant all runtime permissions")
|
||||
androidRootCmd.AddCommand(installCmd)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package adb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var screencapAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "screencap",
|
||||
Short: "Start android screen capture",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_screencap", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(serial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := device.ScreenCap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_%d"))
|
||||
if err = os.WriteFile(filepath, res, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("screencap saved to", filepath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
screencapAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial")
|
||||
androidRootCmd.AddCommand(screencapAndroidDevicesCmd)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var buildCmd = &cobra.Command{
|
||||
Use: "build $path ...",
|
||||
Short: "build plugin for testing",
|
||||
Long: `build python/go plugin for testing`,
|
||||
Example: ` $ hrp build plugin/debugtalk.go
|
||||
$ hrp build plugin/debugtalk.py`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_build", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
return hrp.BuildPlugin(args[0], output)
|
||||
},
|
||||
}
|
||||
|
||||
var output string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(buildCmd)
|
||||
|
||||
buildCmd.Flags().StringVarP(&output, "output", "o", "", "funplugin product output path, default: cwd")
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// report panic to sentry
|
||||
sentry.CurrentHub().Recover(err)
|
||||
sentry.Flush(time.Second * 5)
|
||||
|
||||
// print panic trace
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
exitCode := cmd.Execute()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/convert"
|
||||
)
|
||||
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert $path...",
|
||||
Short: "convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
SilenceUsage: false,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
caseConverter := convert.NewConverter(outputDir, profilePath)
|
||||
|
||||
var fromType convert.FromType
|
||||
if fromYAMLFlag {
|
||||
fromType = convert.FromTypeYAML
|
||||
} else if fromPostmanFlag {
|
||||
fromType = convert.FromTypePostman
|
||||
} else if fromHARFlag {
|
||||
fromType = convert.FromTypeHAR
|
||||
} else if fromCurlFlag {
|
||||
fromType = convert.FromTypeCurl
|
||||
} else {
|
||||
fromType = convert.FromTypeJSON
|
||||
log.Info().Str("fromType", fromType.String()).Msg("set default")
|
||||
}
|
||||
|
||||
var outputType convert.OutputType
|
||||
if toYAMLFlag {
|
||||
outputType = convert.OutputTypeYAML
|
||||
} else if toPyTestFlag {
|
||||
packages := []string{"httprunner"}
|
||||
_, err := myexec.EnsurePython3Venv(venv, packages...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("python3 venv is not ready")
|
||||
return errors.Wrap(code.InvalidPython3Venv, err.Error())
|
||||
}
|
||||
|
||||
outputType = convert.OutputTypePyTest
|
||||
} else {
|
||||
outputType = convert.OutputTypeJSON
|
||||
log.Info().Str("outputType", outputType.String()).Msg("set default")
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, arg := range args {
|
||||
if builtin.IsFolderPathExists(arg) {
|
||||
fs, err := os.ReadDir(arg)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", arg).Msg("read dir failed")
|
||||
continue
|
||||
}
|
||||
for _, f := range fs {
|
||||
files = append(files, filepath.Join(arg, f.Name()))
|
||||
}
|
||||
} else {
|
||||
files = append(files, arg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
extName := filepath.Ext(file)
|
||||
if !builtin.Contains(fromType.Extensions(), extName) {
|
||||
log.Warn().Str("path", file).
|
||||
Strs("expectExtensions", fromType.Extensions()).
|
||||
Msg("skip file")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := caseConverter.Convert(file, fromType, outputType); err != nil {
|
||||
log.Error().Err(err).Str("path", file).
|
||||
Str("outputType", outputType.String()).
|
||||
Msg("convert case failed")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
outputDir string
|
||||
profilePath string
|
||||
|
||||
fromJSONFlag bool
|
||||
fromYAMLFlag bool
|
||||
fromPostmanFlag bool
|
||||
fromHARFlag bool
|
||||
fromCurlFlag bool
|
||||
|
||||
toJSONFlag bool
|
||||
toYAMLFlag bool
|
||||
toPyTestFlag bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(convertCmd)
|
||||
|
||||
convertCmd.Flags().BoolVar(&fromJSONFlag, "from-json", true, "load from json case format")
|
||||
convertCmd.Flags().BoolVar(&fromYAMLFlag, "from-yaml", false, "load from yaml case format")
|
||||
convertCmd.Flags().BoolVar(&fromHARFlag, "from-har", false, "load from HAR format")
|
||||
convertCmd.Flags().BoolVar(&fromPostmanFlag, "from-postman", false, "load from postman format")
|
||||
convertCmd.Flags().BoolVar(&fromCurlFlag, "from-curl", false, "load from curl format")
|
||||
|
||||
convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", true, "convert to JSON case scripts")
|
||||
convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML case scripts")
|
||||
convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
|
||||
|
||||
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory")
|
||||
convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies")
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
// run this test to generate markdown docs for hrp command
|
||||
func TestGenMarkdownTree(t *testing.T) {
|
||||
err := doc.GenMarkdownTree(rootCmd, "../../docs/cmd")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
CFBundleVersion string `json:"version"`
|
||||
CFBundleDisplayName string `json:"name"`
|
||||
CFBundleIdentifier string `json:"bundleId"`
|
||||
}
|
||||
|
||||
var listAppsCmd = &cobra.Command{
|
||||
Use: "apps",
|
||||
Short: "List all iOS installed apps",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_apps", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
device.GetDeviceInfo()
|
||||
var applicationType uixt.ApplicationType
|
||||
switch appType {
|
||||
case "user":
|
||||
applicationType = uixt.ApplicationTypeUser
|
||||
case "system":
|
||||
applicationType = uixt.ApplicationTypeSystem
|
||||
case "internal":
|
||||
applicationType = uixt.ApplicationTypeInternal
|
||||
case "all":
|
||||
applicationType = uixt.ApplicationTypeAny
|
||||
}
|
||||
|
||||
result, err := device.ListApps(applicationType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get app list failed %v", err)
|
||||
}
|
||||
|
||||
for _, app := range result {
|
||||
a := Application{}
|
||||
mapstructure.Decode(app, &a)
|
||||
|
||||
fmt.Printf("%-30.30s %-50.50s %-s\n",
|
||||
a.CFBundleDisplayName, a.CFBundleIdentifier, a.CFBundleVersion)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appType string
|
||||
|
||||
func init() {
|
||||
listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid")
|
||||
listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|internal|all]")
|
||||
iosRootCmd.AddCommand(listAppsCmd)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/danielpaulus/go-ios/ios"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
d ios.DeviceEntry
|
||||
UDID string `json:"UDID"`
|
||||
Status string `json:"status"`
|
||||
ConnectionType string `json:"connectionType"`
|
||||
ConnectionSpeed int `json:"connectionSpeed"`
|
||||
DeviceDetail *uixt.DeviceDetail `json:"deviceDetail,omitempty"`
|
||||
}
|
||||
|
||||
func (device *Device) GetStatus() string {
|
||||
if device.ConnectionType != "" {
|
||||
return "online"
|
||||
} else {
|
||||
return "offline"
|
||||
}
|
||||
}
|
||||
|
||||
func (device *Device) ToFormat() string {
|
||||
result, _ := json.MarshalIndent(device, "", "\t")
|
||||
return string(result)
|
||||
}
|
||||
|
||||
var listDevicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "List all iOS devices",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
devices, err := uixt.GetIOSDevices(udid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
deviceProperties := d.Properties
|
||||
device := &Device{
|
||||
d: d,
|
||||
UDID: deviceProperties.SerialNumber,
|
||||
ConnectionType: deviceProperties.ConnectionType,
|
||||
ConnectionSpeed: deviceProperties.ConnectionSpeed,
|
||||
}
|
||||
device.Status = device.GetStatus()
|
||||
|
||||
fmt.Println(device.UDID, device.ConnectionType, device.Status)
|
||||
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var udid string
|
||||
|
||||
func init() {
|
||||
listDevicesCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid")
|
||||
iosRootCmd.AddCommand(listDevicesCmd)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var iosRootCmd = &cobra.Command{
|
||||
Use: "ios",
|
||||
Short: "simple utils for ios device management",
|
||||
}
|
||||
|
||||
func getDevice(udid string) (*uixt.IOSDevice, error) {
|
||||
device, err := uixt.NewIOSDevice(uixt.WithUDID(udid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func Init(rootCmd *cobra.Command) {
|
||||
rootCmd.AddCommand(iosRootCmd)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install [flags] PACKAGE",
|
||||
Short: "push package to the device and install them automatically",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
_, err = getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
device, err := uixt.NewIOSDevice(uixt.WithUDID(udid))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = device.Install(args[0])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("success")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
installCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's serial")
|
||||
|
||||
iosRootCmd.AddCommand(installCmd)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//go:build localtest
|
||||
|
||||
package ios
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetDevice(t *testing.T) {
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("device: %v", device)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
// mountCmd represents the mount command
|
||||
var mountCmd = &cobra.Command{
|
||||
Use: "mount",
|
||||
Short: "A brief description of your command",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_mount", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
images, errImage := device.ListImages()
|
||||
if err != nil {
|
||||
return fmt.Errorf("list device images failed: %v", err)
|
||||
}
|
||||
if listDeveloperDiskImage {
|
||||
for i, imgSign := range images {
|
||||
fmt.Printf("[%d] %s\n", i+1, imgSign)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if errImage == nil && len(images) > 0 {
|
||||
log.Info().Strs("images", images).Msg("ios developer image is already mounted")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info().Str("dir", developerDiskImageDir).Msg("start to mount ios developer image")
|
||||
|
||||
if !builtin.IsFolderPathExists(developerDiskImageDir) {
|
||||
return fmt.Errorf("developer disk image directory not exist: %s", developerDiskImageDir)
|
||||
}
|
||||
|
||||
if err = device.AutoMountImage(developerDiskImageDir); err != nil {
|
||||
return fmt.Errorf("mount developer disk image failed: %s", err)
|
||||
}
|
||||
|
||||
log.Info().Msg("mount developer disk image successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
const defaultDeveloperDiskImageDir = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/"
|
||||
|
||||
var (
|
||||
developerDiskImageDir string
|
||||
listDeveloperDiskImage bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
mountCmd.Flags().BoolVar(&listDeveloperDiskImage, "list", false, "list developer disk images")
|
||||
mountCmd.Flags().StringVarP(&developerDiskImageDir, "dir", "d", defaultDeveloperDiskImageDir, "specify developer disk image directory")
|
||||
mountCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid")
|
||||
iosRootCmd.AddCommand(mountCmd)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var psCmd = &cobra.Command{
|
||||
Use: "ps",
|
||||
Short: "show running processes",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_ps", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runningProcesses, err := device.ListProcess(!isAll)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range runningProcesses {
|
||||
fmt.Printf("%4d %-"+fmt.Sprintf("%d", len(runningProcesses))+"s %20s %s\n",
|
||||
p.Pid, p.Name, time.Since(p.StartDate).String(), bundleID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var isAll bool
|
||||
|
||||
func init() {
|
||||
psCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid")
|
||||
psCmd.Flags().BoolVarP(&isAll, "all", "a", false, "print all processes including system processes")
|
||||
iosRootCmd.AddCommand(psCmd)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var rebootCmd = &cobra.Command{
|
||||
Use: "reboot",
|
||||
Short: "reboot ios device",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_reboot", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = device.Reboot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("reboot %s success\n", device.UDID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rebootCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid")
|
||||
iosRootCmd.AddCommand(rebootCmd)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var uninstallCmd = &cobra.Command{
|
||||
Use: "uninstall [flags] PACKAGE",
|
||||
Short: "uninstall package automatically",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
if len(bundleId) == 0 {
|
||||
return fmt.Errorf("bundleId is empty")
|
||||
}
|
||||
|
||||
_, err = getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
device, err := uixt.NewIOSDevice(uixt.WithUDID(udid))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = device.Uninstall(bundleId)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("success")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var bundleId string
|
||||
|
||||
func init() {
|
||||
uninstallCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's serial")
|
||||
uninstallCmd.Flags().StringVarP(&bundleId, "bundleId", "b", "", "bundleId to uninstall")
|
||||
|
||||
iosRootCmd.AddCommand(uninstallCmd)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package ios
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var xctestCmd = &cobra.Command{
|
||||
Use: "xctest",
|
||||
Short: "run xctest",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_xctest", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
if bundleID == "" {
|
||||
return fmt.Errorf("bundleID is required")
|
||||
}
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = device.RunXCTest(context.Background(), bundleID, testRunnerBundleID, xctestConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run xctest failed")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
bundleID string
|
||||
testRunnerBundleID string
|
||||
xctestConfig string
|
||||
)
|
||||
|
||||
func init() {
|
||||
xctestCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify ios device's UDID")
|
||||
xctestCmd.Flags().StringVarP(&bundleID, "bundleID", "b", "com.gtf.wda.runner.xctrunner", "specify ios bundleID")
|
||||
xctestCmd.Flags().StringVarP(&testRunnerBundleID, "testRunnerBundleID", "t", "com.gtf.wda.runner.xctrunner", "specify ios testRunnerBundleID")
|
||||
xctestCmd.Flags().StringVarP(&xctestConfig, "xctestConfig", "x", "GtfWdaRunner.xctest", "specify ios xctestConfig")
|
||||
iosRootCmd.AddCommand(xctestCmd)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/pytest"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var pytestCmd = &cobra.Command{
|
||||
Use: "pytest $path ...",
|
||||
Short: "run API test with pytest",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableFlagParsing: true, // allow to pass any args to pytest
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_pytest", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
packages := []string{"httprunner"}
|
||||
_, err = myexec.EnsurePython3Venv(venv, packages...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("python3 venv is not ready")
|
||||
return errors.Wrap(code.InvalidPython3Venv, err.Error())
|
||||
}
|
||||
return pytest.RunPytest(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pytestCmd)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/cmd/adb"
|
||||
"github.com/httprunner/httprunner/v5/hrp/cmd/ios"
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/version"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "hrp",
|
||||
Short: "Next-Generation API Testing Solution.",
|
||||
Long: `
|
||||
██╗ ██╗████████╗████████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗███╗ ██╗███████╗██████╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║████╗ ██║██╔════╝██╔══██╗
|
||||
███████║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██████╔╝
|
||||
██╔══██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██╔══██╗
|
||||
██║ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║██║ ╚████║███████╗██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
||||
|
||||
HttpRunner is an open source API testing tool that supports HTTP(S)/HTTP2/WebSocket/RPC
|
||||
network protocols, covering API testing, performance testing and digital experience
|
||||
monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
|
||||
|
||||
License: Apache-2.0
|
||||
Website: https://httprunner.com
|
||||
Github: https://github.com/httprunner/httprunner
|
||||
Copyright 2017 debugtalk`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
hrp.InitLogger(logLevel, logJSON)
|
||||
},
|
||||
Version: version.VERSION,
|
||||
TraverseChildren: true, // parses flags on all parents before executing child command
|
||||
SilenceUsage: true, // silence usage when an error occurs
|
||||
}
|
||||
|
||||
var (
|
||||
logLevel string
|
||||
logJSON bool
|
||||
venv string
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() int {
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "INFO", "set log level")
|
||||
rootCmd.PersistentFlags().BoolVar(&logJSON, "log-json", false, "set log to json format (default colorized console)")
|
||||
rootCmd.PersistentFlags().StringVar(&venv, "venv", "", "specify python3 venv path")
|
||||
|
||||
ios.Init(rootCmd)
|
||||
adb.Init(rootCmd)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
return code.GetErrorCode(err)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
)
|
||||
|
||||
// runCmd represents the run command
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run $path...",
|
||||
Short: "run API test with go engine",
|
||||
Long: `run yaml/json testcase files for API test`,
|
||||
Example: ` $ hrp run demo.json # run specified json testcase file
|
||||
$ hrp run demo.yaml # run specified yaml testcase file
|
||||
$ hrp run examples/ # run testcases in specified folder`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var paths []hrp.ITestCase
|
||||
for _, arg := range args {
|
||||
path := hrp.TestCasePath(arg)
|
||||
paths = append(paths, &path)
|
||||
}
|
||||
runner := makeHRPRunner()
|
||||
return runner.Run(paths...)
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
continueOnFailure bool
|
||||
requestsLogOff bool
|
||||
httpStatOn bool
|
||||
pluginLogOn bool
|
||||
proxyUrl string
|
||||
saveTests bool
|
||||
genHTMLReport bool
|
||||
caseTimeout float32
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs")
|
||||
runCmd.Flags().BoolVar(&requestsLogOff, "log-requests-off", false, "turn off request & response details logging")
|
||||
runCmd.Flags().BoolVar(&httpStatOn, "http-stat", false, "turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)")
|
||||
runCmd.Flags().BoolVar(&pluginLogOn, "log-plugin", false, "turn on plugin logging")
|
||||
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
|
||||
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
|
||||
runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
|
||||
runCmd.Flags().Float32Var(&caseTimeout, "case-timeout", 3600, "set testcase timeout (seconds)")
|
||||
}
|
||||
|
||||
func makeHRPRunner() *hrp.HRPRunner {
|
||||
runner := hrp.NewRunner(nil).
|
||||
SetFailfast(!continueOnFailure).
|
||||
SetSaveTests(saveTests).
|
||||
SetCaseTimeout(caseTimeout)
|
||||
if genHTMLReport {
|
||||
runner.GenHTMLReport()
|
||||
}
|
||||
if !requestsLogOff {
|
||||
runner.SetRequestsLogOn()
|
||||
}
|
||||
if httpStatOn {
|
||||
runner.SetHTTPStatOn()
|
||||
}
|
||||
if pluginLogOn {
|
||||
runner.SetPluginLogOn()
|
||||
}
|
||||
if venv != "" {
|
||||
runner.SetPython3Venv(venv)
|
||||
}
|
||||
if proxyUrl != "" {
|
||||
runner.SetProxyUrl(proxyUrl)
|
||||
}
|
||||
return runner
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/scaffold"
|
||||
)
|
||||
|
||||
var scaffoldCmd = &cobra.Command{
|
||||
Use: "startproject $project_name",
|
||||
Aliases: []string{"scaffold"},
|
||||
Short: "create a scaffold project",
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !ignorePlugin && !genPythonPlugin && !genGoPlugin {
|
||||
return errors.New("please specify function plugin type")
|
||||
}
|
||||
|
||||
var pluginType scaffold.PluginType
|
||||
if empty {
|
||||
pluginType = scaffold.Empty
|
||||
} else if ignorePlugin {
|
||||
pluginType = scaffold.Ignore
|
||||
} else if genGoPlugin {
|
||||
pluginType = scaffold.Go
|
||||
} else {
|
||||
pluginType = scaffold.Py // default
|
||||
}
|
||||
|
||||
err := scaffold.CreateScaffold(args[0], pluginType, venv, force)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create scaffold project failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("projectName", args[0]).Msg("create scaffold success")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
empty bool
|
||||
ignorePlugin bool
|
||||
genPythonPlugin bool
|
||||
genGoPlugin bool
|
||||
force bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(scaffoldCmd)
|
||||
scaffoldCmd.Flags().BoolVarP(&force, "force", "f", false, "force to overwrite existing project")
|
||||
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
|
||||
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
|
||||
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")
|
||||
scaffoldCmd.Flags().BoolVar(&empty, "empty", false, "generate empty project")
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/server"
|
||||
)
|
||||
|
||||
// serverCmd represents the server command
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server start",
|
||||
Short: "start hrp server",
|
||||
Long: `start hrp server. exec automation by http`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return server.NewServer(port)
|
||||
},
|
||||
}
|
||||
|
||||
var port int
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
serverCmd.Flags().IntVarP(&port, "port", "p", 8082, "Port to run the server on")
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/wiki"
|
||||
)
|
||||
|
||||
var wikiCmd = &cobra.Command{
|
||||
Use: "wiki",
|
||||
Aliases: []string{"info", "docs", "doc"},
|
||||
Short: "visit https://httprunner.com",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_wiki", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
return wiki.OpenWiki()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(wikiCmd)
|
||||
}
|
||||
243
hrp/code/code.go
243
hrp/code/code.go
@@ -1,243 +0,0 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// general: [0, 2)
|
||||
const (
|
||||
Success = 0
|
||||
GeneralFail = 1
|
||||
)
|
||||
|
||||
// environment: [2, 10)
|
||||
var (
|
||||
ConfigureError = errors.New("configure error") // 3
|
||||
UnauthorizedError = errors.New("unauthorized error") // 4
|
||||
NetworkError = errors.New("network error") // 5
|
||||
NetworkConfigureError = errors.New("network configure error") // 6
|
||||
PanicError = errors.New("panic error") // 7
|
||||
InvalidPython3Venv = errors.New("prepare python3 venv failed") // 9
|
||||
)
|
||||
|
||||
// loader: [10, 20)
|
||||
var (
|
||||
LoadFileError = errors.New("load file error") // 10
|
||||
LoadJSONError = errors.New("load json error") // 11
|
||||
LoadYAMLError = errors.New("load yaml error") // 12
|
||||
LoadEnvError = errors.New("load .env error") // 13
|
||||
LoadCSVError = errors.New("load csv error") // 14
|
||||
InvalidCaseError = errors.New("invalid case error") // 15
|
||||
UnsupportedFileExtension = errors.New("unsupported file extension") // 16
|
||||
ReferencedFileNotFound = errors.New("referenced file not found") // 17
|
||||
InvalidPluginFile = errors.New("invalid plugin file") // 18
|
||||
InvalidParamError = errors.New("invalid param error") // 19
|
||||
)
|
||||
|
||||
// parser: [20, 30)
|
||||
var (
|
||||
ParseError = errors.New("parse error") // 20
|
||||
VariableNotFound = errors.New("variable not found") // 21
|
||||
ParseFunctionError = errors.New("parse function failed") // 22
|
||||
CallFunctionError = errors.New("call function failed") // 23
|
||||
ParseVariablesError = errors.New("parse variables failed") // 24
|
||||
FFmpegError = errors.New("ffmpeg error") // 25
|
||||
FFprobeError = errors.New("ffprobe error") // 26
|
||||
)
|
||||
|
||||
// runner: [30, 40)
|
||||
var (
|
||||
StartRunnerFailed = errors.New("start runner failed") // 30
|
||||
InitPluginFailed = errors.New("init plugin failed") // 31
|
||||
BuildGoPluginFailed = errors.New("build go plugin failed") // 32
|
||||
BuildPyPluginFailed = errors.New("build py plugin failed") // 33
|
||||
MaxRetryError = errors.New("max retry error") // 37
|
||||
InterruptError = errors.New("interrupt error") // 38
|
||||
TimeoutError = errors.New("timeout error") // 39
|
||||
)
|
||||
|
||||
// summary: [40, 50)
|
||||
var (
|
||||
GenSummaryFailed = errors.New("generate summary failed") // 40
|
||||
CollectValidResultFailed = errors.New("no valid result") // 44
|
||||
DownloadFailed = errors.New("download failed") // 48
|
||||
UploadFailed = errors.New("upload failed") // 49
|
||||
)
|
||||
|
||||
// device related: [50, 70)
|
||||
var (
|
||||
DeviceConnectionError = errors.New("device general connection error") // 50
|
||||
DeviceHTTPDriverError = errors.New("device HTTP driver error") // 51
|
||||
DeviceUSBDriverError = errors.New("device USB driver error") // 52
|
||||
DeviceAppNotInstalled = errors.New("device app not installed") // 59
|
||||
DeviceGetInfoError = errors.New("device get info error") // 60
|
||||
DeviceConfigureError = errors.New("device configure error") // 61
|
||||
DeviceShellExecError = errors.New("device shell exec error") // 62
|
||||
DeviceOfflineError = errors.New("device offline") // 63
|
||||
DeviceInstallFailed = errors.New("device install app failed") // 64
|
||||
DeviceScreenShotError = errors.New("device screenshot error") // 65
|
||||
DeviceCaptureLogError = errors.New("device capture log error") // 66
|
||||
DeviceUIResponseSlow = errors.New("device UI response slow") // 67
|
||||
)
|
||||
|
||||
// UI automation related: [70, 80)
|
||||
var (
|
||||
MobileUIDriverAppNotInstalled = errors.New("mobile UI driver app not installed") // 68
|
||||
MobileUIDriverAppCrashed = errors.New("mobile UI driver app crashed") // 69
|
||||
MobileUIDriverError = errors.New("mobile UI driver error") // 70
|
||||
MobileUILaunchAppError = errors.New("mobile UI launch app error") // 71
|
||||
MobileUITapError = errors.New("mobile UI tap error") // 72
|
||||
MobileUISwipeError = errors.New("mobile UI swipe error") // 73
|
||||
MobileUIInputError = errors.New("mobile UI input error") // 74
|
||||
MobileUIValidationError = errors.New("mobile UI validation error") // 75
|
||||
MobileUIAssertForegroundAppError = errors.New("mobile UI assert foreground app error") // 76
|
||||
MobileUIAssertForegroundActivityError = errors.New("mobile UI assert foreground activity error") // 77
|
||||
MobileUIPopupError = errors.New("mobile UI popup error") // 78
|
||||
LoopActionNotFoundError = errors.New("loop action not found error") // 79
|
||||
)
|
||||
|
||||
// AI related: [80, 90)
|
||||
var (
|
||||
CVEnvMissedError = errors.New("CV env missed error") // 80
|
||||
CVRequestError = errors.New("CV prepare request error") // 81
|
||||
CVServiceConnectionError = errors.New("CV service connect error") // 82
|
||||
CVResponseError = errors.New("CV parse response error") // 83
|
||||
CVResultNotFoundError = errors.New("CV result not found") // 84
|
||||
|
||||
StateUnknowError = errors.New("detect state failed") // 85
|
||||
)
|
||||
|
||||
// trackings related: [90, 100)
|
||||
var (
|
||||
TrackingGetError = errors.New("get trackings failed") // 90
|
||||
TrackingFomatError = errors.New("tracking format error") // 91
|
||||
)
|
||||
|
||||
// risk control related: [100, 110)
|
||||
var (
|
||||
RiskControlLogout = errors.New("risk control logout") // 100
|
||||
RiskControlSlideVerification = errors.New("risk control slide verification") // 101
|
||||
RiskControlAccountActivation = errors.New("risk control account activation") // 102
|
||||
)
|
||||
|
||||
var errorsMap = map[error]int{
|
||||
// environment
|
||||
ConfigureError: 3,
|
||||
UnauthorizedError: 4,
|
||||
NetworkError: 5,
|
||||
NetworkConfigureError: 6,
|
||||
PanicError: 7,
|
||||
InvalidPython3Venv: 9,
|
||||
|
||||
// loader
|
||||
LoadFileError: 10,
|
||||
LoadJSONError: 11,
|
||||
LoadYAMLError: 12,
|
||||
LoadEnvError: 13,
|
||||
LoadCSVError: 14,
|
||||
InvalidCaseError: 15,
|
||||
UnsupportedFileExtension: 16,
|
||||
ReferencedFileNotFound: 17,
|
||||
InvalidPluginFile: 18,
|
||||
InvalidParamError: 19,
|
||||
|
||||
// parser
|
||||
ParseError: 20,
|
||||
VariableNotFound: 21,
|
||||
ParseFunctionError: 22,
|
||||
CallFunctionError: 23,
|
||||
ParseVariablesError: 24,
|
||||
FFmpegError: 25,
|
||||
FFprobeError: 26,
|
||||
|
||||
// runner
|
||||
StartRunnerFailed: 30,
|
||||
InitPluginFailed: 31,
|
||||
BuildGoPluginFailed: 32,
|
||||
BuildPyPluginFailed: 33,
|
||||
MaxRetryError: 37,
|
||||
InterruptError: 38,
|
||||
TimeoutError: 39,
|
||||
|
||||
// summary
|
||||
GenSummaryFailed: 40,
|
||||
CollectValidResultFailed: 44,
|
||||
DownloadFailed: 48,
|
||||
UploadFailed: 49,
|
||||
|
||||
// device related
|
||||
DeviceConnectionError: 50,
|
||||
DeviceHTTPDriverError: 51,
|
||||
DeviceUSBDriverError: 52,
|
||||
DeviceAppNotInstalled: 59,
|
||||
DeviceGetInfoError: 60,
|
||||
DeviceConfigureError: 61,
|
||||
DeviceShellExecError: 62,
|
||||
DeviceOfflineError: 63,
|
||||
DeviceInstallFailed: 64,
|
||||
DeviceScreenShotError: 65,
|
||||
DeviceCaptureLogError: 66,
|
||||
DeviceUIResponseSlow: 67,
|
||||
|
||||
// UI automation related
|
||||
MobileUIDriverAppNotInstalled: 68,
|
||||
MobileUIDriverAppCrashed: 69,
|
||||
MobileUIDriverError: 70,
|
||||
MobileUILaunchAppError: 71,
|
||||
MobileUITapError: 72,
|
||||
MobileUISwipeError: 73,
|
||||
MobileUIInputError: 74,
|
||||
MobileUIValidationError: 75,
|
||||
MobileUIAssertForegroundAppError: 76,
|
||||
MobileUIAssertForegroundActivityError: 77,
|
||||
MobileUIPopupError: 78,
|
||||
LoopActionNotFoundError: 79,
|
||||
|
||||
// AI related
|
||||
CVEnvMissedError: 80,
|
||||
CVRequestError: 81,
|
||||
CVServiceConnectionError: 82,
|
||||
CVResponseError: 83,
|
||||
CVResultNotFoundError: 84,
|
||||
StateUnknowError: 85,
|
||||
|
||||
// trackings related
|
||||
TrackingGetError: 90,
|
||||
TrackingFomatError: 91,
|
||||
|
||||
// risk control related
|
||||
RiskControlLogout: 100,
|
||||
RiskControlSlideVerification: 101,
|
||||
RiskControlAccountActivation: 102,
|
||||
}
|
||||
|
||||
func IsErrorPredefined(err error) bool {
|
||||
_, ok := errorsMap[errors.Cause(err)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func GetErrorCode(err error) (errCode int) {
|
||||
if err == nil {
|
||||
return Success
|
||||
}
|
||||
|
||||
e := errors.Cause(err)
|
||||
if code, ok := errorsMap[e]; ok {
|
||||
errCode = code
|
||||
} else {
|
||||
errCode = GeneralFail
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetErrorByCode(errCode int) error {
|
||||
if errCode < 0 {
|
||||
return nil
|
||||
}
|
||||
for key, value := range errorsMap {
|
||||
if value == errCode {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetErrorCode(t *testing.T) {
|
||||
err := LoadYAMLError
|
||||
code := GetErrorCode(err)
|
||||
fmt.Println(code)
|
||||
}
|
||||
|
||||
func TestGetErrorByCode(t *testing.T) {
|
||||
code := 0
|
||||
err := GetErrorByCode(code)
|
||||
fmt.Println("[TestGetErrorByCode]:err:",err)
|
||||
}
|
||||
173
hrp/compat.go
173
hrp/compat.go
@@ -1,173 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ConvertCaseCompatibility converts TestCase compatible with Golang engine style
|
||||
func ConvertCaseCompatibility(tc *TestCaseDef) (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("[MakeCompat] convert compat testcase error: %v", p)
|
||||
}
|
||||
}()
|
||||
for _, step := range tc.Steps {
|
||||
// 1. deal with request body compatibility
|
||||
convertCompatRequestBody(step.Request)
|
||||
|
||||
// 2. deal with validators compatibility
|
||||
err = convertCompatValidator(step.Validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. deal with extract expr including hyphen
|
||||
convertExtract(step.Extract)
|
||||
|
||||
// 4. deal with mobile step compatibility
|
||||
if step.Android != nil {
|
||||
convertCompatMobileStep(step.Android)
|
||||
} else if step.IOS != nil {
|
||||
convertCompatMobileStep(step.IOS)
|
||||
} else if step.Harmony != nil {
|
||||
convertCompatMobileStep(step.Harmony)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCompatRequestBody(request *Request) {
|
||||
if request != nil && request.Body == nil {
|
||||
if request.Json != nil {
|
||||
if request.Headers == nil {
|
||||
request.Headers = make(map[string]string)
|
||||
}
|
||||
request.Headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
request.Body = request.Json
|
||||
request.Json = nil
|
||||
} else if request.Data != nil {
|
||||
request.Body = request.Data
|
||||
request.Data = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convertCompatValidator(Validators []interface{}) (err error) {
|
||||
for i, iValidator := range Validators {
|
||||
if _, ok := iValidator.(Validator); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var validatorMap map[string]interface{}
|
||||
if v, ok := iValidator.(map[string]interface{}); ok {
|
||||
validatorMap = v
|
||||
} else if v, ok := iValidator.(map[interface{}]interface{}); ok {
|
||||
// convert map[interface{}]interface{} to map[string]interface{}
|
||||
validatorMap = make(map[string]interface{})
|
||||
for key, value := range v {
|
||||
strKey := fmt.Sprintf("%v", key)
|
||||
validatorMap[strKey] = value
|
||||
}
|
||||
} else {
|
||||
return errors.Wrap(code.InvalidCaseError,
|
||||
fmt.Sprintf("unexpected validator format: %v", iValidator))
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
iCheck, checkExisted := validatorMap["check"]
|
||||
iAssert, assertExisted := validatorMap["assert"]
|
||||
iExpect, expectExisted := validatorMap["expect"]
|
||||
// validator check priority: Golang > Python engine style
|
||||
if checkExisted && assertExisted && expectExisted {
|
||||
// Golang engine style
|
||||
validator.Check = iCheck.(string)
|
||||
validator.Assert = iAssert.(string)
|
||||
validator.Expect = iExpect
|
||||
if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
|
||||
validator.Message = iMsg.(string)
|
||||
}
|
||||
validator.Check = convertJmespathExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
continue
|
||||
}
|
||||
if len(validatorMap) == 1 {
|
||||
// Python engine style
|
||||
for assertMethod, iValidatorContent := range validatorMap {
|
||||
validatorContent := iValidatorContent.([]interface{})
|
||||
if len(validatorContent) > 3 {
|
||||
return errors.Wrap(code.InvalidCaseError,
|
||||
fmt.Sprintf("unexpected validator format: %v", validatorMap))
|
||||
}
|
||||
validator.Check = validatorContent[0].(string)
|
||||
validator.Assert = assertMethod
|
||||
validator.Expect = validatorContent[1]
|
||||
if len(validatorContent) == 3 {
|
||||
validator.Message = validatorContent[2].(string)
|
||||
}
|
||||
}
|
||||
validator.Check = convertJmespathExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
continue
|
||||
}
|
||||
return errors.Wrap(code.InvalidCaseError,
|
||||
fmt.Sprintf("unexpected validator format: %v", validatorMap))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertExtract deals with extract expr including hyphen
|
||||
func convertExtract(extract map[string]string) {
|
||||
for key, value := range extract {
|
||||
extract[key] = convertJmespathExpr(value)
|
||||
}
|
||||
}
|
||||
|
||||
func convertCompatMobileStep(mobileUI *MobileUI) {
|
||||
if mobileUI == nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(mobileUI.Actions); i++ {
|
||||
ma := mobileUI.Actions[i]
|
||||
actionOptions := uixt.NewActionOptions(ma.GetOptions()...)
|
||||
// append tap_cv params to screenshot_with_ui_types option
|
||||
if ma.Method == uixt.ACTION_TapByCV {
|
||||
uiTypes, _ := builtin.ConvertToStringSlice(ma.Params)
|
||||
ma.ActionOptions.ScreenShotWithUITypes = append(ma.ActionOptions.ScreenShotWithUITypes, uiTypes...)
|
||||
ma.ActionOptions.ScreenShotWithUpload = true
|
||||
}
|
||||
// set default max_retry_times to 10 for swipe_to_tap_texts
|
||||
if ma.Method == uixt.ACTION_SwipeToTapTexts && actionOptions.MaxRetryTimes == 0 {
|
||||
ma.ActionOptions.MaxRetryTimes = 10
|
||||
}
|
||||
// set default max_retry_times to 10 for swipe_to_tap_text
|
||||
if ma.Method == uixt.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
||||
ma.ActionOptions.MaxRetryTimes = 10
|
||||
}
|
||||
if ma.Method == uixt.ACTION_Swipe {
|
||||
ma.ActionOptions.Direction = ma.Params
|
||||
}
|
||||
mobileUI.Actions[i] = ma
|
||||
}
|
||||
}
|
||||
|
||||
// convertJmespathExpr deals with limited jmespath expression conversion
|
||||
func convertJmespathExpr(checkExpr string) string {
|
||||
if strings.Contains(checkExpr, textExtractorSubRegexp) {
|
||||
return checkExpr
|
||||
}
|
||||
checkItems := strings.Split(checkExpr, ".")
|
||||
for i, checkItem := range checkItems {
|
||||
checkItem = strings.Trim(checkItem, "\"")
|
||||
lowerItem := strings.ToLower(checkItem)
|
||||
if strings.HasPrefix(lowerItem, "content-") || lowerItem == "user-agent" {
|
||||
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
|
||||
}
|
||||
}
|
||||
return strings.Join(checkItems, ".")
|
||||
}
|
||||
275
hrp/config.go
275
hrp/config.go
@@ -1,275 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
type IConfig interface {
|
||||
Get() *TConfig
|
||||
}
|
||||
|
||||
// NewConfig returns a new constructed testcase config with specified testcase name.
|
||||
func NewConfig(name string) *TConfig {
|
||||
return &TConfig{
|
||||
Name: name,
|
||||
Environs: make(map[string]string),
|
||||
Variables: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// define struct for testcase config
|
||||
type TConfig struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` // deprecated in v4.1, moved to env
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // public request headers
|
||||
Environs map[string]string `json:"environs,omitempty" yaml:"environs,omitempty"` // environment variables
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"`
|
||||
IOS []*uixt.IOSDevice `json:"ios,omitempty" yaml:"ios,omitempty"`
|
||||
Android []*uixt.AndroidDevice `json:"android,omitempty" yaml:"android,omitempty"`
|
||||
Harmony []*uixt.HarmonyDevice `json:"harmony,omitempty" yaml:"harmony,omitempty"`
|
||||
RequestTimeout float32 `json:"request_timeout,omitempty" yaml:"request_timeout,omitempty"` // request timeout in seconds
|
||||
CaseTimeout float32 `json:"case_timeout,omitempty" yaml:"case_timeout,omitempty"` // testcase timeout in seconds
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
||||
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
|
||||
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
||||
}
|
||||
|
||||
func (c *TConfig) Get() *TConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current testcase.
|
||||
func (c *TConfig) WithVariables(variables map[string]interface{}) *TConfig {
|
||||
c.Variables = variables
|
||||
return c
|
||||
}
|
||||
|
||||
// SetBaseURL sets base URL for current testcase.
|
||||
func (c *TConfig) SetBaseURL(baseURL string) *TConfig {
|
||||
c.BaseURL = baseURL
|
||||
return c
|
||||
}
|
||||
|
||||
// SetHeaders sets global headers for current testcase.
|
||||
func (c *TConfig) SetHeaders(headers map[string]string) *TConfig {
|
||||
c.Headers = headers
|
||||
return c
|
||||
}
|
||||
|
||||
// SetVerifySSL sets whether to verify SSL for current testcase.
|
||||
func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
|
||||
c.Verify = verify
|
||||
return c
|
||||
}
|
||||
|
||||
// WithParameters sets parameters for current testcase.
|
||||
func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
|
||||
c.Parameters = parameters
|
||||
return c
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time config for current testcase.
|
||||
func (c *TConfig) SetThinkTime(strategy thinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
|
||||
c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit}
|
||||
return c
|
||||
}
|
||||
|
||||
// SetRequestTimeout sets request timeout in seconds.
|
||||
func (c *TConfig) SetRequestTimeout(seconds float32) *TConfig {
|
||||
c.RequestTimeout = seconds
|
||||
return c
|
||||
}
|
||||
|
||||
// SetCaseTimeout sets testcase timeout in seconds.
|
||||
func (c *TConfig) SetCaseTimeout(seconds float32) *TConfig {
|
||||
c.CaseTimeout = seconds
|
||||
return c
|
||||
}
|
||||
|
||||
// ExportVars specifies variable names to export for current testcase.
|
||||
func (c *TConfig) ExportVars(vars ...string) *TConfig {
|
||||
c.Export = vars
|
||||
return c
|
||||
}
|
||||
|
||||
// SetWeight sets weight for current testcase, which is used in load testing.
|
||||
func (c *TConfig) SetWeight(weight int) *TConfig {
|
||||
c.Weight = weight
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig {
|
||||
c.WebSocketSetting = &WebSocketConfig{
|
||||
ReconnectionTimes: times,
|
||||
ReconnectionInterval: interval,
|
||||
MaxMessageSize: size,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig {
|
||||
wdaOptions := &uixt.IOSDevice{}
|
||||
for _, option := range options {
|
||||
option(wdaOptions)
|
||||
}
|
||||
|
||||
// each device can have its own settings
|
||||
if wdaOptions.UDID != "" {
|
||||
c.IOS = append(c.IOS, wdaOptions)
|
||||
return c
|
||||
}
|
||||
|
||||
// device UDID is not specified, settings will be shared
|
||||
if len(c.IOS) == 0 {
|
||||
c.IOS = append(c.IOS, wdaOptions)
|
||||
} else {
|
||||
c.IOS[0] = wdaOptions
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) SetHarmony(options ...uixt.HarmonyDeviceOption) *TConfig {
|
||||
harmonyOptions := &uixt.HarmonyDevice{}
|
||||
for _, option := range options {
|
||||
option(harmonyOptions)
|
||||
}
|
||||
|
||||
// each device can have its own settings
|
||||
if harmonyOptions.ConnectKey != "" {
|
||||
c.Harmony = append(c.Harmony, harmonyOptions)
|
||||
return c
|
||||
}
|
||||
|
||||
// device UDID is not specified, settings will be shared
|
||||
if len(c.Harmony) == 0 {
|
||||
c.Harmony = append(c.Harmony, harmonyOptions)
|
||||
} else {
|
||||
c.Harmony[0] = harmonyOptions
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) SetAndroid(options ...uixt.AndroidDeviceOption) *TConfig {
|
||||
uiaOptions := &uixt.AndroidDevice{}
|
||||
for _, option := range options {
|
||||
option(uiaOptions)
|
||||
}
|
||||
|
||||
// each device can have its own settings
|
||||
if uiaOptions.SerialNumber != "" {
|
||||
c.Android = append(c.Android, uiaOptions)
|
||||
return c
|
||||
}
|
||||
|
||||
// device UDID is not specified, settings will be shared
|
||||
if len(c.Android) == 0 {
|
||||
c.Android = append(c.Android, uiaOptions)
|
||||
} else {
|
||||
c.Android[0] = uiaOptions
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// EnablePlugin enables plugin for current testcase.
|
||||
// default to disable plugin
|
||||
func (c *TConfig) EnablePlugin() *TConfig {
|
||||
c.PluginSetting = &PluginConfig{}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) DisableAutoPopupHandler() *TConfig {
|
||||
c.IgnorePopup = true
|
||||
return c
|
||||
}
|
||||
|
||||
type ThinkTimeConfig struct {
|
||||
Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore
|
||||
Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5
|
||||
Limit float64 `json:"limit,omitempty" yaml:"limit,omitempty"` // limit think time no more than specific time, ignore if value <= 0
|
||||
}
|
||||
|
||||
func (ttc *ThinkTimeConfig) checkThinkTime() {
|
||||
if ttc == nil {
|
||||
return
|
||||
}
|
||||
// unset strategy, set default strategy
|
||||
if ttc.Strategy == "" {
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
// check think time
|
||||
if ttc.Strategy == thinkTimeRandomPercentage {
|
||||
if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
value, ok := ttc.Setting.(map[string]interface{})
|
||||
if !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["min_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["max_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
left, err := builtin.Interface2Float64(value["min_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
right, err := builtin.Interface2Float64(value["max_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right}
|
||||
} else if ttc.Strategy == thinkTimeMultiply {
|
||||
if ttc.Setting == nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
value, err := builtin.Interface2Float64(ttc.Setting)
|
||||
if err != nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
ttc.Setting = value
|
||||
} else if ttc.Strategy != thinkTimeIgnore {
|
||||
// unrecognized strategy, set default strategy
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
}
|
||||
|
||||
type thinkTimeStrategy string
|
||||
|
||||
const (
|
||||
thinkTimeDefault thinkTimeStrategy = "default" // as recorded
|
||||
thinkTimeRandomPercentage thinkTimeStrategy = "random_percentage" // use random percentage of recorded think time
|
||||
thinkTimeMultiply thinkTimeStrategy = "multiply" // multiply recorded think time
|
||||
thinkTimeIgnore thinkTimeStrategy = "ignore" // ignore recorded think time
|
||||
)
|
||||
|
||||
const (
|
||||
thinkTimeDefaultMultiply = 1
|
||||
)
|
||||
|
||||
var thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5}
|
||||
|
||||
type PluginConfig struct {
|
||||
Path string
|
||||
Type string // bin、so、py
|
||||
Content []byte
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
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, ",")
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
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/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/json"
|
||||
)
|
||||
|
||||
func Dump2JSON(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to json")
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
package pytest
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
)
|
||||
|
||||
func RunPytest(args []string) error {
|
||||
args = append([]string{"run"}, args...)
|
||||
return myexec.ExecPython3Command("httprunner", args...)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v5/hrp/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
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
base_url=https://postman-echo.com
|
||||
USERNAME=debugtalk
|
||||
PASSWORD=123456
|
||||
@@ -1,14 +0,0 @@
|
||||
reports/
|
||||
*.so
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
output/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.python-version
|
||||
logs/
|
||||
|
||||
# plugin
|
||||
debugtalk.bin
|
||||
debugtalk.so
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,44 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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}"
|
||||
@@ -1,13 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
[pytest]
|
||||
addopts = -s
|
||||
# https://docs.pytest.org/en/latest/how-to/output.html
|
||||
junit_logging = all
|
||||
junit_duration_report = total
|
||||
log_cli = False
|
||||
@@ -1,359 +0,0 @@
|
||||
<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 +0,0 @@
|
||||
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
config:
|
||||
name: "request methods testcase: empty testcase"
|
||||
variables:
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
- name:
|
||||
variables:
|
||||
request:
|
||||
method: GET
|
||||
url: "https://"
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"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\""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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"]
|
||||
@@ -1,60 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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"]
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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
|
||||
@@ -1,170 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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
|
||||
@@ -1,211 +0,0 @@
|
||||
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/hrp/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")
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/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 +0,0 @@
|
||||
v5.0.0+2502052133
|
||||
@@ -1,13 +0,0 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed VERSION
|
||||
var VERSION string
|
||||
|
||||
func init() {
|
||||
VERSION = strings.TrimSpace(VERSION)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "open"
|
||||
@@ -1,3 +0,0 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "xdg-open"
|
||||
@@ -1,3 +0,0 @@
|
||||
package wiki
|
||||
|
||||
const openCmd = "explorer"
|
||||
163
hrp/loader.go
163
hrp/loader.go
@@ -1,163 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/json"
|
||||
)
|
||||
|
||||
// LoadTestCases load testcases from TestCasePath or TestCase
|
||||
func LoadTestCases(tests ...ITestCase) ([]*TestCase, error) {
|
||||
testCases := make([]*TestCase, 0)
|
||||
|
||||
for _, iTestCase := range tests {
|
||||
if testcase, ok := iTestCase.(*TestCase); ok {
|
||||
testCases = append(testCases, testcase)
|
||||
continue
|
||||
}
|
||||
|
||||
if testcase, ok := iTestCase.(*TestCaseJSON); ok {
|
||||
tc, err := testcase.GetTestCase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
testCases = append(testCases, tc)
|
||||
continue
|
||||
}
|
||||
|
||||
// iTestCase should be a TestCasePath, file path or folder path
|
||||
tcPath, ok := iTestCase.(*TestCasePath)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid iTestCase type")
|
||||
}
|
||||
|
||||
casePath := string(*tcPath)
|
||||
err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error {
|
||||
if dir == nil {
|
||||
// casePath is a file other than a dir
|
||||
path = casePath
|
||||
} else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") {
|
||||
// skip hidden folders
|
||||
return fs.SkipDir
|
||||
} else {
|
||||
// casePath is a dir
|
||||
path = filepath.Join(casePath, path)
|
||||
}
|
||||
|
||||
// ignore non-testcase files
|
||||
ext := filepath.Ext(path)
|
||||
if ext != ".yml" && ext != ".yaml" && ext != ".json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// filtered testcases
|
||||
testCasePath := TestCasePath(path)
|
||||
tc, err := testCasePath.GetTestCase()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("path", path).Msg("load testcase failed")
|
||||
return nil
|
||||
}
|
||||
testCases = append(testCases, tc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read dir failed")
|
||||
}
|
||||
}
|
||||
|
||||
if len(testCases) < 1 {
|
||||
return nil, errors.New("test case count less than 1 or parse error")
|
||||
}
|
||||
|
||||
log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
|
||||
return testCases, nil
|
||||
}
|
||||
|
||||
// LoadFileObject loads file content with file extension and assigns to structObj
|
||||
func LoadFileObject(path string, structObj interface{}) (err error) {
|
||||
log.Info().Str("path", path).Msg("load file")
|
||||
file, err := readFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read file failed")
|
||||
}
|
||||
// remove BOM at the beginning of file
|
||||
file = bytes.TrimLeft(file, "\xef\xbb\xbf")
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".json", ".har":
|
||||
decoder := json.NewDecoder(bytes.NewReader(file))
|
||||
decoder.UseNumber()
|
||||
err = decoder.Decode(structObj)
|
||||
if err != nil {
|
||||
err = errors.Wrap(code.LoadJSONError, err.Error())
|
||||
}
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(file, structObj)
|
||||
if err != nil {
|
||||
err = errors.Wrap(code.LoadYAMLError, err.Error())
|
||||
}
|
||||
case ".env":
|
||||
err = parseEnvContent(file, structObj)
|
||||
if err != nil {
|
||||
err = errors.Wrap(code.LoadEnvError, err.Error())
|
||||
}
|
||||
default:
|
||||
err = code.UnsupportedFileExtension
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseEnvContent(file []byte, obj interface{}) error {
|
||||
envMap := obj.(map[string]string)
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
// empty line or comment line
|
||||
continue
|
||||
}
|
||||
var kv []string
|
||||
if strings.Contains(line, "=") {
|
||||
kv = strings.SplitN(line, "=", 2)
|
||||
} else if strings.Contains(line, ":") {
|
||||
kv = strings.SplitN(line, ":", 2)
|
||||
}
|
||||
if len(kv) != 2 {
|
||||
return errors.New(".env format error")
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(kv[0])
|
||||
value := strings.TrimSpace(kv[1])
|
||||
envMap[key] = value
|
||||
|
||||
// set env
|
||||
log.Info().Str("key", key).Msg("set env")
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
var err error
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("convert absolute path failed")
|
||||
return nil, errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read file failed")
|
||||
return nil, errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadTestCases(t *testing.T) {
|
||||
// load test cases from folder path
|
||||
tc := TestCasePath("../examples/demo-with-py-plugin/testcases/")
|
||||
testCases, err := LoadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 4, len(testCases)) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from folder path, including sub folders
|
||||
tc = TestCasePath("../examples/demo-with-py-plugin/")
|
||||
testCases, err = LoadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 4, len(testCases)) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from single file path
|
||||
tc = TestCasePath(demoTestCaseWithPluginJSONPath)
|
||||
testCases, err = LoadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 1, len(testCases)) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from TestCase instance
|
||||
testcase := &TestCase{
|
||||
Config: NewConfig("TestCase").SetWeight(3),
|
||||
}
|
||||
testCases, err = LoadTestCases(testcase)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, len(testCases), 1) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from TestCaseJSON
|
||||
testcaseJSON := TestCaseJSON(`
|
||||
{
|
||||
"config":{"name":"TestCaseJSON"},
|
||||
"teststeps":[
|
||||
{"name": "step1", "request":{"url": "https://httpbin.org/get"}},
|
||||
{"name": "step2", "shell":{"string": "ls -l"}}
|
||||
]
|
||||
}`)
|
||||
testCases, err = LoadTestCases(&testcaseJSON)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, len(testCases), 1) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCase(t *testing.T) {
|
||||
tcJSON := &TestCaseDef{}
|
||||
tcYAML := &TestCaseDef{}
|
||||
err := LoadFileObject(demoTestCaseWithPluginJSONPath, tcJSON)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
err = LoadFileObject(demoTestCaseWithPluginYAMLPath, tcYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.Steps[1].StepName, tcYAML.Steps[1].StepName) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.Steps[1].Request, tcJSON.Steps[1].Request) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func InitLogger(logLevel string, logJSON bool) {
|
||||
// Error Logging with Stacktrace
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
// set log timestamp precise to milliseconds
|
||||
zerolog.TimeFieldFormat = "2006-01-02T15:04:05.999Z0700"
|
||||
|
||||
// init log writer
|
||||
var msg string
|
||||
var writer io.Writer
|
||||
if !logJSON {
|
||||
// log a human-friendly, colorized output
|
||||
noColor := false
|
||||
if runtime.GOOS == "windows" {
|
||||
noColor = true
|
||||
}
|
||||
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: time.RFC3339Nano,
|
||||
NoColor: noColor,
|
||||
}
|
||||
msg = "log with colorized console"
|
||||
} else {
|
||||
// default logger
|
||||
writer = os.Stderr
|
||||
msg = "log with json output"
|
||||
}
|
||||
log.Logger = zerolog.New(writer).With().Timestamp().Logger()
|
||||
log.Info().Msg(msg)
|
||||
|
||||
// Setting Global Log Level
|
||||
level := strings.ToUpper(logLevel)
|
||||
log.Info().Str("log_level", level).Msg("set global log level")
|
||||
switch level {
|
||||
case "DEBUG":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "INFO":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "WARN":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "ERROR":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
case "FATAL":
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
case "PANIC":
|
||||
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type TParamsConfig struct {
|
||||
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` // overall pick-order strategy
|
||||
Strategies map[string]iteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters
|
||||
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
|
||||
}
|
||||
|
||||
type iteratorPickOrder string
|
||||
|
||||
const (
|
||||
pickOrderSequential iteratorPickOrder = "sequential"
|
||||
pickOrderRandom iteratorPickOrder = "random"
|
||||
pickOrderUnique iteratorPickOrder = "unique"
|
||||
)
|
||||
|
||||
/*
|
||||
[
|
||||
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
|
||||
]
|
||||
*/
|
||||
type Parameters []map[string]interface{}
|
||||
|
||||
type iteratorStrategy struct {
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Parser) initParametersIterator(cfg *TConfig) (*ParametersIterator, error) {
|
||||
parameters, err := p.loadParameters(cfg.Parameters, cfg.Variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newParametersIterator(parameters, cfg.ParametersSetting), nil
|
||||
}
|
||||
|
||||
func newParametersIterator(parameters map[string]Parameters, config *TParamsConfig) *ParametersIterator {
|
||||
if config == nil {
|
||||
config = &TParamsConfig{}
|
||||
}
|
||||
iterator := &ParametersIterator{
|
||||
data: parameters,
|
||||
hasNext: true,
|
||||
sequentialParameters: nil,
|
||||
randomParameterNames: nil,
|
||||
limit: config.Limit,
|
||||
index: 0,
|
||||
}
|
||||
|
||||
if len(parameters) == 0 {
|
||||
iterator.data = map[string]Parameters{}
|
||||
iterator.limit = 1
|
||||
return iterator
|
||||
}
|
||||
|
||||
parametersList := make([]Parameters, 0)
|
||||
for paramName := range parameters {
|
||||
// check parameter individual pick order strategy
|
||||
strategy, ok := config.Strategies[paramName]
|
||||
if !ok || strategy.PickOrder == "" {
|
||||
// default to overall pick order strategy
|
||||
strategy.PickOrder = config.PickOrder
|
||||
}
|
||||
|
||||
// group parameters by pick order strategy
|
||||
if strategy.PickOrder == pickOrderRandom {
|
||||
iterator.randomParameterNames = append(iterator.randomParameterNames, paramName)
|
||||
} else {
|
||||
parametersList = append(parametersList, parameters[paramName])
|
||||
}
|
||||
}
|
||||
|
||||
// generate cartesian product for sequential parameters
|
||||
iterator.sequentialParameters = genCartesianProduct(parametersList)
|
||||
|
||||
if iterator.limit < 0 {
|
||||
log.Warn().Msg("parameters unlimited mode is only supported for load testing")
|
||||
iterator.limit = 0
|
||||
}
|
||||
if iterator.limit == 0 {
|
||||
// limit not set
|
||||
if len(iterator.sequentialParameters) > 0 {
|
||||
// use cartesian product of sequential parameters size as limit
|
||||
iterator.limit = len(iterator.sequentialParameters)
|
||||
} else {
|
||||
// all parameters are selected by random
|
||||
// only run once
|
||||
iterator.limit = 1
|
||||
}
|
||||
} else { // limit > 0
|
||||
log.Info().Int("limit", iterator.limit).Msg("set limit for parameters")
|
||||
}
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
type ParametersIterator struct {
|
||||
sync.Mutex
|
||||
data map[string]Parameters
|
||||
hasNext bool // cache query result
|
||||
sequentialParameters Parameters // cartesian product for sequential parameters
|
||||
randomParameterNames []string // value is parameter names
|
||||
limit int // limit count for iteration
|
||||
index int // current iteration index
|
||||
}
|
||||
|
||||
// SetUnlimitedMode is used for load testing
|
||||
func (iter *ParametersIterator) SetUnlimitedMode() {
|
||||
log.Info().Msg("set parameters unlimited mode")
|
||||
iter.limit = -1
|
||||
}
|
||||
|
||||
func (iter *ParametersIterator) HasNext() bool {
|
||||
if !iter.hasNext {
|
||||
return false
|
||||
}
|
||||
|
||||
// unlimited mode
|
||||
if iter.limit == -1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// reached limit
|
||||
if iter.index >= iter.limit {
|
||||
// cache query result
|
||||
iter.hasNext = false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (iter *ParametersIterator) Next() map[string]interface{} {
|
||||
iter.Lock()
|
||||
defer iter.Unlock()
|
||||
|
||||
if !iter.hasNext {
|
||||
return nil
|
||||
}
|
||||
|
||||
var selectedParameters map[string]interface{}
|
||||
if len(iter.sequentialParameters) == 0 {
|
||||
selectedParameters = make(map[string]interface{})
|
||||
} else if iter.index < len(iter.sequentialParameters) {
|
||||
selectedParameters = iter.sequentialParameters[iter.index]
|
||||
} else {
|
||||
// loop back to the first sequential parameter
|
||||
index := iter.index % len(iter.sequentialParameters)
|
||||
selectedParameters = iter.sequentialParameters[index]
|
||||
}
|
||||
|
||||
// merge with random parameters
|
||||
for _, paramName := range iter.randomParameterNames {
|
||||
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
randIndex := randSource.Intn(len(iter.data[paramName]))
|
||||
for k, v := range iter.data[paramName][randIndex] {
|
||||
selectedParameters[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
iter.index++
|
||||
if iter.limit > 0 && iter.index >= iter.limit {
|
||||
iter.hasNext = false
|
||||
}
|
||||
|
||||
return selectedParameters
|
||||
}
|
||||
|
||||
func (iter *ParametersIterator) Data() map[string]interface{} {
|
||||
res := map[string]interface{}{}
|
||||
for key, params := range iter.data {
|
||||
res[key] = params
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func genCartesianProduct(multiParameters []Parameters) Parameters {
|
||||
if len(multiParameters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cartesianProduct := multiParameters[0]
|
||||
for i := 0; i < len(multiParameters)-1; i++ {
|
||||
var tempProduct Parameters
|
||||
for _, param1 := range cartesianProduct {
|
||||
for _, param2 := range multiParameters[i+1] {
|
||||
tempProduct = append(tempProduct, mergeVariables(param1, param2))
|
||||
}
|
||||
}
|
||||
cartesianProduct = tempProduct
|
||||
}
|
||||
|
||||
return cartesianProduct
|
||||
}
|
||||
|
||||
/*
|
||||
loadParameters loads parameters from multiple sources.
|
||||
|
||||
parameter value may be in three types:
|
||||
|
||||
(1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
|
||||
(2) call built-in parameterize function, "${parameterize(account.csv)}"
|
||||
(3) call custom function in debugtalk.py, "${gen_app_version()}"
|
||||
|
||||
configParameters = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1
|
||||
"username-password": "${parameterize(account.csv)}", // case 2
|
||||
"app_version": "${gen_app_version()}", // case 3
|
||||
}
|
||||
|
||||
=>
|
||||
|
||||
{
|
||||
"user_agent": [
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
{"user_agent": "iOS/10.3"},
|
||||
],
|
||||
"username-password": [
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
],
|
||||
"app_version": [
|
||||
{"app_version": "1.0.0"},
|
||||
{"app_version": "1.0.1"},
|
||||
]
|
||||
}
|
||||
*/
|
||||
func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
|
||||
map[string]Parameters, error) {
|
||||
|
||||
if len(configParameters) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
parsedParameters := make(map[string]Parameters)
|
||||
|
||||
for k, v := range configParameters {
|
||||
var parametersRawList interface{}
|
||||
rawValue := reflect.ValueOf(v)
|
||||
|
||||
switch rawValue.Kind() {
|
||||
case reflect.Slice:
|
||||
// case 1
|
||||
// e.g. user_agent: ["iOS/10.1", "iOS/10.2"]
|
||||
// => ["iOS/10.1", "iOS/10.2"]
|
||||
parametersRawList = rawValue.Interface()
|
||||
|
||||
case reflect.String:
|
||||
// case 2 or case 3
|
||||
// e.g. username-password: ${parameterize(examples/hrp/account.csv)}
|
||||
// => [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
|
||||
// => [["test1", "111111"], ["test2", "222222"]]
|
||||
// e.g. "app_version": "${gen_app_version()}"
|
||||
// => ["1.0.0", "1.0.1"]
|
||||
parsedParameterContent, err := p.ParseString(rawValue.String(), variablesMapping)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("parametersRawContent", rawValue.String()).
|
||||
Msg("parse parameters content failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedParameterRawValue := reflect.ValueOf(parsedParameterContent)
|
||||
if parsedParameterRawValue.Kind() != reflect.Slice {
|
||||
log.Error().
|
||||
Interface("parsedParameterContent", parsedParameterRawValue).
|
||||
Msg("parsed parameters content is not slice")
|
||||
return nil, errors.New("parsed parameters content should be slice")
|
||||
}
|
||||
parametersRawList = parsedParameterRawValue.Interface()
|
||||
|
||||
default:
|
||||
log.Error().
|
||||
Interface("parameters", configParameters).
|
||||
Msg("config parameters raw value should be slice or string (functions call)")
|
||||
return nil, errors.New("config parameters raw value format error")
|
||||
}
|
||||
|
||||
parameterSlice, err := convertParameters(k, parametersRawList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedParameters[k] = parameterSlice
|
||||
}
|
||||
return parsedParameters, nil
|
||||
}
|
||||
|
||||
/*
|
||||
convert parameters to standard format
|
||||
|
||||
key and parametersRawList may be in three types:
|
||||
|
||||
case 1:
|
||||
|
||||
key = "user_agent"
|
||||
parametersRawList = ["iOS/10.1", "iOS/10.2"]
|
||||
|
||||
case 2:
|
||||
|
||||
key = "username-password"
|
||||
parametersRawList = [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
|
||||
|
||||
case 3:
|
||||
|
||||
key = "username-password"
|
||||
parametersRawList = [["test1", "111111"], ["test2", "222222"]]
|
||||
*/
|
||||
func convertParameters(key string, parametersRawList interface{}) (parameterSlice []map[string]interface{}, err error) {
|
||||
parametersRawSlice := reflect.ValueOf(parametersRawList)
|
||||
if parametersRawSlice.Kind() != reflect.Slice {
|
||||
return nil, errors.New("parameters raw value is not list")
|
||||
}
|
||||
|
||||
// ["user_agent"], ["username", "password"], ["app_version"]
|
||||
parameterNames := strings.Split(key, "-")
|
||||
|
||||
for i := 0; i < parametersRawSlice.Len(); i++ {
|
||||
parametersLine := make(map[string]interface{})
|
||||
elem := parametersRawSlice.Index(i)
|
||||
// e.g. Type: interface{} | []interface{}, convert interface{} to []interface{}
|
||||
if elem.Kind() == reflect.Interface {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
switch elem.Kind() {
|
||||
case reflect.Slice:
|
||||
// case 3
|
||||
// e.g. "username-password": ["test1", "111111"]
|
||||
// => {"username": "test1", "password": "111111"}
|
||||
if len(parameterNames) != elem.Len() {
|
||||
log.Error().
|
||||
Strs("parameterNames", parameterNames).
|
||||
Int("lineIndex", i).
|
||||
Interface("content", elem.Interface()).
|
||||
Msg("parameters line length does not match to names length")
|
||||
return nil, errors.New("parameters line length does not match to names length")
|
||||
}
|
||||
|
||||
for j := 0; j < elem.Len(); j++ {
|
||||
parametersLine[parameterNames[j]] = elem.Index(j).Interface()
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
// case 2
|
||||
// e.g. "username-password": {"username": "test1", "password": "111111", "other": "111"}
|
||||
// => {"username": "test1", "password": "passwd1"}
|
||||
for _, name := range parameterNames {
|
||||
lineMap := elem.Interface().(map[string]interface{})
|
||||
if _, ok := lineMap[name]; ok {
|
||||
parametersLine[name] = elem.MapIndex(reflect.ValueOf(name)).Interface()
|
||||
} else {
|
||||
log.Error().
|
||||
Strs("parameterNames", parameterNames).
|
||||
Str("name", name).
|
||||
Msg("parameter name not found")
|
||||
return nil, errors.New("parameter name not found")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// case 1
|
||||
// e.g. "user_agent": "iOS/10.1"
|
||||
// -> {"user_agent": "iOS/10.1"}
|
||||
if len(parameterNames) != 1 {
|
||||
log.Error().
|
||||
Strs("parameterNames", parameterNames).
|
||||
Int("lineIndex", i).
|
||||
Msg("parameters format error")
|
||||
return nil, errors.New("parameters format error")
|
||||
}
|
||||
parametersLine[parameterNames[0]] = elem.Interface()
|
||||
}
|
||||
parameterSlice = append(parameterSlice, parametersLine)
|
||||
}
|
||||
return parameterSlice, nil
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadParameters(t *testing.T) {
|
||||
testData := []struct {
|
||||
configParameters map[string]interface{}
|
||||
loadedParameters map[string]Parameters
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/$file)}", hrpExamplesDir),
|
||||
},
|
||||
map[string]Parameters{
|
||||
"username-password": {
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
{"username": "test3", "password": "333333"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": [][]interface{}{
|
||||
{"test1", "111111"},
|
||||
{"test2", "222222"},
|
||||
},
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
"app_version": []interface{}{4.0},
|
||||
},
|
||||
map[string]Parameters{
|
||||
"username-password": {
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
},
|
||||
"user_agent": {
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
},
|
||||
"app_version": {
|
||||
{"app_version": 4.0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": []interface{}{
|
||||
[]interface{}{"test1", "111111"},
|
||||
[]interface{}{"test2", "222222"},
|
||||
},
|
||||
},
|
||||
map[string]Parameters{
|
||||
"username-password": {
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
variablesMapping := map[string]interface{}{
|
||||
"file": "account.csv",
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
value, err := parser.loadParameters(data.configParameters, variablesMapping)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.loadedParameters, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadParametersError(t *testing.T) {
|
||||
testData := []struct {
|
||||
configParameters map[string]interface{}
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username_password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir),
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir),
|
||||
"user-agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${param(%s/account.csv)}", hrpExamplesDir),
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
_, err := parser.loadParameters(data.configParameters, map[string]interface{}{})
|
||||
if !assert.Error(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitParametersIteratorCount(t *testing.T) {
|
||||
configParameters := map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), // 3
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
expectLimit int
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
// default equals to set overall parameters pick-order to "sequential"
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
PickOrder: "sequential",
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
// default equals to set each individual parameters pick-order to "sequential"
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": {Name: "user-info", PickOrder: "sequential"},
|
||||
"user_agent": {Name: "user-identity", PickOrder: "sequential"},
|
||||
"app_version": {Name: "app-version", PickOrder: "sequential"},
|
||||
},
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"user_agent": {Name: "user-identity", PickOrder: "sequential"},
|
||||
"app_version": {Name: "app-version", PickOrder: "sequential"},
|
||||
},
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
|
||||
// set overall parameters overall pick-order to "random"
|
||||
// each random parameters only select one item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
PickOrder: "random",
|
||||
},
|
||||
},
|
||||
1, // 1 * 1 * 1
|
||||
},
|
||||
// set some individual parameters pick-order to "random"
|
||||
// this will override overall strategy
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"user_agent": {Name: "user-identity", PickOrder: "random"},
|
||||
},
|
||||
},
|
||||
},
|
||||
3, // 3 * 1 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": {Name: "user-info", PickOrder: "random"},
|
||||
},
|
||||
},
|
||||
},
|
||||
2, // 1 * 2 * 1
|
||||
},
|
||||
|
||||
// set limit for parameters
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters, // total: 6 = 3 * 2 * 1
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 4, // limit could be less than total
|
||||
},
|
||||
},
|
||||
4,
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters, // total: 6 = 3 * 2 * 1
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 9, // limit could also be greater than total
|
||||
},
|
||||
},
|
||||
9,
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
1,
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectLimit, iterator.limit) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
for i := 0; i < data.expectLimit; i++ {
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
// should not have next
|
||||
if !assert.False(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitParametersIteratorUnlimitedCount(t *testing.T) {
|
||||
configParameters := map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), // 3
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{},
|
||||
},
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
// set unlimited mode
|
||||
iterator.SetUnlimitedMode()
|
||||
if !assert.Equal(t, -1, iterator.limit) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
if !assert.Equal(t, 100, iterator.index) {
|
||||
t.Fatal()
|
||||
}
|
||||
// should also have next
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitParametersIteratorContent(t *testing.T) {
|
||||
configParameters := map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), // 3
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
checkIndex int
|
||||
expectParameters map[string]interface{}
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
},
|
||||
0, // check first item
|
||||
map[string]interface{}{
|
||||
"username": "test1", "password": "111111", "user_agent": "iOS/10.1", "app_version": 4.0,
|
||||
},
|
||||
},
|
||||
|
||||
// set limit for parameters
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: map[string]interface{}{
|
||||
"username-password": []map[string]interface{}{ // 1
|
||||
{"username": "test1", "password": 111111, "other": "111"},
|
||||
},
|
||||
"user_agent": []string{"iOS/10.1", "iOS/10.2"}, // 2
|
||||
},
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 5, // limit could also be greater than total
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": {Name: "user-info", PickOrder: "random"},
|
||||
},
|
||||
},
|
||||
},
|
||||
2, // check 3th item, equals to the first item
|
||||
map[string]interface{}{
|
||||
"username": "test1", "password": 111111, "user_agent": "iOS/10.1",
|
||||
},
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
0,
|
||||
map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// get expected parameters item
|
||||
for i := 0; i < data.checkIndex; i++ {
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
parametersItem := iterator.Next()
|
||||
|
||||
if !assert.Equal(t, data.expectParameters, parametersItem) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenCartesianProduct(t *testing.T) {
|
||||
testData := []struct {
|
||||
multiParameters []Parameters
|
||||
expect Parameters
|
||||
}{
|
||||
{
|
||||
[]Parameters{
|
||||
{
|
||||
{"app_version": 4.0},
|
||||
},
|
||||
{
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
},
|
||||
{
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
Parameters{
|
||||
{"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.1", "username": "test1"},
|
||||
{"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.2", "username": "test1"},
|
||||
{"app_version": 4.0, "password": "222222", "user_agent": "iOS/10.1", "username": "test2"},
|
||||
{"app_version": 4.0, "password": "222222", "user_agent": "iOS/10.2", "username": "test2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]Parameters{},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
parameters := genCartesianProduct(data.multiParameters)
|
||||
if !assert.Equal(t, data.expect, parameters) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertParameters(t *testing.T) {
|
||||
testData := []struct {
|
||||
key string
|
||||
parametersRawList interface{}
|
||||
expect []map[string]interface{}
|
||||
}{
|
||||
{
|
||||
"username-password",
|
||||
[]map[string]interface{}{
|
||||
{"username": "test1", "password": 111111, "other": "111"},
|
||||
{"username": "test2", "password": 222222, "other": "222"},
|
||||
},
|
||||
[]map[string]interface{}{
|
||||
{"username": "test1", "password": 111111},
|
||||
{"username": "test2", "password": 222222},
|
||||
},
|
||||
},
|
||||
{
|
||||
"username-password",
|
||||
[][]string{
|
||||
{"test1", "111111"},
|
||||
{"test2", "222222"},
|
||||
},
|
||||
[]map[string]interface{}{
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"app_version",
|
||||
[]float64{3.1, 3.0},
|
||||
[]map[string]interface{}{
|
||||
{"app_version": 3.1},
|
||||
{"app_version": 3.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"user_agent",
|
||||
[]string{"iOS/10.1", "iOS/10.2"},
|
||||
[]map[string]interface{}{
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
value, err := convertParameters(data.key, data.parametersRawList)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertParametersError(t *testing.T) {
|
||||
testData := []struct {
|
||||
key string
|
||||
parametersRawList interface{}
|
||||
}{
|
||||
{
|
||||
"app_version",
|
||||
123, // not slice
|
||||
},
|
||||
{
|
||||
"app_version",
|
||||
"123", // not slice
|
||||
},
|
||||
{
|
||||
"username-password",
|
||||
[]map[string]interface{}{ // parameter names not match
|
||||
{"username": "test1", "other": "111"},
|
||||
{"username": "test2", "other": "222"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"username-password",
|
||||
[][]string{ // parameter names length not match
|
||||
{"test1"},
|
||||
{"test2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
_, err := convertParameters(data.key, data.parametersRawList)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
568
hrp/parser.go
568
hrp/parser.go
@@ -1,568 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/httprunner/funplugin"
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
"github.com/maja42/goval"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp/code"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
func newParser() *Parser {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
Plugin funplugin.IPlugin // plugin is used to call functions
|
||||
}
|
||||
|
||||
func buildURL(baseURL, stepURL string, queryParams url.Values) (fullUrl *url.URL) {
|
||||
uStep, err := url.Parse(stepURL)
|
||||
if err != nil {
|
||||
log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse url failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// append query params
|
||||
if paramStr := queryParams.Encode(); paramStr != "" {
|
||||
if uStep.RawQuery == "" {
|
||||
uStep.RawQuery = paramStr
|
||||
} else {
|
||||
uStep.RawQuery = uStep.RawQuery + "&" + paramStr
|
||||
}
|
||||
}
|
||||
|
||||
// ensure path suffix '/' exists
|
||||
if uStep.RawQuery == "" {
|
||||
uStep.Path = strings.TrimRight(uStep.Path, "/") + "/"
|
||||
}
|
||||
|
||||
fullUrl = uStep
|
||||
}()
|
||||
|
||||
// step url is absolute url
|
||||
if uStep.Host != "" {
|
||||
return uStep
|
||||
}
|
||||
|
||||
// step url is relative, based on base url
|
||||
uConfig, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed")
|
||||
return
|
||||
}
|
||||
|
||||
// merge url
|
||||
uStep.Scheme = uConfig.Scheme
|
||||
uStep.Host = uConfig.Host
|
||||
uStep.Path = path.Join(uConfig.Path, uStep.Path)
|
||||
return uStep
|
||||
}
|
||||
|
||||
func (p *Parser) ParseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) {
|
||||
parsedHeaders := make(map[string]string)
|
||||
headers, err := p.Parse(rawHeaders, variablesMapping)
|
||||
if err != nil {
|
||||
return rawHeaders, err
|
||||
}
|
||||
for k, v := range headers.(map[string]interface{}) {
|
||||
parsedHeaders[k] = convertString(v)
|
||||
}
|
||||
return parsedHeaders, nil
|
||||
}
|
||||
|
||||
func convertString(raw interface{}) string {
|
||||
if str, ok := raw.(string); ok {
|
||||
return str
|
||||
}
|
||||
if float, ok := raw.(float64); ok {
|
||||
// f: avoid conversion to exponential notation
|
||||
return strconv.FormatFloat(float, 'f', -1, 64)
|
||||
}
|
||||
// convert to string
|
||||
return fmt.Sprintf("%v", raw)
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(raw interface{}, variablesMapping map[string]interface{}) (interface{}, error) {
|
||||
rawValue := reflect.ValueOf(raw)
|
||||
switch rawValue.Kind() {
|
||||
case reflect.String:
|
||||
// json.Number
|
||||
if rawValue, ok := raw.(builtinJSON.Number); ok {
|
||||
return parseJSONNumber(rawValue)
|
||||
}
|
||||
// other string
|
||||
value := rawValue.String()
|
||||
value = strings.TrimSpace(value)
|
||||
return p.ParseString(value, variablesMapping)
|
||||
case reflect.Slice:
|
||||
parsedSlice := make([]interface{}, rawValue.Len())
|
||||
for i := 0; i < rawValue.Len(); i++ {
|
||||
parsedValue, err := p.Parse(rawValue.Index(i).Interface(), variablesMapping)
|
||||
if err != nil {
|
||||
return raw, err
|
||||
}
|
||||
parsedSlice[i] = parsedValue
|
||||
}
|
||||
return parsedSlice, nil
|
||||
case reflect.Map: // convert any map to map[string]interface{}
|
||||
parsedMap := make(map[string]interface{})
|
||||
for _, k := range rawValue.MapKeys() {
|
||||
parsedKey, err := p.ParseString(k.String(), variablesMapping)
|
||||
if err != nil {
|
||||
return raw, err
|
||||
}
|
||||
v := rawValue.MapIndex(k)
|
||||
parsedValue, err := p.Parse(v.Interface(), variablesMapping)
|
||||
if err != nil {
|
||||
return raw, err
|
||||
}
|
||||
|
||||
key := convertString(parsedKey)
|
||||
parsedMap[key] = parsedValue
|
||||
}
|
||||
return parsedMap, nil
|
||||
default:
|
||||
// other types, e.g. nil, int, float, bool
|
||||
return builtin.TypeNormalization(raw), nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSONNumber(raw builtinJSON.Number) (value interface{}, err error) {
|
||||
if strings.Contains(raw.String(), ".") {
|
||||
// float64
|
||||
value, err = raw.Float64()
|
||||
} else {
|
||||
// int64
|
||||
value, err = raw.Int64()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.ParseError,
|
||||
fmt.Sprintf("parse json number failed: %v", err))
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
const (
|
||||
regexVariable = `[a-zA-Z_]\w*` // variable name should start with a letter or underscore
|
||||
regexFunctionName = `[a-zA-Z_]\w*` // function name should start with a letter or underscore
|
||||
regexNumber = `^-?\d+(\.\d+)?$` // match number, e.g. 123, -123, 1.23, -1.23
|
||||
)
|
||||
|
||||
var (
|
||||
regexCompileVariable = regexp.MustCompile(fmt.Sprintf(`\$\{(%s)\}|\$(%s)`, regexVariable, regexVariable)) // parse ${var} or $var
|
||||
regexCompileFunction = regexp.MustCompile(fmt.Sprintf(`\$\{(%s)\(([\$\w\.\-/\s=,]*)\)\}`, regexFunctionName)) // parse ${func1($a, $b)}
|
||||
regexCompileNumber = regexp.MustCompile(regexNumber) // parse number
|
||||
)
|
||||
|
||||
// ParseString parse string with variables
|
||||
func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{}) (interface{}, error) {
|
||||
matchStartPosition := 0
|
||||
parsedString := ""
|
||||
remainedString := raw
|
||||
|
||||
for matchStartPosition < len(raw) {
|
||||
// locate $ char position
|
||||
startPosition := strings.Index(remainedString, "$")
|
||||
if startPosition == -1 { // no $ found
|
||||
// append remained string
|
||||
parsedString += remainedString
|
||||
break
|
||||
}
|
||||
|
||||
// found $, check if variable or function
|
||||
matchStartPosition += startPosition
|
||||
parsedString += remainedString[0:startPosition]
|
||||
remainedString = remainedString[startPosition:]
|
||||
|
||||
// Notice: notation priority
|
||||
// $$ > ${func($a, $b)} > $var
|
||||
|
||||
// search $$, use $$ to escape $ notation
|
||||
if strings.HasPrefix(remainedString, "$$") { // found $$
|
||||
matchStartPosition += 2
|
||||
parsedString += "$"
|
||||
remainedString = remainedString[2:]
|
||||
continue
|
||||
}
|
||||
|
||||
// search function like ${func($a, $b)}
|
||||
funcMatched := regexCompileFunction.FindStringSubmatch(remainedString)
|
||||
if len(funcMatched) == 3 {
|
||||
funcName := funcMatched[1]
|
||||
argsStr := funcMatched[2]
|
||||
arguments, err := parseFunctionArguments(argsStr)
|
||||
if err != nil {
|
||||
return raw, errors.Wrap(code.ParseFunctionError, err.Error())
|
||||
}
|
||||
parsedArgs, err := p.Parse(arguments, variablesMapping)
|
||||
if err != nil {
|
||||
return raw, err
|
||||
}
|
||||
|
||||
result, err := p.callFunc(funcName, parsedArgs.([]interface{})...)
|
||||
if err != nil {
|
||||
log.Error().Str("funcName", funcName).Interface("arguments", arguments).
|
||||
Err(err).Msg("call function failed")
|
||||
return raw, errors.Wrap(code.CallFunctionError, err.Error())
|
||||
}
|
||||
log.Info().Str("funcName", funcName).Interface("arguments", arguments).
|
||||
Interface("output", result).Msg("call function success")
|
||||
|
||||
if funcMatched[0] == raw {
|
||||
// raw_string is a function, e.g. "${add_one(3)}", return its eval value directly
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// raw_string contains one or many functions, e.g. "abc${add_one(3)}def"
|
||||
matchStartPosition += len(funcMatched[0])
|
||||
parsedString += convertString(result)
|
||||
remainedString = raw[matchStartPosition:]
|
||||
log.Debug().
|
||||
Str("parsedString", parsedString).
|
||||
Int("matchStartPosition", matchStartPosition).
|
||||
Msg("[parseString] parse function")
|
||||
continue
|
||||
}
|
||||
|
||||
// search variable like ${var} or $var
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(remainedString)
|
||||
if len(varMatched) == 3 {
|
||||
var varName string
|
||||
if varMatched[1] != "" {
|
||||
varName = varMatched[1] // match ${var}
|
||||
} else {
|
||||
varName = varMatched[2] // match $var
|
||||
}
|
||||
varValue, ok := variablesMapping[varName]
|
||||
if !ok {
|
||||
return raw, errors.Wrap(code.VariableNotFound,
|
||||
fmt.Sprintf("variable %s not found", varName))
|
||||
}
|
||||
|
||||
if fmt.Sprintf("${%s}", varName) == raw || fmt.Sprintf("$%s", varName) == raw {
|
||||
// raw string is a variable, $var or ${var}, return its value directly
|
||||
return varValue, nil
|
||||
}
|
||||
|
||||
matchStartPosition += len(varMatched[0])
|
||||
parsedString += convertString(varValue)
|
||||
remainedString = raw[matchStartPosition:]
|
||||
log.Debug().
|
||||
Str("parsedString", parsedString).
|
||||
Int("matchStartPosition", matchStartPosition).
|
||||
Msg("[parseString] parse variable")
|
||||
continue
|
||||
}
|
||||
|
||||
parsedString += remainedString
|
||||
break
|
||||
}
|
||||
|
||||
return parsedString, nil
|
||||
}
|
||||
|
||||
// callFunc calls function with arguments
|
||||
// only support return at most one result value
|
||||
func (p *Parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) {
|
||||
// call with plugin function
|
||||
if p.Plugin != nil {
|
||||
if p.Plugin.Has(funcName) {
|
||||
return p.Plugin.Call(funcName, arguments...)
|
||||
}
|
||||
commonName := fungo.ConvertCommonName(funcName)
|
||||
if p.Plugin.Has(commonName) {
|
||||
return p.Plugin.Call(commonName, arguments...)
|
||||
}
|
||||
}
|
||||
|
||||
// get builtin function
|
||||
function, ok := builtin.Functions[funcName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("function %s is not found", funcName)
|
||||
}
|
||||
fn := reflect.ValueOf(function)
|
||||
|
||||
// call with builtin function
|
||||
return fungo.CallFunc(fn, arguments...)
|
||||
}
|
||||
|
||||
// merge two variables mapping, the first variables have higher priority
|
||||
func mergeVariables(variables, overriddenVariables map[string]interface{}) map[string]interface{} {
|
||||
if overriddenVariables == nil {
|
||||
return variables
|
||||
}
|
||||
if variables == nil {
|
||||
return overriddenVariables
|
||||
}
|
||||
|
||||
mergedVariables := make(map[string]interface{})
|
||||
for k, v := range overriddenVariables {
|
||||
mergedVariables[k] = v
|
||||
}
|
||||
for k, v := range variables {
|
||||
if fmt.Sprintf("${%s}", k) == v || fmt.Sprintf("$%s", k) == v {
|
||||
// e.g. {"base_url": "$base_url"}
|
||||
// or {"base_url": "${base_url}"}
|
||||
continue
|
||||
}
|
||||
|
||||
mergedVariables[k] = v
|
||||
}
|
||||
return mergedVariables
|
||||
}
|
||||
|
||||
// merge two map, the first map have higher priority
|
||||
func mergeMap(m, overriddenMap map[string]string) map[string]string {
|
||||
if overriddenMap == nil {
|
||||
return m
|
||||
}
|
||||
if m == nil {
|
||||
return overriddenMap
|
||||
}
|
||||
|
||||
mergedMap := make(map[string]string)
|
||||
for k, v := range overriddenMap {
|
||||
mergedMap[k] = v
|
||||
}
|
||||
for k, v := range m {
|
||||
mergedMap[k] = v
|
||||
}
|
||||
return mergedMap
|
||||
}
|
||||
|
||||
// merge two validators slice, the first validators have higher priority
|
||||
func mergeValidators(validators, overriddenValidators []interface{}) []interface{} {
|
||||
if validators == nil {
|
||||
return overriddenValidators
|
||||
}
|
||||
if overriddenValidators == nil {
|
||||
return validators
|
||||
}
|
||||
var mergedValidators []interface{}
|
||||
validators = append(validators, overriddenValidators...)
|
||||
for _, validator := range validators {
|
||||
flag := true
|
||||
for _, mergedValidator := range mergedValidators {
|
||||
if validator.(Validator).Check == mergedValidator.(Validator).Check {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
mergedValidators = append(mergedValidators, validator)
|
||||
}
|
||||
}
|
||||
return mergedValidators
|
||||
}
|
||||
|
||||
// merge two slices, the first slice have higher priority
|
||||
func mergeSlices(slice, overriddenSlice []string) []string {
|
||||
if slice == nil {
|
||||
return overriddenSlice
|
||||
}
|
||||
if overriddenSlice == nil {
|
||||
return slice
|
||||
}
|
||||
|
||||
for _, value := range overriddenSlice {
|
||||
if !builtin.Contains(slice, value) {
|
||||
slice = append(slice, value)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
var eval = goval.NewEvaluator()
|
||||
|
||||
// literalEval parse string to number if possible
|
||||
func literalEval(raw string) (interface{}, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
|
||||
// return raw string if not number
|
||||
if !regexCompileNumber.Match([]byte(raw)) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// eval string to number
|
||||
result, err := eval.Evaluate(raw, nil, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("[literalEval] eval %s failed", raw)
|
||||
return raw, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseFunctionArguments(argsStr string) ([]interface{}, error) {
|
||||
argsStr = strings.TrimSpace(argsStr)
|
||||
if argsStr == "" {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
// split arguments by comma
|
||||
args := strings.Split(argsStr, ",")
|
||||
arguments := make([]interface{}, len(args))
|
||||
for index, arg := range args {
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse argument to number if possible
|
||||
arg, err := literalEval(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arguments[index] = arg
|
||||
}
|
||||
|
||||
return arguments, nil
|
||||
}
|
||||
|
||||
func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]interface{}, error) {
|
||||
parsedVariables := make(map[string]interface{})
|
||||
var traverseRounds int
|
||||
|
||||
for len(parsedVariables) != len(variables) {
|
||||
for varName, varValue := range variables {
|
||||
// skip parsed variables
|
||||
if _, ok := parsedVariables[varName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// extract variables from current value
|
||||
extractVarsSet := extractVariables(varValue)
|
||||
|
||||
// check if reference variable itself
|
||||
// e.g.
|
||||
// variables = {"token": "abc$token"}
|
||||
// variables = {"key": ["$key", 2]}
|
||||
if _, ok := extractVarsSet[varName]; ok {
|
||||
log.Error().Interface("variables", variables).Msg("[parseVariables] variable self reference error")
|
||||
return variables, errors.Wrap(code.ParseVariablesError,
|
||||
fmt.Sprintf("variable self reference: %v", varName))
|
||||
}
|
||||
|
||||
// check if reference variable not in variables mapping
|
||||
// e.g.
|
||||
// {"varA": "123$varB", "varB": "456$varC"} => $varC not defined
|
||||
// {"varC": "${sum_two($a, $b)}"} => $a, $b not defined
|
||||
var undefinedVars []string
|
||||
for extractVar := range extractVarsSet {
|
||||
if _, ok := variables[extractVar]; !ok { // not in variables mapping
|
||||
undefinedVars = append(undefinedVars, extractVar)
|
||||
}
|
||||
}
|
||||
if len(undefinedVars) > 0 {
|
||||
log.Error().Interface("undefinedVars", undefinedVars).Msg("[parseVariables] variable not defined error")
|
||||
return variables, errors.Wrap(code.ParseVariablesError,
|
||||
fmt.Sprintf("variable not defined: %v", undefinedVars))
|
||||
}
|
||||
|
||||
parsedValue, err := p.Parse(varValue, parsedVariables)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
parsedVariables[varName] = parsedValue
|
||||
}
|
||||
traverseRounds += 1
|
||||
// check if circular reference exists
|
||||
if traverseRounds > len(variables) {
|
||||
log.Error().Msg("[parseVariables] circular reference error, break infinite loop!")
|
||||
return variables, errors.Wrap(code.ParseVariablesError, "circular reference")
|
||||
}
|
||||
}
|
||||
|
||||
return parsedVariables, nil
|
||||
}
|
||||
|
||||
type variableSet map[string]struct{}
|
||||
|
||||
func extractVariables(raw interface{}) variableSet {
|
||||
rawValue := reflect.ValueOf(raw)
|
||||
switch rawValue.Kind() {
|
||||
case reflect.String:
|
||||
return findallVariables(rawValue.String())
|
||||
case reflect.Slice:
|
||||
varSet := make(variableSet)
|
||||
for i := 0; i < rawValue.Len(); i++ {
|
||||
for extractVar := range extractVariables(rawValue.Index(i).Interface()) {
|
||||
varSet[extractVar] = struct{}{}
|
||||
}
|
||||
}
|
||||
return varSet
|
||||
case reflect.Map:
|
||||
varSet := make(variableSet)
|
||||
for _, key := range rawValue.MapKeys() {
|
||||
value := rawValue.MapIndex(key)
|
||||
for extractVar := range extractVariables(value.Interface()) {
|
||||
varSet[extractVar] = struct{}{}
|
||||
}
|
||||
}
|
||||
return varSet
|
||||
default:
|
||||
// other types, e.g. nil, int, float, bool
|
||||
return make(variableSet)
|
||||
}
|
||||
}
|
||||
|
||||
func findallVariables(raw string) variableSet {
|
||||
matchStartPosition := 0
|
||||
remainedString := raw
|
||||
varSet := make(variableSet)
|
||||
|
||||
for matchStartPosition < len(raw) {
|
||||
// locate $ char position
|
||||
startPosition := strings.Index(remainedString, "$")
|
||||
if startPosition == -1 { // no $ found
|
||||
return varSet
|
||||
}
|
||||
|
||||
// found $, check if variable or function
|
||||
matchStartPosition += startPosition
|
||||
remainedString = remainedString[startPosition:]
|
||||
|
||||
// Notice: notation priority
|
||||
// $$ > $var
|
||||
|
||||
// search $$, use $$ to escape $ notation
|
||||
if strings.HasPrefix(remainedString, "$$") { // found $$
|
||||
matchStartPosition += 2
|
||||
remainedString = remainedString[2:]
|
||||
continue
|
||||
}
|
||||
|
||||
// search variable like ${var} or $var
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(remainedString)
|
||||
if len(varMatched) == 3 {
|
||||
var varName string
|
||||
if varMatched[1] != "" {
|
||||
varName = varMatched[1] // match ${var}
|
||||
} else {
|
||||
varName = varMatched[2] // match $var
|
||||
}
|
||||
varSet[varName] = struct{}{}
|
||||
|
||||
matchStartPosition += len(varMatched[0])
|
||||
remainedString = raw[matchStartPosition:]
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return varSet
|
||||
}
|
||||
@@ -1,786 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildURL(t *testing.T) {
|
||||
var preparedURL *url.URL
|
||||
|
||||
preparedURL = buildURL("https://postman-echo.com", "/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
preparedURL = buildURL("https://postman-echo.com", "get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
preparedURL = buildURL("https://postman-echo.com/", "/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
preparedURL = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
preparedURL = buildURL("https://postman-echo.com/abc", "get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// omit query string in base url
|
||||
preparedURL = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
preparedURL = buildURL("", "https://postman-echo.com/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// notice: step request url > config base url
|
||||
preparedURL = buildURL("https://postman-echo.com", "https://httpbin.org/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://httpbin.org/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// websocket url
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com/raw", "", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com", "/raw", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "ws://echo.websocket.events/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
queryParams := url.Values{}
|
||||
queryParams.Add("c", "3")
|
||||
queryParams.Add("d", "4")
|
||||
preparedURL = buildURL("https://postman-echo.com/", "/get/", queryParams)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get?c=3&d=4") {
|
||||
t.Fatal()
|
||||
}
|
||||
preparedURL = buildURL("https://postman-echo.com/abc", "get?a=1&b=2", queryParams)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2&c=3&d=4") {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCompileVariable(t *testing.T) {
|
||||
testData := []string{
|
||||
"$var1",
|
||||
"${var1}",
|
||||
"$v",
|
||||
"var_1$_v",
|
||||
"${var_1}#XYZ",
|
||||
"func1($var_1, $var_3)",
|
||||
}
|
||||
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 3) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCompileAbnormalVariable(t *testing.T) {
|
||||
testData := []string{
|
||||
"var1",
|
||||
"${var1",
|
||||
"$123",
|
||||
"var_1$",
|
||||
"func1($123, var_3)",
|
||||
}
|
||||
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 0) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCompileFunction(t *testing.T) {
|
||||
testData := []string{
|
||||
"${func1()}",
|
||||
"${func1($a)}",
|
||||
"${func1($a, $b)}",
|
||||
"${func1($a, 123)}",
|
||||
"${func1(123, $b)}",
|
||||
"abc${func1(123, $b)}123",
|
||||
}
|
||||
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileFunction.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 3) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCompileAbnormalFunction(t *testing.T) {
|
||||
testData := []string{
|
||||
"${func1()",
|
||||
"${func1(}",
|
||||
"${func1)}",
|
||||
"$func1()}",
|
||||
"${1func1()}", // function name can not start with number
|
||||
"${func1($a}",
|
||||
"abc$func1(123, $b)}123",
|
||||
// "${func1($a $b)}",
|
||||
// "${func1($a, $123)}",
|
||||
// "${func1(123 $b)}",
|
||||
}
|
||||
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileFunction.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 0) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataStringWithVariables(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"var_1": "abc",
|
||||
"var_2": "def",
|
||||
"var_3": 123,
|
||||
"var_4": map[string]interface{}{"a": 1},
|
||||
"var_5": true,
|
||||
"var_6": nil,
|
||||
"v": 4.5, // variable name with one character
|
||||
"_v": 6.9, // variable name starts with underscore
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
// no variable
|
||||
{"var_1", "var_1"},
|
||||
// single variable
|
||||
{"$var_1", "abc"},
|
||||
{"${var_1}", "abc"},
|
||||
{"$var_3", 123},
|
||||
{"$var_4", map[string]interface{}{"a": 1}},
|
||||
{"${var_4}", map[string]interface{}{"a": 1}},
|
||||
{"$var_5", true},
|
||||
{"$var_6", nil},
|
||||
{"$v", 4.5},
|
||||
{"var_1$_v", "var_16.9"},
|
||||
// single variable with prefix or suffix
|
||||
{"$var_1#XYZ", "abc#XYZ"},
|
||||
{"${var_1}#XYZ", "abc#XYZ"},
|
||||
{"ABC$var_1", "ABCabc"},
|
||||
{"ABC${var_1}", "ABCabc"},
|
||||
{"ABC$var_1/", "ABCabc/"},
|
||||
{"ABC${v}/", "ABC4.5/"},
|
||||
// multiple variables
|
||||
{"/$var_1/$var_2/var3", "/abc/def/var3"},
|
||||
{"/${var_1}/$var_2/var3", "/abc/def/var3"},
|
||||
{"ABC$var_1$var_3", "ABCabc123"},
|
||||
{"ABC$var_1${var_3}", "ABCabc123"},
|
||||
{"ABC$var_1/$var_3", "ABCabc/123"},
|
||||
{"ABC${var_1}/${var_3}", "ABCabc/123"},
|
||||
{"ABC$var_1/123$var_1/456", "ABCabc/123abc/456"},
|
||||
{"ABC$var_1/123${var_1}/456", "ABCabc/123abc/456"},
|
||||
{"ABC$var_1/$var_2/$var_1", "ABCabc/def/abc"},
|
||||
{"ABC$var_1/$var_2/${var_1}", "ABCabc/def/abc"},
|
||||
{"func1($var_1, $var_3)", "func1(abc, 123)"},
|
||||
{"func1($var_1, ${var_3})", "func1(abc, 123)"},
|
||||
// TODO: fix compatibility with python version
|
||||
{"abc$var_4", "abcmap[a:1]"}, // "abc{'a': 1}"
|
||||
{"abc$var_5", "abctrue"}, // "abcTrue"
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataStringWithUndefinedVariables(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"var_1": "abc",
|
||||
"var_2": "def",
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"/api/$SECRET_KEY", "/api/$SECRET_KEY"}, // raise error
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataStringWithVariablesAbnormal(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"var_1": "abc",
|
||||
"var_2": "def",
|
||||
"var_3": 123,
|
||||
"var_4": map[string]interface{}{"a": 1},
|
||||
"var_5": true,
|
||||
"var_6": nil,
|
||||
"v": 4.5, // variable name with one character
|
||||
"_v": 6.9, // variable name starts with underscore
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"$", "$"},
|
||||
{"var_1$", "var_1$"},
|
||||
{"var_1$123", "var_1$123"}, // variable should starts with a letter
|
||||
{"ABC$var_1{", "ABCabc{"}, // {
|
||||
{"ABC$var_1}", "ABCabc}"}, // }
|
||||
{"{ABC$var_1{}a}", "{ABCabc{}a}"}, // {xx}
|
||||
{"AB{C$var_1{}a}", "AB{Cabc{}a}"}, // {xx{}x}
|
||||
{"ABC$$var_1{", "ABC$var_1{"}, // $$
|
||||
{"ABC$$$var_1{", "ABC$abc{"}, // $$$
|
||||
{"ABC$$$$var_1{", "ABC$$var_1{"}, // $$$$
|
||||
{"ABC$var_1${", "ABCabc${"}, // ${
|
||||
{"ABC$var_1${a", "ABCabc${a"}, // ${
|
||||
{"ABC$var_1$}a", "ABCabc$}a"}, // $}
|
||||
{"ABC$var_1}{a", "ABCabc}{a"}, // }{
|
||||
{"ABC$var_1{}a", "ABCabc{}a"}, // {}
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataMapWithVariables(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"var1": "foo1",
|
||||
"val1": 200,
|
||||
"var2": 123, // key is int
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
expr map[string]interface{}
|
||||
expect interface{}
|
||||
}{
|
||||
{map[string]interface{}{"key": "$var1"}, map[string]interface{}{"key": "foo1"}},
|
||||
{map[string]interface{}{"foo1": "$val1", "foo2": "bar2"}, map[string]interface{}{"foo1": 200, "foo2": "bar2"}},
|
||||
// parse map key, key is string
|
||||
{map[string]interface{}{"$var1": "$val1"}, map[string]interface{}{"foo1": 200}},
|
||||
// parse map key, key is int
|
||||
{map[string]interface{}{"$var2": "$val1"}, map[string]interface{}{"123": 200}},
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHeaders(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"var1": "foo1",
|
||||
"val1": 200,
|
||||
"var2": 123, // key is int
|
||||
"val2": nil, // value is nil
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
rawHeaders map[string]string
|
||||
expectHeaders map[string]string
|
||||
}{
|
||||
{map[string]string{"key": "$var1"}, map[string]string{"key": "foo1"}},
|
||||
{map[string]string{"foo1": "$val1", "foo2": "bar2"}, map[string]string{"foo1": "200", "foo2": "bar2"}},
|
||||
// parse map key, key is string
|
||||
{map[string]string{"$var1": "$val1"}, map[string]string{"foo1": "200"}},
|
||||
// parse map key, key is int
|
||||
{map[string]string{"$var2": "$val1"}, map[string]string{"123": "200"}},
|
||||
// parse map key & value, key is int, value is nil
|
||||
{map[string]string{"$var2": "$val2"}, map[string]string{"123": "<nil>"}},
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
parsedHeaders, err := parser.ParseHeaders(data.rawHeaders, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectHeaders, parsedHeaders) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeVariables(t *testing.T) {
|
||||
testData := []struct {
|
||||
stepVariables map[string]interface{}
|
||||
configVariables map[string]interface{}
|
||||
expectVariables map[string]interface{}
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{"base_url": "$base_url", "foo1": "bar1"},
|
||||
map[string]interface{}{"base_url": "https://httpbin.org", "foo1": "bar111"},
|
||||
map[string]interface{}{"base_url": "https://httpbin.org", "foo1": "bar1"},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{"n": 3, "b": 34.5, "varFoo2": "${max($a, $b)}"},
|
||||
map[string]interface{}{"n": 5, "a": 12.3, "b": 3.45, "varFoo1": "7a6K3", "varFoo2": 12.3},
|
||||
map[string]interface{}{"n": 3, "a": 12.3, "b": 34.5, "varFoo1": "7a6K3", "varFoo2": "${max($a, $b)}"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
mergedVariables := mergeVariables(data.stepVariables, data.configVariables)
|
||||
if !assert.Equal(t, data.expectVariables, mergedVariables) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMap(t *testing.T) {
|
||||
testData := []struct {
|
||||
m map[string]string
|
||||
overriddenMap map[string]string
|
||||
expectMap map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Cache-Control": "no-cache"},
|
||||
},
|
||||
{
|
||||
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"},
|
||||
map[string]string{"Host": "Postman-echo.com", "Connection": "keep-alive", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b342c2723"},
|
||||
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723", "Connection": "keep-alive"},
|
||||
},
|
||||
{
|
||||
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||
nil,
|
||||
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
mergedMap := mergeMap(data.m, data.overriddenMap)
|
||||
if !assert.Equal(t, data.expectMap, mergedMap) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeSlices(t *testing.T) {
|
||||
testData := []struct {
|
||||
slice []string
|
||||
overriddenSlice []string
|
||||
expectSlice []string
|
||||
}{
|
||||
{
|
||||
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}", "${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||
},
|
||||
{
|
||||
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||
nil,
|
||||
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
mergedSlice := mergeSlices(data.slice, data.overriddenSlice)
|
||||
if !assert.Equal(t, data.expectSlice, mergedSlice) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeValidators(t *testing.T) {
|
||||
testData := []struct {
|
||||
validators []interface{}
|
||||
overriddenValidators []interface{}
|
||||
expectValidators []interface{}
|
||||
}{
|
||||
{
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||
[]interface{}{Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"}},
|
||||
[]interface{}{
|
||||
Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"},
|
||||
Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||
},
|
||||
{
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||
nil,
|
||||
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
mergedValidators := mergeValidators(data.validators, data.overriddenValidators)
|
||||
if !assert.Equal(t, data.expectValidators, mergedValidators) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallBuiltinFunction(t *testing.T) {
|
||||
parser := newParser()
|
||||
|
||||
// call function without arguments
|
||||
_, err := parser.callFunc("get_timestamp")
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with one argument
|
||||
timeStart := time.Now()
|
||||
_, err = parser.callFunc("sleep", 1)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Greater(t, time.Since(timeStart), time.Duration(1)*time.Second) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with one argument
|
||||
result, err := parser.callFunc("gen_random_string", 10)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 10, len(result.(string))) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with two argument
|
||||
result, err = parser.callFunc("max", float64(10), 9.99)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, float64(10), result.(float64)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiteralEval(t *testing.T) {
|
||||
testData := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"123", 123},
|
||||
{"1.23", 1.23},
|
||||
{"-123", -123},
|
||||
{"-1.23", -1.23},
|
||||
{"abc", "abc"},
|
||||
{" a bc ", "a bc"},
|
||||
{" a $bc ", "a $bc"},
|
||||
{"$var", "$var"},
|
||||
{" $var ", "$var"},
|
||||
{" $var1 ", "$var1"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
value, err := literalEval(data.expr)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFunctionArguments(t *testing.T) {
|
||||
testData := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"", []interface{}{}},
|
||||
{"123", []interface{}{123}},
|
||||
{"1.23", []interface{}{1.23}},
|
||||
{"-123", []interface{}{-123}},
|
||||
{"-1.23", []interface{}{-1.23}},
|
||||
{"abc", []interface{}{"abc"}},
|
||||
{"$var", []interface{}{"$var"}},
|
||||
{"1,2", []interface{}{1, 2}},
|
||||
{"1,2.3", []interface{}{1, 2.3}},
|
||||
{"1, -2.3", []interface{}{1, -2.3}},
|
||||
{"1,,2", []interface{}{1, nil, 2}},
|
||||
{" $var1 , 2 ", []interface{}{"$var1", 2}},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
value, err := parseFunctionArguments(data.expr)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataStringWithFunctions(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
}
|
||||
|
||||
testData1 := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"${gen_random_string(5)}", 5},
|
||||
{"${gen_random_string($n)}", 5},
|
||||
{"123${gen_random_string(5)}abc", 11},
|
||||
{"123${gen_random_string($n)}abc", 11},
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData1 {
|
||||
value, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, len(value.(string))) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
testData2 := []struct {
|
||||
expr string
|
||||
expect interface{}
|
||||
}{
|
||||
{"${max($a, $b)}", 12.3},
|
||||
{"abc${max($a, $b)}123", "abc12.3123"},
|
||||
{"abc${max($a, 3.45)}123", "abc12.3123"},
|
||||
}
|
||||
|
||||
for _, data := range testData2 {
|
||||
value, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertString(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expect interface{}
|
||||
}{
|
||||
{"", ""},
|
||||
{"abc", "abc"},
|
||||
{"123", "123"},
|
||||
{123, "123"},
|
||||
{1.23, "1.23"},
|
||||
{100000000000, "100000000000"}, // avoid exponential notation
|
||||
{100000000000.23, "100000000000.23"},
|
||||
{nil, "<nil>"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
value := convertString(data.raw)
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVariables(t *testing.T) {
|
||||
testData := []struct {
|
||||
rawVars map[string]interface{}
|
||||
expectVars map[string]interface{}
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varC", "varC": "123", "a": 1, "b": 2},
|
||||
map[string]interface{}{"varA": "123", "varB": "123", "varC": "123", "a": int64(1), "b": int64(2)},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{"n": 34.5, "a": 12.3, "b": "$n", "varFoo2": "${max($a, $b)}"},
|
||||
map[string]interface{}{"n": 34.5, "a": 12.3, "b": 34.5, "varFoo2": 34.5},
|
||||
},
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
value, err := parser.ParseVariables(data.rawVars)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectVars, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVariablesAbnormal(t *testing.T) {
|
||||
testData := []struct {
|
||||
rawVars map[string]interface{}
|
||||
expectVars map[string]interface{}
|
||||
}{
|
||||
{ // self referenced variable $varA
|
||||
map[string]interface{}{"varA": "$varA"},
|
||||
map[string]interface{}{"varA": "$varA"},
|
||||
},
|
||||
{ // undefined variable $varC
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varC", "a": 1, "b": 2},
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varC", "a": 1, "b": 2},
|
||||
},
|
||||
{ // circular reference
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varA"},
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varA"},
|
||||
},
|
||||
}
|
||||
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
value, err := parser.ParseVariables(data.rawVars)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectVars, value) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVariables(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expectVars []string
|
||||
}{
|
||||
{nil, nil},
|
||||
{"/$var1/$var1", []string{"var1"}},
|
||||
{
|
||||
map[string]interface{}{"varA": "$varB", "varB": "$varC", "varC": "123"},
|
||||
[]string{"varB", "varC"},
|
||||
},
|
||||
{
|
||||
[]interface{}{"varA", "$varB", 123, "$varC", "123"},
|
||||
[]string{"varB", "varC"},
|
||||
},
|
||||
{ // nested map and slice
|
||||
map[string]interface{}{"varA": "$varB", "varB": map[string]interface{}{"C": "$varC", "D": []string{"$varE"}}},
|
||||
[]string{"varB", "varC", "varE"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
var varList []string
|
||||
for varName := range extractVariables(data.raw) {
|
||||
varList = append(varList, varName)
|
||||
}
|
||||
sort.Strings(varList)
|
||||
if !assert.Equal(t, data.expectVars, varList) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindallVariables(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expectVars []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"$variable", []string{"variable"}},
|
||||
{"${variable}123", []string{"variable"}},
|
||||
{"/blog/$postid", []string{"postid"}},
|
||||
{"/$var1/$var2", []string{"var1", "var2"}},
|
||||
{"/$var1/$var1", []string{"var1"}},
|
||||
{"abc", nil},
|
||||
{"Z:2>1*0*1+1$a", []string{"a"}},
|
||||
{"Z:2>1*0*1+1$$a", nil},
|
||||
{"Z:2>1*0*1+1$$$a", []string{"a"}},
|
||||
{"Z:2>1*0*1+1$$$$a", nil},
|
||||
{"Z:2>1*0*1+1$$a$b", []string{"b"}},
|
||||
{"Z:2>1*0*1+1$$a$$b", nil},
|
||||
{"Z:2>1*0*1+1$a$b", []string{"a", "b"}},
|
||||
{"Z:2>1*0*1+1$$1", nil},
|
||||
{"a$var", []string{"var"}},
|
||||
{"a$v b", []string{"v"}},
|
||||
{"${func()}", nil},
|
||||
{"a${func(1,2)}b", nil},
|
||||
{"${gen_md5($TOKEN, $data, $random)}", []string{"TOKEN", "data", "random"}},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
var varList []string
|
||||
for varName := range findallVariables(data.raw) {
|
||||
varList = append(varList, varName)
|
||||
}
|
||||
sort.Strings(varList)
|
||||
if !assert.Equal(t, data.expectVars, varList) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
# hrp convert
|
||||
|
||||
## 快速上手
|
||||
|
||||
```shell
|
||||
$ hrp convert -h
|
||||
convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases
|
||||
|
||||
Usage:
|
||||
hrp convert $path... [flags]
|
||||
|
||||
Flags:
|
||||
--from-har load from HAR format
|
||||
--from-json load from json case format (default true)
|
||||
--from-postman load from postman format
|
||||
--from-yaml load from yaml case format
|
||||
-h, --help help for convert
|
||||
-d, --output-dir string specify output directory
|
||||
-p, --profile string specify profile path to override headers and cookies
|
||||
--to-json convert to JSON case scripts (default true)
|
||||
--to-pytest convert to pytest scripts
|
||||
--to-yaml convert to YAML case scripts
|
||||
|
||||
Global Flags:
|
||||
--log-json set log to json format
|
||||
-l, --log-level string set log level (default "INFO")
|
||||
--venv string specify python3 venv path
|
||||
```
|
||||
|
||||
`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 HttpRunner JSON/YAML/gotest/pytest 形态的测试用例,同时也支持 HttpRunner 测试用例各个形态之间的相互转化。
|
||||
|
||||
该指令所有选项的详细说明如下:
|
||||
|
||||
- `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的 HttpRunner 测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例
|
||||
- `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹;默认输出的文件夹为源文件所在的文件夹
|
||||
- `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例:
|
||||
|
||||
- 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
Header1: "this header will be created or updated"
|
||||
cookies:
|
||||
Cookie1: "this cookie will be created or updated"
|
||||
```
|
||||
|
||||
- 根据 profile 覆盖原有的 `Headers` 和 `Cookies` 信息
|
||||
|
||||
```yaml
|
||||
override: true
|
||||
headers:
|
||||
Header1: "all original headers will be overridden"
|
||||
cookies:
|
||||
Cookie1: "all original cookies will be overridden"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 输出的测试用例文件名格式为 `源文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖
|
||||
2. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式
|
||||
3. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格
|
||||
|
||||
|
||||
## 转换流程图
|
||||
|
||||
`hrp convert` 的转换过程流程图如下:
|
||||

|
||||
|
||||
## 开发进度
|
||||
|
||||
`hrp convert` 当前的开发进度如下:
|
||||
|
||||
| from \ to | JSON | YAML | GoTest | PyTest |
|
||||
|:---------:|:----:|:----:|:------:|:------:|
|
||||
| HAR | ✅ | ✅ | ❌ | ✅ |
|
||||
| Postman | ✅ | ✅ | ❌ | ✅ |
|
||||
| JMeter | ❌ | ❌ | ❌ | ❌ |
|
||||
| Swagger | ❌ | ❌ | ❌ | ❌ |
|
||||
| curl | ✅ | ✅ | ❌ | ✅ |
|
||||
| Apache ab | ❌ | ❌ | ❌ | ❌ |
|
||||
| JSON | ✅ | ✅ | ❌ | ✅ |
|
||||
| YAML | ✅ | ✅ | ❌ | ✅ |
|
||||
| GoTest | ❌ | ❌ | ❌ | ❌ |
|
||||
| PyTest | ❌ | ❌ | ❌ | ❌ |
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -1 +0,0 @@
|
||||
package convert
|
||||
@@ -1,507 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/json"
|
||||
)
|
||||
|
||||
const (
|
||||
originCmdKey = "_origin_cmd_key"
|
||||
targetUrlKey = "_target_url_key"
|
||||
)
|
||||
|
||||
var curlOptionAliasMap = map[string]string{
|
||||
"-a": "--append",
|
||||
"-A": "--user-agent",
|
||||
"-b": "--cookie",
|
||||
"-B": "--use-ascii",
|
||||
"-c": "--cookie-jar",
|
||||
"-C": "--continue-at",
|
||||
"-d": "--data",
|
||||
"-D": "--dump-header",
|
||||
"-e": "--referer",
|
||||
"-E": "--cert",
|
||||
"-f": "--fail",
|
||||
"-F": "--form",
|
||||
"-g": "--globoff",
|
||||
"-G": "--get",
|
||||
"-h": "--help",
|
||||
"-H": "--header",
|
||||
"-i": "--include",
|
||||
"-I": "--head",
|
||||
"-j": "--junk-session-cookies",
|
||||
"-J": "--remote-header-name",
|
||||
"-k": "--insecure",
|
||||
"-K": "--config",
|
||||
"-l": "--list-only",
|
||||
"-L": "--location",
|
||||
"-m": "--max-time",
|
||||
"-M": "--manual",
|
||||
"-n": "--netrc",
|
||||
"-N": "--no-buffer",
|
||||
"-o": "--output",
|
||||
"-O": "--remote-name",
|
||||
"-p": "--proxytunnel",
|
||||
"-P": "--ftp-port",
|
||||
"-q": "--disable",
|
||||
"-Q": "--quote",
|
||||
"-r": "--range",
|
||||
"-R": "--remote-time",
|
||||
"-s": "--silent",
|
||||
"-S": "--show-error",
|
||||
"-t": "--telnet-option",
|
||||
"-T": "--upload-file",
|
||||
"-u": "--user",
|
||||
"-U": "--proxy-user",
|
||||
"-v": "--verbose",
|
||||
"-V": "--version",
|
||||
"-w": "--write-out",
|
||||
"-x": "--proxy",
|
||||
"-X": "--request",
|
||||
"-Y": "--speed-limit",
|
||||
"-y": "--speed-time",
|
||||
"-z": "--time-cond",
|
||||
"-Z": "--parallel",
|
||||
}
|
||||
|
||||
var curlOptionWhiteMap = map[string]struct{}{
|
||||
"--cookie": {},
|
||||
"--data": {},
|
||||
"--form": {},
|
||||
"--get": {},
|
||||
"--head": {},
|
||||
"--header": {},
|
||||
"--request": {},
|
||||
}
|
||||
|
||||
var curlOptionWhiteList []string
|
||||
|
||||
func init() {
|
||||
for option := range curlOptionWhiteMap {
|
||||
curlOptionWhiteList = append(curlOptionWhiteList, option)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCurlCase loads testcase from one or more curl commands in .txt file
|
||||
func LoadCurlCase(path string) (*hrp.TestCaseDef, error) {
|
||||
cmds, err := readFileLines(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tCase := &hrp.TestCaseDef{
|
||||
Config: &hrp.TConfig{
|
||||
Name: "testcase converted from curl command",
|
||||
},
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
tSteps, err := LoadCurlSteps(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tCase.Steps = append(tCase.Steps, tSteps...)
|
||||
}
|
||||
err = hrp.ConvertCaseCompatibility(tCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func readFileLines(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("open file failed")
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || line == "\n" {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return lines, scanner.Err()
|
||||
}
|
||||
|
||||
// LoadCurlSteps loads one teststep from one curl command
|
||||
func LoadCurlSteps(cmd string) ([]*hrp.TStep, error) {
|
||||
caseCurl, err := loadCaseCurl(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caseCurl.toTSteps()
|
||||
}
|
||||
|
||||
func loadCaseCurl(cmd string) (CaseCurl, error) {
|
||||
caseCurl := make(CaseCurl)
|
||||
var err error
|
||||
caseCurl, err = parseCaseCurl(cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load curl command failed")
|
||||
}
|
||||
// deal with option alias, turn all options to long form
|
||||
if err = caseCurl.toAlias(); err != nil {
|
||||
return nil, errors.Wrap(err, "identify curl option alias failed")
|
||||
}
|
||||
// check if caseCurl contains unsupported args
|
||||
if err = caseCurl.checkOptions(); err != nil {
|
||||
return nil, errors.Wrap(err, "check curl option failed")
|
||||
}
|
||||
caseCurl.Set(originCmdKey, cmd)
|
||||
return caseCurl, nil
|
||||
}
|
||||
|
||||
// parseCaseCurl parses command string to map, save command keyword and bool option as map key only.
|
||||
// Otherwise, save option as map key and the following args([]string) as map value
|
||||
func parseCaseCurl(cmd string) (CaseCurl, error) {
|
||||
cmdWords, err := shlex.Split(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the command string to map
|
||||
res := make(CaseCurl)
|
||||
var i int
|
||||
if cmdWords[i] != "curl" {
|
||||
return nil, errors.New("command not started with curl")
|
||||
}
|
||||
i++
|
||||
for i < len(cmdWords) {
|
||||
if !strings.HasPrefix(cmdWords[i], "-") {
|
||||
// save target url
|
||||
res.Add(targetUrlKey, cmdWords[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
option := cmdWords[i]
|
||||
i++
|
||||
if i < len(cmdWords) && !strings.HasPrefix(cmdWords[i], "-") {
|
||||
// option with only one following argument
|
||||
res.Add(option, cmdWords[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// option with no argument, i.e. bool option, save key only
|
||||
res[option] = nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type CaseCurl map[string][]string
|
||||
|
||||
// Get gets the first value associated with the given key.
|
||||
// If there are no values associated with the key, Get returns the empty string.
|
||||
func (c CaseCurl) Get(key string, index int) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
vs := c[key]
|
||||
if index >= 0 && index < len(vs) {
|
||||
return vs[index]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c CaseCurl) Set(key, value string) {
|
||||
c[key] = []string{value}
|
||||
}
|
||||
|
||||
func (c CaseCurl) Add(key, value string) {
|
||||
c[key] = append(c[key], value)
|
||||
}
|
||||
|
||||
// HaveKey checks key existed or not
|
||||
func (c CaseCurl) HaveKey(key string) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := c[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c CaseCurl) toAlias() error {
|
||||
for option, args := range c {
|
||||
if !strings.HasPrefix(option, "-") || strings.HasPrefix(option, "--") {
|
||||
// not a short option like -X, pass
|
||||
continue
|
||||
}
|
||||
longOption, ok := curlOptionAliasMap[option]
|
||||
if !ok {
|
||||
return errors.Errorf("unexpected curl option: %v", option)
|
||||
}
|
||||
// FIXME: need to copy args or not?
|
||||
c[longOption] = args
|
||||
delete(c, option)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c CaseCurl) checkOptions() error {
|
||||
for option := range c {
|
||||
if option == originCmdKey || option == targetUrlKey {
|
||||
continue
|
||||
}
|
||||
_, ok := curlOptionWhiteMap[option]
|
||||
if !ok {
|
||||
return errors.Errorf("option %v not supported yet. available options: %v", option, curlOptionWhiteList)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c CaseCurl) toTSteps() ([]*hrp.TStep, error) {
|
||||
var tSteps []*hrp.TStep
|
||||
for _, rawUrl := range c[targetUrlKey] {
|
||||
log.Info().
|
||||
Str("url", rawUrl).
|
||||
Msg("convert test steps")
|
||||
|
||||
step := &stepFromCurl{
|
||||
TStep: &hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
},
|
||||
}
|
||||
if err := step.makeRequestName(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestMethod(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(rawUrl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(rawUrl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tSteps = append(tSteps, step.TStep)
|
||||
}
|
||||
return tSteps, nil
|
||||
}
|
||||
|
||||
type stepFromCurl struct {
|
||||
*hrp.TStep
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestName(c CaseCurl) error {
|
||||
s.StepName = c.Get(originCmdKey, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestMethod(c CaseCurl) error {
|
||||
// default --get
|
||||
s.Request.Method = http.MethodGet
|
||||
if c.HaveKey("--data") || c.HaveKey("--form") {
|
||||
s.Request.Method = http.MethodPost
|
||||
}
|
||||
if c.HaveKey("--head") {
|
||||
s.Request.Method = http.MethodHead
|
||||
}
|
||||
if c.HaveKey("--request") {
|
||||
s.Request.Method = hrp.HTTPMethod(strings.ToUpper(c.Get("--request", 0)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestURL(rawUrl string) error {
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse URL error")
|
||||
}
|
||||
// default protocol consistent with curl (http)
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestParams(rawUrl string) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse URL error")
|
||||
}
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
queryValues := u.Query()
|
||||
// query key may correspond to more than one value, get first query key only
|
||||
for k := range queryValues {
|
||||
s.Request.Params[k] = queryValues.Get(k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestHeaders(c CaseCurl) error {
|
||||
s.Request.Headers = make(map[string]string)
|
||||
headerList := c["--header"]
|
||||
for _, headerExpr := range headerList {
|
||||
if err := s.makeRequestHeader(headerExpr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestHeader(headerExpr string) error {
|
||||
headerExpr = strings.TrimSpace(headerExpr)
|
||||
if strings.HasPrefix(headerExpr, "@") {
|
||||
return errors.Errorf("loading header from file not supported: %v", headerExpr)
|
||||
}
|
||||
if strings.TrimSpace(headerExpr) == ";" || strings.HasPrefix(strings.TrimSpace(headerExpr), ":") {
|
||||
return errors.Errorf("invalid curl header format: %v", headerExpr)
|
||||
}
|
||||
if s.Request.Headers == nil {
|
||||
s.Request.Headers = make(map[string]string)
|
||||
}
|
||||
if i := strings.Index(headerExpr, ":"); i != -1 {
|
||||
headerKey := strings.TrimSpace(headerExpr[:i])
|
||||
var headerValue string
|
||||
if i < len(headerExpr)-1 {
|
||||
headerValue = strings.TrimSpace(headerExpr[i+1:])
|
||||
}
|
||||
if strings.ToLower(headerKey) == "host" {
|
||||
// headerExpr modifying internal header like "Host:"
|
||||
log.Warn().Str("--header", headerExpr).Msg("modifying internal header not supported")
|
||||
return nil
|
||||
}
|
||||
if headerValue != "" {
|
||||
// normal headerExpr like "User-Agent: httprunner"
|
||||
s.Request.Headers[headerKey] = headerValue
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if i := strings.Index(headerExpr, ";"); i != -1 {
|
||||
// headerExpr terminated with a semicolon like "X-Custom-Header;"
|
||||
headerKey := strings.TrimSpace(headerExpr[:i])
|
||||
if strings.ToLower(headerKey) == "host" {
|
||||
log.Warn().Str("--header", headerExpr).Msg("modifying internal header not supported")
|
||||
return nil
|
||||
}
|
||||
s.Request.Headers[headerKey] = ""
|
||||
return nil
|
||||
}
|
||||
log.Warn().Str("--header", headerExpr).Msg("pass meaningless curl header expression")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestCookies(c CaseCurl) error {
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
cookieList := c["--cookie"]
|
||||
for _, cookieExpr := range cookieList {
|
||||
if err := s.makeRequestCookie(cookieExpr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestCookie(cookieExpr string) error {
|
||||
if !strings.Contains(cookieExpr, "=") {
|
||||
return errors.Errorf("loading cookie from file not supported: %v", cookieExpr)
|
||||
}
|
||||
if s.Request.Cookies == nil {
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
}
|
||||
// deal with cookieExpr like "name1=value1; name2 = value2"
|
||||
cookies := strings.Split(cookieExpr, ";")
|
||||
for _, cookie := range cookies {
|
||||
i := strings.Index(cookie, "=")
|
||||
if i == -1 {
|
||||
log.Warn().Str("--cookie", cookie).Msg("pass meaningless curl cookie expression")
|
||||
continue
|
||||
}
|
||||
cookieKey := strings.TrimSpace(cookie[:i])
|
||||
var cookieValue string
|
||||
if i < len(cookie)-1 {
|
||||
cookieValue = strings.TrimSpace(cookie[i+1:])
|
||||
}
|
||||
s.Request.Cookies[cookieKey] = cookieValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestBody(c CaseCurl) error {
|
||||
// check priority: --data > --form
|
||||
dataList, dataExisted := c["--data"]
|
||||
formList, formExisted := c["--form"]
|
||||
if dataExisted {
|
||||
if err := s.makeRequestData(dataList); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if formExisted {
|
||||
if err := s.makeRequestForm(formList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestData(dataList []string) error {
|
||||
dataMap := make(map[string]interface{})
|
||||
for _, dataExpr := range dataList {
|
||||
if strings.HasPrefix(dataExpr, "@") {
|
||||
return errors.Errorf("loading data from file not supported: %v", dataExpr)
|
||||
}
|
||||
var m map[string]interface{}
|
||||
// --data may be json string, try to unmarshal to map first
|
||||
err := json.Unmarshal([]byte(dataExpr), &m)
|
||||
if err == nil {
|
||||
for k, v := range m {
|
||||
dataMap[k] = v
|
||||
}
|
||||
continue
|
||||
}
|
||||
dataValues, err := url.ParseQuery(dataExpr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for dataKey := range dataValues {
|
||||
dataMap[dataKey] = strings.Trim(dataValues.Get(dataKey), "\"'")
|
||||
}
|
||||
}
|
||||
s.Request.Body = dataMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromCurl) makeRequestForm(formList []string) error {
|
||||
if s.Request.Upload == nil {
|
||||
s.Request.Upload = make(map[string]interface{})
|
||||
}
|
||||
for _, formExpr := range formList {
|
||||
if !strings.Contains(formExpr, "=") {
|
||||
return errors.Errorf("option --form: is badly used: %v", formExpr)
|
||||
}
|
||||
if i := strings.Index(formExpr, "="); i != -1 {
|
||||
formKey := strings.TrimSpace(formExpr[:i])
|
||||
var formValue string
|
||||
if i < len(formExpr)-1 {
|
||||
formValue = strings.TrimSpace(formExpr[i+1:])
|
||||
}
|
||||
s.Request.Upload[formKey] = strings.Trim(formValue, "\"")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var curlPath = "../../../examples/data/curl/curl_examples.txt"
|
||||
|
||||
func TestLoadCurlCase(t *testing.T) {
|
||||
tCase, err := LoadCurlCase(curlPath)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, 6, len(tCase.Steps)) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl httpbin.org
|
||||
if !assert.Equal(t, "curl httpbin.org", tCase.Steps[0].StepName) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.EqualValues(t, "GET", tCase.Steps[0].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "http://httpbin.org", tCase.Steps[0].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl https://httpbin.org/get?key1=value1&key2=value2
|
||||
if !assert.Equal(t, "https://httpbin.org/get", tCase.Steps[1].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}, tCase.Steps[1].Request.Params) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl -H "Content-Type: application/json" \
|
||||
// -H "Authorization: Bearer b7d03a6947b217efb6f3ec3bd3504582" \
|
||||
// -d '{"type":"A","name":"www","data":"162.10.66.0","priority":null,"port":null,"weight":null}' \
|
||||
// "https://httpbin.org/post"
|
||||
if !assert.EqualValues(t, "POST", tCase.Steps[2].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"Authorization": "Bearer b7d03a6947b217efb6f3ec3bd3504582",
|
||||
"Content-Type": "application/json",
|
||||
}, tCase.Steps[2].Request.Headers) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{
|
||||
"data": "162.10.66.0",
|
||||
"name": "www",
|
||||
"port": nil,
|
||||
"priority": nil,
|
||||
"type": "A",
|
||||
"weight": nil,
|
||||
}, tCase.Steps[2].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl -F "dummyName=dummyFile" -F file1=@file1.txt -F file2=@file2.txt https://httpbin.org/post
|
||||
if !assert.Equal(t, map[string]interface{}{
|
||||
"dummyName": "dummyFile",
|
||||
"file1": "@file1.txt",
|
||||
"file2": "@file2.txt",
|
||||
}, tCase.Steps[3].Request.Upload) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl https://httpbin.org/post \
|
||||
// -d 'shipment[to_address][id]=adr_HrBKVA85' \
|
||||
// -d 'shipment[from_address][id]=adr_VtuTOj7o' \
|
||||
// -d 'shipment[parcel][id]=prcl_WDv2VzHp' \
|
||||
// -d 'shipment[is_return]=true' \
|
||||
// -d 'shipment[customs_info][id]=cstinfo_bl5sE20Y'
|
||||
if !assert.Equal(t, map[string]interface{}{
|
||||
"shipment[customs_info][id]": "cstinfo_bl5sE20Y",
|
||||
"shipment[from_address][id]": "adr_VtuTOj7o",
|
||||
"shipment[is_return]": "true",
|
||||
"shipment[parcel][id]": "prcl_WDv2VzHp",
|
||||
"shipment[to_address][id]": "adr_HrBKVA85",
|
||||
}, tCase.Steps[4].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// curl https://httpbing.org/post -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
// --data "key1=value+1&key2=value%3A2"
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}, tCase.Steps[5].Request.Headers) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{
|
||||
"key1": "value 1",
|
||||
"key2": "value:2",
|
||||
}, tCase.Steps[5].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func convert2GoTestScripts(paths ...string) error {
|
||||
log.Warn().Msg("convert to gotest scripts is not supported yet")
|
||||
os.Exit(1)
|
||||
|
||||
// format pytest scripts with black
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testcase.tmpl
|
||||
var testcaseTemplate string
|
||||
@@ -1,624 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/json"
|
||||
)
|
||||
|
||||
// ==================== model definition starts here ====================
|
||||
|
||||
/*
|
||||
HTTP Archive (HAR) format
|
||||
https://w3c.github.io/web-performance/specs/HAR/Overview.html
|
||||
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
|
||||
*/
|
||||
|
||||
// CaseHar is a container type for deserialization
|
||||
type CaseHar struct {
|
||||
Log Log `json:"log"`
|
||||
}
|
||||
|
||||
// Log represents the root of the exported data. This object MUST be present and its name MUST be "log".
|
||||
type Log struct {
|
||||
// The object contains the following name/value pairs:
|
||||
|
||||
// Required. Version number of the format.
|
||||
Version string `json:"version"`
|
||||
// Required. An object of type creator that contains the name and version
|
||||
// information of the log creator application.
|
||||
Creator Creator `json:"creator"`
|
||||
// Optional. An object of type browser that contains the name and version
|
||||
// information of the user agent.
|
||||
Browser Browser `json:"browser"`
|
||||
// Optional. An array of objects of type page, each representing one exported
|
||||
// (tracked) page. Leave out this field if the application does not support
|
||||
// grouping by pages.
|
||||
Pages []Page `json:"pages,omitempty"`
|
||||
// Required. An array of objects of type entry, each representing one
|
||||
// exported (tracked) HTTP request.
|
||||
Entries []Entry `json:"entries"`
|
||||
// Optional. A comment provided by the user or the application. Sorting
|
||||
// entries by startedDateTime (starting from the oldest) is preferred way how
|
||||
// to export data since it can make importing faster. However the reader
|
||||
// application should always make sure the array is sorted (if required for
|
||||
// the import).
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Creator contains information about the log creator application
|
||||
type Creator struct {
|
||||
// Required. The name of the application that created the log.
|
||||
Name string `json:"name"`
|
||||
// Required. The version number of the application that created the log.
|
||||
Version string `json:"version"`
|
||||
// Optional. A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Browser that created the log
|
||||
type Browser struct {
|
||||
// Required. The name of the browser that created the log.
|
||||
Name string `json:"name"`
|
||||
// Required. The version number of the browser that created the log.
|
||||
Version string `json:"version"`
|
||||
// Optional. A comment provided by the user or the browser.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Page object for every exported web page and one <entry> object for every HTTP request.
|
||||
// In case when an HTTP trace tool isn't able to group requests by a page,
|
||||
// the <pages> object is empty and individual requests doesn't have a parent page.
|
||||
type Page struct {
|
||||
/* There is one <page> object for every exported web page and one <entry>
|
||||
object for every HTTP request. In case when an HTTP trace tool isn't able to
|
||||
group requests by a page, the <pages> object is empty and individual
|
||||
requests doesn't have a parent page.
|
||||
*/
|
||||
|
||||
// Date and time stamp for the beginning of the page load
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00).
|
||||
StartedDateTime string `json:"startedDateTime"`
|
||||
// Unique identifier of a page within the . Entries use it to refer the parent page.
|
||||
ID string `json:"id"`
|
||||
// Page title.
|
||||
Title string `json:"title"`
|
||||
// Detailed timing info about page load.
|
||||
PageTiming PageTiming `json:"pageTiming"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PageTiming describes timings for various events (states) fired during the page load.
|
||||
// All times are specified in milliseconds. If a time info is not available appropriate field is set to -1.
|
||||
type PageTiming struct {
|
||||
// Content of the page loaded. Number of milliseconds since page load started
|
||||
// (page.startedDateTime). Use -1 if the timing does not apply to the current
|
||||
// request.
|
||||
// Depeding on the browser, onContentLoad property represents DOMContentLoad
|
||||
// event or document.readyState == interactive.
|
||||
OnContentLoad int `json:"onContentLoad"`
|
||||
// Page is loaded (onLoad event fired). Number of milliseconds since page
|
||||
// load started (page.startedDateTime). Use -1 if the timing does not apply
|
||||
// to the current request.
|
||||
OnLoad int `json:"onLoad"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Entry is a unique, optional Reference to the parent page.
|
||||
// Leave out this field if the application does not support grouping by pages.
|
||||
type Entry struct {
|
||||
Pageref string `json:"pageref,omitempty"`
|
||||
// Date and time stamp of the request start
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD).
|
||||
StartedDateTime string `json:"startedDateTime"`
|
||||
// Total elapsed time of the request in milliseconds. This is the sum of all
|
||||
// timings available in the timings object (i.e. not including -1 values) .
|
||||
Time float32 `json:"time"`
|
||||
// Detailed info about the request.
|
||||
Request Request `json:"request"`
|
||||
// Detailed info about the response.
|
||||
Response Response `json:"response"`
|
||||
// Info about cache usage.
|
||||
Cache Cache `json:"cache"`
|
||||
// Detailed timing info about request/response round trip.
|
||||
PageTimings PageTimings `json:"pageTimings"`
|
||||
// optional (new in 1.2) IP address of the server that was connected
|
||||
// (result of DNS resolution).
|
||||
ServerIPAddress string `json:"serverIPAddress,omitempty"`
|
||||
// optional (new in 1.2) Unique ID of the parent TCP/IP connection, can be
|
||||
// the client port number. Note that a port number doesn't have to be unique
|
||||
// identifier in cases where the port is shared for more connections. If the
|
||||
// port isn't available for the application, any other unique connection ID
|
||||
// can be used instead (e.g. connection index). Leave out this field if the
|
||||
// application doesn't support this info.
|
||||
Connection string `json:"connection,omitempty"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Request contains detailed info about performed request.
|
||||
type Request struct {
|
||||
// Request method (GET, POST, ...).
|
||||
Method string `json:"method"`
|
||||
// Absolute URL of the request (fragments are not included).
|
||||
URL string `json:"url"`
|
||||
// Request HTTP Version.
|
||||
HTTPVersion string `json:"httpVersion"`
|
||||
// List of cookie objects.
|
||||
Cookies []Cookie `json:"cookies"`
|
||||
// List of header objects.
|
||||
Headers []NVP `json:"headers"`
|
||||
// List of query parameter objects.
|
||||
QueryString []NVP `json:"queryString"`
|
||||
// Posted data.
|
||||
PostData PostData `json:"postData"`
|
||||
// Total number of bytes from the start of the HTTP request message until
|
||||
// (and including) the double CRLF before the body. Set to -1 if the info
|
||||
// is not available.
|
||||
HeaderSize int `json:"headerSize"`
|
||||
// Size of the request body (POST data payload) in bytes. Set to -1 if the
|
||||
// info is not available.
|
||||
BodySize int `json:"bodySize"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Response contains detailed info about the response.
|
||||
type Response struct {
|
||||
// Response status.
|
||||
Status int `json:"status"`
|
||||
// Response status description.
|
||||
StatusText string `json:"statusText"`
|
||||
// Response HTTP Version.
|
||||
HTTPVersion string `json:"httpVersion"`
|
||||
// List of cookie objects.
|
||||
Cookies []Cookie `json:"cookies"`
|
||||
// List of header objects.
|
||||
Headers []NVP `json:"headers"`
|
||||
// Details about the response body.
|
||||
Content Content `json:"content"`
|
||||
// Redirection target URL from the Location response header.
|
||||
RedirectURL string `json:"redirectURL"`
|
||||
// Total number of bytes from the start of the HTTP response message until
|
||||
// (and including) the double CRLF before the body. Set to -1 if the info is
|
||||
// not available.
|
||||
// The size of received response-headers is computed only from headers that
|
||||
// are really received from the server. Additional headers appended by the
|
||||
// browser are not included in this number, but they appear in the list of
|
||||
// header objects.
|
||||
HeadersSize int `json:"headersSize"`
|
||||
// Size of the received response body in bytes. Set to zero in case of
|
||||
// responses coming from the cache (304). Set to -1 if the info is not
|
||||
// available.
|
||||
BodySize int `json:"bodySize"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Cookie contains list of all cookies (used in <request> and <response> objects).
|
||||
type Cookie struct {
|
||||
// The name of the cookie.
|
||||
Name string `json:"name"`
|
||||
// The cookie value.
|
||||
Value string `json:"value"`
|
||||
// optional The path pertaining to the cookie.
|
||||
Path string `json:"path,omitempty"`
|
||||
// optional The host of the cookie.
|
||||
Domain string `json:"domain,omitempty"`
|
||||
// optional Cookie expiration time.
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.123+02:00).
|
||||
Expires string `json:"expires,omitempty"`
|
||||
// optional Set to true if the cookie is HTTP only, false otherwise.
|
||||
HTTPOnly bool `json:"httpOnly,omitempty"`
|
||||
// optional (new in 1.2) True if the cookie was transmitted over ssl, false
|
||||
// otherwise.
|
||||
Secure bool `json:"secure,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment bool `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// NVP is simply a name/value pair with a comment
|
||||
type NVP struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PostData describes posted data, if any (embedded in <request> object).
|
||||
type PostData struct {
|
||||
// Mime type of posted data.
|
||||
MimeType string `json:"mimeType"`
|
||||
// List of posted parameters (in case of URL encoded parameters).
|
||||
Params []PostParam `json:"params"`
|
||||
// Plain text posted data
|
||||
Text string `json:"text"`
|
||||
// optional (new in 1.2) A comment provided by the user or the
|
||||
// application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PostParam is a list of posted parameters, if any (embedded in <postData> object).
|
||||
type PostParam struct {
|
||||
// name of a posted parameter.
|
||||
Name string `json:"name"`
|
||||
// optional value of a posted parameter or content of a posted file.
|
||||
Value string `json:"value,omitempty"`
|
||||
// optional name of a posted file.
|
||||
FileName string `json:"fileName,omitempty"`
|
||||
// optional content type of a posted file.
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Content describes details about response content (embedded in <response> object).
|
||||
type Content struct {
|
||||
// Length of the returned content in bytes. Should be equal to
|
||||
// response.bodySize if there is no compression and bigger when the content
|
||||
// has been compressed.
|
||||
Size int `json:"size"`
|
||||
// optional Number of bytes saved. Leave out this field if the information
|
||||
// is not available.
|
||||
Compression int `json:"compression,omitempty"`
|
||||
// MIME type of the response text (value of the Content-Type response
|
||||
// header). The charset attribute of the MIME type is included (if
|
||||
// available).
|
||||
MimeType string `json:"mimeType"`
|
||||
// optional Response body sent from the server or loaded from the browser
|
||||
// cache. This field is populated with textual content only. The text field
|
||||
// is either HTTP decoded text or a encoded (e.g. "base64") representation of
|
||||
// the response body. Leave out this field if the information is not
|
||||
// available.
|
||||
Text string `json:"text,omitempty"`
|
||||
// optional (new in 1.2) Encoding used for response text field e.g
|
||||
// "base64". Leave out this field if the text field is HTTP decoded
|
||||
// (decompressed & unchunked), than trans-coded from its original character
|
||||
// set into UTF-8.
|
||||
Encoding string `json:"encoding,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Cache contains info about a request coming from browser cache.
|
||||
type Cache struct {
|
||||
// optional State of a cache entry before the request. Leave out this field
|
||||
// if the information is not available.
|
||||
BeforeRequest CacheObject `json:"beforeRequest,omitempty"`
|
||||
// optional State of a cache entry after the request. Leave out this field if
|
||||
// the information is not available.
|
||||
AfterRequest CacheObject `json:"afterRequest,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// CacheObject is used by both beforeRequest and afterRequest
|
||||
type CacheObject struct {
|
||||
// optional - Expiration time of the cache entry.
|
||||
Expires string `json:"expires,omitempty"`
|
||||
// The last time the cache entry was opened.
|
||||
LastAccess string `json:"lastAccess"`
|
||||
// Etag
|
||||
ETag string `json:"eTag"`
|
||||
// The number of times the cache entry has been opened.
|
||||
HitCount int `json:"hitCount"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PageTimings describes various phases within request-response round trip.
|
||||
// All times are specified in milliseconds.
|
||||
type PageTimings struct {
|
||||
Blocked int `json:"blocked,omitempty"`
|
||||
// optional - Time spent in a queue waiting for a network connection. Use -1
|
||||
// if the timing does not apply to the current request.
|
||||
DNS int `json:"dns,omitempty"`
|
||||
// optional - DNS resolution time. The time required to resolve a host name.
|
||||
// Use -1 if the timing does not apply to the current request.
|
||||
Connect int `json:"connect,omitempty"`
|
||||
// optional - Time required to create TCP connection. Use -1 if the timing
|
||||
// does not apply to the current request.
|
||||
Send int `json:"send"`
|
||||
// Time required to send HTTP request to the server.
|
||||
Wait int `json:"wait"`
|
||||
// Waiting for a response from the server.
|
||||
Receive int `json:"receive"`
|
||||
// Time required to read entire response from the server (or cache).
|
||||
Ssl int `json:"ssl,omitempty"`
|
||||
// optional (new in 1.2) - Time required for SSL/TLS negotiation. If this
|
||||
// field is defined then the time is also included in the connect field (to
|
||||
// ensure backward compatibility with HAR 1.1). Use -1 if the timing does not
|
||||
// apply to the current request.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// optional (new in 1.2) - A comment provided by the user or the application.
|
||||
}
|
||||
|
||||
// TestResult contains results for an individual HTTP request
|
||||
type TestResult struct {
|
||||
URL string `json:"url"`
|
||||
Status int `json:"status"` // 200, 500, etc.
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Latency int `json:"latency"` // milliseconds
|
||||
Method string `json:"method"`
|
||||
HarFile string `json:"harfile"`
|
||||
}
|
||||
|
||||
// ==================== model definition ends here ====================
|
||||
|
||||
func LoadHARCase(path string) (*hrp.TestCaseDef, error) {
|
||||
// load har file
|
||||
caseHAR, err := loadCaseHAR(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to TestCase format
|
||||
return caseHAR.ToTestCase()
|
||||
}
|
||||
|
||||
func loadCaseHAR(path string) (*CaseHar, error) {
|
||||
caseHAR := new(CaseHar)
|
||||
err := hrp.LoadFileObject(path, caseHAR)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load har file failed")
|
||||
}
|
||||
if reflect.ValueOf(*caseHAR).IsZero() {
|
||||
return nil, errors.New("invalid har file")
|
||||
}
|
||||
return caseHAR, nil
|
||||
}
|
||||
|
||||
// convert CaseHar to TestCase format
|
||||
func (c *CaseHar) ToTestCase() (*hrp.TestCaseDef, error) {
|
||||
teststeps, err := c.prepareTestSteps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tCase := &hrp.TestCaseDef{
|
||||
Config: c.prepareConfig(),
|
||||
Steps: teststeps,
|
||||
}
|
||||
err = hrp.ConvertCaseCompatibility(tCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func (c *CaseHar) prepareConfig() *hrp.TConfig {
|
||||
return hrp.NewConfig("testcase description").
|
||||
SetVerifySSL(false)
|
||||
}
|
||||
|
||||
func (c *CaseHar) prepareTestSteps() ([]*hrp.TStep, error) {
|
||||
var steps []*hrp.TStep
|
||||
for _, entry := range c.Log.Entries {
|
||||
step, err := c.prepareTestStep(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (c *CaseHar) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
|
||||
log.Info().
|
||||
Str("method", entry.Request.Method).
|
||||
Str("url", entry.Request.URL).
|
||||
Msg("convert teststep")
|
||||
|
||||
step := &stepFromHAR{
|
||||
TStep: hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
StepConfig: hrp.StepConfig{
|
||||
Validators: make([]interface{}, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := step.makeRequestMethod(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeValidate(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step.TStep, nil
|
||||
}
|
||||
|
||||
type stepFromHAR struct {
|
||||
hrp.TStep
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
|
||||
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
|
||||
u, err := url.Parse(entry.Request.URL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request url failed")
|
||||
return err
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
for _, param := range entry.Request.QueryString {
|
||||
s.Request.Params[param.Name] = param.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
|
||||
// use cookies from har
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
for _, cookie := range entry.Request.Cookies {
|
||||
s.Request.Cookies[cookie.Name] = cookie.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
|
||||
// use headers from har
|
||||
s.Request.Headers = make(map[string]string)
|
||||
for _, header := range entry.Request.Headers {
|
||||
if strings.EqualFold(header.Name, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.Request.Headers[header.Name] = header.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
|
||||
mimeType := entry.Request.PostData.MimeType
|
||||
if mimeType == "" {
|
||||
// GET/HEAD/DELETE without body
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST/PUT with body
|
||||
if strings.HasPrefix(mimeType, "application/json") {
|
||||
// post json
|
||||
var body interface{}
|
||||
if entry.Request.PostData.Text == "" {
|
||||
body = nil
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request body failed")
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.Request.Body = body
|
||||
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
|
||||
// post form
|
||||
paramsMap := make(map[string]string)
|
||||
for _, param := range entry.Request.PostData.Params {
|
||||
paramsMap[param.Name] = param.Value
|
||||
}
|
||||
s.Request.Body = paramsMap
|
||||
} else if strings.HasPrefix(mimeType, "text/plain") {
|
||||
// post raw data
|
||||
s.Request.Body = entry.Request.PostData.Text
|
||||
} else {
|
||||
// TODO
|
||||
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeValidate(entry *Entry) error {
|
||||
// make validator for response status code
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "status_code",
|
||||
Assert: "equals",
|
||||
Expect: entry.Response.Status,
|
||||
Message: "assert response status code",
|
||||
})
|
||||
|
||||
// make validators for response headers
|
||||
for _, header := range entry.Response.Headers {
|
||||
// assert Content-Type
|
||||
if strings.EqualFold(header.Name, "Content-Type") {
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "headers.\"Content-Type\"",
|
||||
Assert: "equals",
|
||||
Expect: header.Value,
|
||||
Message: "assert response header Content-Type",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make validators for response body
|
||||
respBody := entry.Response.Content
|
||||
if respBody.Text == "" {
|
||||
// response body is empty
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(respBody.MimeType, "application/json") {
|
||||
var data []byte
|
||||
var err error
|
||||
// response body is json
|
||||
if respBody.Encoding == "base64" {
|
||||
// decode base64 text
|
||||
data, err = base64.StdEncoding.DecodeString(respBody.Text)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode base64 error")
|
||||
}
|
||||
} else if respBody.Encoding == "" {
|
||||
// no encoding
|
||||
data = []byte(respBody.Text)
|
||||
} else {
|
||||
// other encoding type
|
||||
return nil
|
||||
}
|
||||
// convert to json
|
||||
var body interface{}
|
||||
if err = json.Unmarshal(data, &body); err != nil {
|
||||
return errors.Wrap(err, "json.Unmarshal body error")
|
||||
}
|
||||
jsonBody, ok := body.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("response body is not json, not matched with MimeType")
|
||||
}
|
||||
|
||||
// response body is json
|
||||
keys := make([]string, 0, len(jsonBody))
|
||||
for k := range jsonBody {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
// sort map keys to keep validators in stable order
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
value := jsonBody[key]
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
continue
|
||||
case []interface{}:
|
||||
continue
|
||||
default:
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: fmt.Sprintf("body.%s", key),
|
||||
Assert: "equals",
|
||||
Expect: v,
|
||||
Message: fmt.Sprintf("assert response body %s", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
)
|
||||
|
||||
var harPath = "../../../examples/data/har/demo.har"
|
||||
|
||||
var caseHar *CaseHar
|
||||
|
||||
func init() {
|
||||
caseHar, _ = loadCaseHAR(harPath)
|
||||
}
|
||||
|
||||
func TestLoadHAR(t *testing.T) {
|
||||
caseHAR, err := loadCaseHAR(harPath)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "GET", caseHAR.Log.Entries[0].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "POST", caseHAR.Log.Entries[1].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTCaseFromHAR(t *testing.T) {
|
||||
tCase, err := LoadHARCase(harPath)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request method
|
||||
if !assert.EqualValues(t, "GET", tCase.Steps[0].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.EqualValues(t, "POST", tCase.Steps[1].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request url
|
||||
if !assert.Equal(t, "https://postman-echo.com/get", tCase.Steps[0].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "https://postman-echo.com/post", tCase.Steps[1].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request params
|
||||
if !assert.Equal(t, "HDnY8", tCase.Steps[0].Request.Params["foo1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request cookies
|
||||
if !assert.NotEmpty(t, tCase.Steps[1].Request.Cookies["sails.sid"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request headers
|
||||
if !assert.Equal(t, "HttpRunnerPlus", tCase.Steps[0].Request.Headers["User-Agent"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "postman-echo.com", tCase.Steps[0].Request.Headers["Host"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request data
|
||||
if !assert.Equal(t, nil, tCase.Steps[0].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.Steps[1].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]string{"foo1": "HDnY8", "foo2": "12.3"}, tCase.Steps[2].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make validators
|
||||
validator, ok := tCase.Steps[0].Validators[0].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "status_code", validator.Check) {
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok = tCase.Steps[0].Validators[1].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) {
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok = tCase.Steps[0].Validators[2].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "body.url", validator.Check) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestURL(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
URL: "http://127.0.0.1:8080/api/login",
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, "http://127.0.0.1:8080/api/login", step.Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestHeaders(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
Headers: []NVP{
|
||||
{Name: "Content-Type", Value: "application/json; charset=utf-8"},
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}, step.Request.Headers) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestCookies(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
Cookies: []Cookie{
|
||||
{Name: "abc", Value: "123"},
|
||||
{Name: "UserName", Value: "leolee"},
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"abc": "123",
|
||||
"UserName": "leolee",
|
||||
}, step.Request.Cookies) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestDataParams(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
PostData: PostData{
|
||||
MimeType: "application/x-www-form-urlencoded; charset=utf-8",
|
||||
Params: []PostParam{
|
||||
{Name: "a", Value: "1"},
|
||||
{Name: "b", Value: "2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{"a": "1", "b": "2"}, step.Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestDataJSON(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
PostData: PostData{
|
||||
MimeType: "application/json; charset=utf-8",
|
||||
Text: "{\"a\":\"1\",\"b\":\"2\"}",
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]interface{}{"a": "1", "b": "2"}, step.Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
PostData: PostData{
|
||||
MimeType: "application/json; charset=utf-8",
|
||||
Text: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, nil, step.Request.Body) { // TODO
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeValidate(t *testing.T) {
|
||||
entry := &Entry{
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Headers: []NVP{
|
||||
{Name: "Content-Type", Value: "application/json; charset=utf-8"},
|
||||
},
|
||||
Content: Content{
|
||||
Size: 71,
|
||||
MimeType: "application/json; charset=utf-8",
|
||||
// map[Code:200 IsSuccess:true Message:<nil> Value:map[BlnResult:true]]
|
||||
Text: "eyJJc1N1Y2Nlc3MiOnRydWUsIkNvZGUiOjIwMCwiTWVzc2FnZSI6bnVsbCwiVmFsdWUiOnsiQmxuUmVzdWx0Ijp0cnVlfX0=",
|
||||
Encoding: "base64",
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := caseHar.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok := step.Validators[0].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
Check: "status_code",
|
||||
Expect: 200,
|
||||
Assert: "equals",
|
||||
Message: "assert response status code",
|
||||
}) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
validator, ok = step.Validators[1].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
Check: "headers.\"Content-Type\"",
|
||||
Expect: "application/json; charset=utf-8",
|
||||
Assert: "equals",
|
||||
Message: "assert response header Content-Type",
|
||||
}) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
validator, ok = step.Validators[2].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
Check: "body.Code",
|
||||
Expect: float64(200), // TODO
|
||||
Assert: "equals",
|
||||
Message: "assert response body Code",
|
||||
}) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package convert
|
||||
@@ -1,27 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
)
|
||||
|
||||
func LoadJSONCase(path string) (*hrp.TestCaseDef, error) {
|
||||
log.Info().Str("path", path).Msg("load json case file")
|
||||
caseJSON := new(hrp.TestCaseDef)
|
||||
err := hrp.LoadFileObject(path, caseJSON)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load json file failed")
|
||||
}
|
||||
|
||||
if caseJSON.Steps == nil {
|
||||
return nil, errors.New("invalid json case file, missing teststeps")
|
||||
}
|
||||
|
||||
err = hrp.ConvertCaseCompatibility(caseJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caseJSON, nil
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
"github.com/httprunner/httprunner/v5/hrp/internal/json"
|
||||
)
|
||||
|
||||
// ==================== model definition starts here ====================
|
||||
|
||||
/*
|
||||
Postman Collection format reference:
|
||||
https://schema.postman.com/json/collection/v2.0.0/collection.json
|
||||
https://schema.postman.com/json/collection/v2.1.0/collection.json
|
||||
*/
|
||||
|
||||
// CasePostman represents the postman exported file
|
||||
type CasePostman struct {
|
||||
Info TInfo `json:"info"`
|
||||
Items []TItem `json:"item"`
|
||||
}
|
||||
|
||||
// TInfo gives information about the collection
|
||||
type TInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
|
||||
// TItem contains the detail information of request and expected responses
|
||||
// item could be defined recursively
|
||||
type TItem struct {
|
||||
Items []TItem `json:"item"`
|
||||
Name string `json:"name"`
|
||||
Request TRequest `json:"request"`
|
||||
Responses []TResponse `json:"response"`
|
||||
}
|
||||
|
||||
type TRequest struct {
|
||||
Method string `json:"method"`
|
||||
Headers []TField `json:"header"`
|
||||
Body TBody `json:"body"`
|
||||
URL TUrl `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type TResponse struct {
|
||||
Name string `json:"name"`
|
||||
OriginalRequest TRequest `json:"originalRequest"`
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Headers []TField `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type TUrl struct {
|
||||
Raw string `json:"raw"`
|
||||
Protocol string `json:"protocol"`
|
||||
Path []string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
Query []TField `json:"query"`
|
||||
Variable []TField `json:"variable"`
|
||||
}
|
||||
|
||||
type TField struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Src string `json:"src"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
type TBody struct {
|
||||
Mode string `json:"mode"`
|
||||
FormData []TField `json:"formdata"`
|
||||
URLEncoded []TField `json:"urlencoded"`
|
||||
Raw string `json:"raw"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Options interface{} `json:"options"`
|
||||
}
|
||||
|
||||
// ==================== model definition ends here ====================
|
||||
|
||||
const (
|
||||
enumBodyRaw = "raw"
|
||||
enumBodyUrlEncoded = "urlencoded"
|
||||
enumBodyFormData = "formdata"
|
||||
enumBodyFile = "file"
|
||||
enumBodyGraphQL = "graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
enumFieldTypeText = "text"
|
||||
enumFieldTypeFile = "file"
|
||||
)
|
||||
|
||||
var contentTypeMap = map[string]string{
|
||||
"text": "text/plain",
|
||||
"javascript": "application/javascript",
|
||||
"json": "application/json",
|
||||
"html": "text/html",
|
||||
"xml": "application/xml",
|
||||
}
|
||||
|
||||
func LoadPostmanCase(path string) (*hrp.TestCaseDef, error) {
|
||||
log.Info().Str("path", path).Msg("load postman case file")
|
||||
casePostman, err := loadCasePostman(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to TestCase format
|
||||
return casePostman.ToTestCase()
|
||||
}
|
||||
|
||||
func loadCasePostman(path string) (*CasePostman, error) {
|
||||
casePostman := new(CasePostman)
|
||||
err := hrp.LoadFileObject(path, casePostman)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load postman file failed")
|
||||
}
|
||||
if casePostman.Items == nil {
|
||||
return nil, errors.New("invalid postman case file, missing items")
|
||||
}
|
||||
|
||||
return casePostman, nil
|
||||
}
|
||||
|
||||
func (c *CasePostman) ToTestCase() (*hrp.TestCaseDef, error) {
|
||||
teststeps, err := c.prepareTestSteps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tCase := &hrp.TestCaseDef{
|
||||
Config: c.prepareConfig(),
|
||||
Steps: teststeps,
|
||||
}
|
||||
err = hrp.ConvertCaseCompatibility(tCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func (c *CasePostman) prepareConfig() *hrp.TConfig {
|
||||
return hrp.NewConfig(c.Info.Name).
|
||||
SetVerifySSL(false)
|
||||
}
|
||||
|
||||
func (c *CasePostman) prepareTestSteps() ([]*hrp.TStep, error) {
|
||||
// recursively convert collection items into a list
|
||||
var itemList []TItem
|
||||
for _, item := range c.Items {
|
||||
extractItemList(item, &itemList)
|
||||
}
|
||||
|
||||
var steps []*hrp.TStep
|
||||
for _, item := range itemList {
|
||||
step, err := c.prepareTestStep(&item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func extractItemList(item TItem, itemList *[]TItem) {
|
||||
// current item contains no other items and request is not empty
|
||||
if len(item.Items) == 0 {
|
||||
if !reflect.DeepEqual(item.Request, TRequest{}) {
|
||||
*itemList = append(*itemList, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// look up all items inside
|
||||
for _, i := range item.Items {
|
||||
// append item name
|
||||
i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name)
|
||||
extractItemList(i, itemList)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CasePostman) prepareTestStep(item *TItem) (*hrp.TStep, error) {
|
||||
log.Info().
|
||||
Str("method", item.Request.Method).
|
||||
Str("url", item.Request.URL.Raw).
|
||||
Msg("convert teststep")
|
||||
|
||||
step := &stepFromPostman{
|
||||
TStep: hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
StepConfig: hrp.StepConfig{
|
||||
Validators: make([]interface{}, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := step.makeRequestName(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestMethod(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step.TStep, nil
|
||||
}
|
||||
|
||||
type stepFromPostman struct {
|
||||
hrp.TStep
|
||||
}
|
||||
|
||||
// makeRequestName indicates the step name the same as item name
|
||||
func (s *stepFromPostman) makeRequestName(item *TItem) error {
|
||||
s.StepName = item.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
|
||||
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestURL(item *TItem) error {
|
||||
rawUrl := item.Request.URL.Raw
|
||||
// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
|
||||
for _, field := range item.Request.URL.Variable {
|
||||
pathVar := ":" + field.Key
|
||||
rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1)
|
||||
}
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse URL error")
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestParams(item *TItem) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
for _, field := range item.Request.URL.Query {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
s.Request.Params[field.Key] = field.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
|
||||
// headers defined in postman collection
|
||||
s.Request.Headers = make(map[string]string)
|
||||
for _, field := range item.Request.Headers {
|
||||
if field.Disabled || strings.EqualFold(field.Key, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.Request.Headers[field.Key] = field.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
|
||||
// cookies defined in postman collection
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
for _, field := range item.Request.Headers {
|
||||
if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.parseRequestCookiesMap(field.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
|
||||
for _, cookie := range strings.Split(cookies, ";") {
|
||||
cookie = strings.TrimSpace(cookie)
|
||||
index := strings.Index(cookie, "=")
|
||||
if index == -1 {
|
||||
log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
|
||||
continue
|
||||
}
|
||||
s.Request.Cookies[cookie[:index]] = cookie[index+1:]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBody(item *TItem) error {
|
||||
mode := item.Request.Body.Mode
|
||||
if mode == "" {
|
||||
return nil
|
||||
}
|
||||
switch mode {
|
||||
case enumBodyRaw:
|
||||
return s.makeRequestBodyRaw(item)
|
||||
case enumBodyFormData:
|
||||
return s.makeRequestBodyFormData(item)
|
||||
case enumBodyUrlEncoded:
|
||||
return s.makeRequestBodyUrlEncoded(item)
|
||||
case enumBodyFile, enumBodyGraphQL:
|
||||
return errors.Errorf("unsupported body type: %v", mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("make request body (raw) failed: %v", p)
|
||||
}
|
||||
}()
|
||||
|
||||
languageType := "text"
|
||||
iOptions := item.Request.Body.Options
|
||||
if iOptions != nil {
|
||||
iLanguage := iOptions.(map[string]interface{})["raw"]
|
||||
if iLanguage != nil {
|
||||
languageType = iLanguage.(map[string]interface{})["language"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
s.Request.Body = item.Request.Body.Raw
|
||||
contentType := s.Request.Headers["Content-Type"]
|
||||
if strings.Contains(contentType, "application/json") || languageType == "json" {
|
||||
var iBody interface{}
|
||||
err = json.Unmarshal([]byte(item.Request.Body.Raw), &iBody)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "make request body (raw -> json) failed")
|
||||
}
|
||||
s.Request.Body = iBody
|
||||
}
|
||||
|
||||
if contentType == "" {
|
||||
s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyFormData(item *TItem) error {
|
||||
s.Request.Upload = make(map[string]interface{})
|
||||
for _, field := range item.Request.Body.FormData {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
// form data could be text or file
|
||||
if field.Type == enumFieldTypeText {
|
||||
s.Request.Upload[field.Key] = field.Value
|
||||
} else if field.Type == enumFieldTypeFile {
|
||||
s.Request.Upload[field.Key] = field.Src
|
||||
} else {
|
||||
return errors.Errorf("make request body form data failed: unexpect field type: %v", field.Type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
|
||||
payloadMap := make(map[string]string)
|
||||
for _, field := range item.Request.Body.URLEncoded {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
payloadMap[field.Key] = field.Value
|
||||
}
|
||||
s.Request.Body = payloadMap
|
||||
s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO makeValidate from test scripts
|
||||
func (s *stepFromPostman) makeValidate(item *TItem) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var collectionPath = "../../../examples/data/postman/postman_collection.json"
|
||||
|
||||
func TestLoadCollection(t *testing.T) {
|
||||
casePostman, err := loadCasePostman(collectionPath)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCaseFromCollection(t *testing.T) {
|
||||
tCase, err := LoadPostmanCase(collectionPath)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check name
|
||||
if !assert.Equal(t, "postman collection demo", tCase.Config.Name) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check method
|
||||
if !assert.EqualValues(t, "GET", tCase.Steps[0].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.EqualValues(t, "POST", tCase.Steps[1].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check url
|
||||
if !assert.Equal(t, "https://postman-echo.com/get", tCase.Steps[0].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "https://postman-echo.com/post", tCase.Steps[1].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check params
|
||||
if !assert.Equal(t, "v1", tCase.Steps[0].Request.Params["k1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check cookies (pass, postman collection doesn't contain cookies)
|
||||
// check headers
|
||||
if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.Steps[2].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "application/json", tCase.Steps[3].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "text/plain", tCase.Steps[4].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "HttpRunner", tCase.Steps[5].Request.Headers["User-Agent"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check body
|
||||
if !assert.Equal(t, nil, tCase.Steps[0].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.Steps[2].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.Steps[3].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "have a nice day", tCase.Steps[4].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, nil, tCase.Steps[5].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package convert
|
||||
@@ -1,23 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
)
|
||||
|
||||
func LoadSwaggerCase(path string) (*hrp.TestCaseDef, error) {
|
||||
// load swagger file
|
||||
caseSwagger := new(spec.Swagger)
|
||||
err := hrp.LoadFileObject(path, caseSwagger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load swagger file failed")
|
||||
}
|
||||
if caseSwagger.Definitions == nil {
|
||||
return nil, errors.New("invalid swagger case file, missing definitions")
|
||||
}
|
||||
|
||||
// TODO: convert swagger to TCase
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/hrp"
|
||||
)
|
||||
|
||||
func LoadYAMLCase(path string) (*hrp.TestCaseDef, error) {
|
||||
// load yaml case file
|
||||
caseJSON := new(hrp.TestCaseDef)
|
||||
err := hrp.LoadFileObject(path, caseJSON)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load yaml file failed")
|
||||
}
|
||||
if reflect.ValueOf(*caseJSON).IsZero() {
|
||||
return nil, errors.New("invalid yaml file")
|
||||
}
|
||||
|
||||
err = hrp.ConvertCaseCompatibility(caseJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caseJSON, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user