Merge branch 'wings' into 'v5'

wings合入

See merge request iesqa/httprunner!65
This commit is contained in:
李隆 2025-03-05 11:45:28 +00:00
commit 201df9afed
156 changed files with 3197 additions and 1733 deletions

View File

@ -10,11 +10,11 @@ import (
"regexp"
"strings"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/funplugin/myexec"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/config"
@ -107,7 +107,7 @@ func (pt *pluginTemplate) generate(tmpl, output string) error {
func (pt *pluginTemplate) generatePy(output string) error {
// specify output file path
if output == "" {
output = filepath.Join(config.RootDir, PluginPySourceGenFile)
output = filepath.Join(config.GetConfig().RootDir, PluginPySourceGenFile)
} else if builtin.IsFolderPathExists(output) {
output = filepath.Join(output, PluginPySourceGenFile)
}
@ -155,7 +155,7 @@ func (pt *pluginTemplate) generateGo(output string) error {
// specify output file path
if output == "" {
output = filepath.Join(config.RootDir, PluginHashicorpGoBuiltFile)
output = filepath.Join(config.GetConfig().RootDir, PluginHashicorpGoBuiltFile)
} else if builtin.IsFolderPathExists(output) {
output = filepath.Join(output, PluginHashicorpGoBuiltFile)
}

View File

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

View File

@ -3,8 +3,8 @@ package adb
import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var serial string

View File

@ -8,8 +8,8 @@ import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var (

View File

@ -4,14 +4,14 @@ 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/funplugin/myexec"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/convert"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/convert"
)
var convertCmd = &cobra.Command{

View File

@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
)
type Application struct {

View File

@ -12,7 +12,7 @@ import (
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
)
type Device struct {

View File

@ -3,8 +3,8 @@ package ios
import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var iosRootCmd = &cobra.Command{

View File

@ -8,8 +8,8 @@ import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var installCmd = &cobra.Command{

40
cmd/ios/tunnel.go Normal file
View File

@ -0,0 +1,40 @@
package ios
import (
"context"
"os"
"strings"
"time"
"github.com/danielpaulus/go-ios/ios"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var tunnelCmd = &cobra.Command{
Use: "tunnel",
Short: "tunnel start",
RunE: func(cmd *cobra.Command, args []string) (err error) {
startTime := time.Now()
defer func() {
sdk.SendGA4Event("hrp_ios_tunnel", map[string]interface{}{
"args": strings.Join(args, "-"),
"success": err == nil,
"engagement_time_msec": time.Since(startTime).Milliseconds(),
})
}()
ctx := context.TODO()
err = uixt.StartTunnel(ctx, os.TempDir(), ios.HttpApiPort(), true)
if err != nil {
log.Error().Err(err).Msg("failed to start tunnel")
}
<-ctx.Done()
return err
},
}
func init() {
iosRootCmd.AddCommand(tunnelCmd)
}

View File

@ -8,8 +8,8 @@ import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var uninstallCmd = &cobra.Command{

View File

@ -14,6 +14,7 @@ var serverCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return server_ext.NewExtRouter().Run(port)
// return server.NewRouter().Run(port)
},
}

View File

@ -8,8 +8,8 @@ import (
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
// ConvertCaseCompatibility converts TestCase compatible with Golang engine style

View File

@ -4,8 +4,8 @@ import (
"reflect"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
type IConfig interface {
@ -80,7 +80,7 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
}
// SetThinkTime sets think time config for current testcase.
func (c *TConfig) SetThinkTime(strategy thinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
func (c *TConfig) SetThinkTime(strategy ThinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit}
return c
}
@ -194,7 +194,7 @@ func (c *TConfig) DisableAutoPopupHandler() *TConfig {
}
type ThinkTimeConfig struct {
Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore
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
}
@ -205,10 +205,10 @@ func (ttc *ThinkTimeConfig) checkThinkTime() {
}
// unset strategy, set default strategy
if ttc.Strategy == "" {
ttc.Strategy = thinkTimeDefault
ttc.Strategy = ThinkTimeDefault
}
// check think time
if ttc.Strategy == thinkTimeRandomPercentage {
if ttc.Strategy == ThinkTimeRandomPercentage {
if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map {
ttc.Setting = thinkTimeDefaultRandom
return
@ -237,7 +237,7 @@ func (ttc *ThinkTimeConfig) checkThinkTime() {
return
}
ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right}
} else if ttc.Strategy == thinkTimeMultiply {
} else if ttc.Strategy == ThinkTimeMultiply {
if ttc.Setting == nil {
ttc.Setting = float64(0) // default
return
@ -248,19 +248,19 @@ func (ttc *ThinkTimeConfig) checkThinkTime() {
return
}
ttc.Setting = value
} else if ttc.Strategy != thinkTimeIgnore {
} else if ttc.Strategy != ThinkTimeIgnore {
// unrecognized strategy, set default strategy
ttc.Strategy = thinkTimeDefault
ttc.Strategy = ThinkTimeDefault
}
}
type thinkTimeStrategy string
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
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 (

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -4,7 +4,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestAndroidDouyinE2E(t *testing.T) {

View File

@ -6,8 +6,8 @@ import (
"strconv"
"time"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var (

View File

@ -6,9 +6,9 @@ import (
"strconv"
"time"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var (

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestAndroidDouyinFeedTest(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestAndroidLiveSwipeTest(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestIOSDouyinFollowLive(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestHamonyDouyinFeedTest(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestIOSDouyinLive(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestWDALog(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestAndroidExpertTest(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestHarmonyDouyinE2E(t *testing.T) {

View File

@ -7,9 +7,9 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option"
)
var rootCmd = &cobra.Command{

View File

@ -15,8 +15,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func convertTimeToSeconds(timeStr string) (int, error) {

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestConvertTimeToSeconds(t *testing.T) {

3
go.mod
View File

@ -4,7 +4,6 @@ go 1.23.0
require (
code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
github.com/Masterminds/semver v1.5.0
github.com/andybalholm/brotli v1.0.4
github.com/danielpaulus/go-ios v1.0.161
@ -54,7 +53,7 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-plugin v1.4.10 // indirect

6
go.sum
View File

@ -1,7 +1,5 @@
code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27 h1:+wNJiEXXIUP6luKJRA4tfwDqfnWUON6LIopKD9tvUns=
code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27/go.mod h1:C2kq6TTE+JAOnqDorSwae1MQzRuex03RshuSUC2U/FY=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
@ -86,8 +84,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=

View File

@ -47,10 +47,6 @@ 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)
}

View File

@ -5,16 +5,13 @@ import (
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/csv"
builtinJSON "encoding/json"
"fmt"
"io"
"math"
"math/rand"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -23,7 +20,6 @@ import (
"strings"
"time"
"github.com/BurntSushi/locker"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
@ -229,6 +225,22 @@ func InterfaceType(raw interface{}) string {
return reflect.TypeOf(raw).String()
}
func LoadFile(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
}
func loadFromCSV(path string) []map[string]interface{} {
log.Info().Str("path", path).Msg("load csv file")
file, err := os.ReadFile(path)
@ -358,21 +370,18 @@ func ConvertToStringSlice(val interface{}) ([]string, error) {
}
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()))
minPort := 20000
maxPort := 50000
for i := 0; i < 10; i++ {
port := rand.Intn(maxPort-minPort+1) + minPort
addr := fmt.Sprintf("0.0.0.0:%d", port)
l, err := net.Listen("tcp", addr)
if err == nil {
defer l.Close() // 端口成功绑定后立即释放,返回该端口号
return port, nil
}
}()
return l.Addr().(*net.TCPAddr).Port, nil
}
return 0, errors.New("failed to get available port")
}
func GetCurrentDay() string {
@ -382,7 +391,7 @@ func GetCurrentDay() string {
return formattedDate
}
func fileExists(filepath string) bool {
func FileExists(filepath string) bool {
_, err := os.Stat(filepath)
if os.IsNotExist(err) {
return false // 文件不存在
@ -390,61 +399,6 @@ func fileExists(filepath string) bool {
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")

View File

@ -3,6 +3,7 @@ package config
import (
"os"
"path/filepath"
"sync"
"time"
"github.com/rs/zerolog/log"
@ -12,39 +13,60 @@ import (
const (
ResultsDirName = "results"
DownloadsDirName = "downloads"
ScreenshotsDirName = "screenshots"
ActionLogDirName = "action_log"
)
var (
type Config struct {
RootDir string
ResultsDir string
ResultsPath string
DownloadsPath string
ScreenShotsPath string
StartTime = time.Now()
StartTimeStr = StartTime.Format("20060102150405")
StartTime time.Time
ActionLogFilePath string
DeviceActionLogFilePath string
}
var (
globalConfig *Config
once sync.Once
)
func init() {
var err error
RootDir, err = os.Getwd()
if err != nil {
panic(err)
}
func GetConfig() *Config {
once.Do(func() {
cfg := &Config{
StartTime: time.Now(),
}
ResultsDir = filepath.Join(ResultsDirName, StartTimeStr)
ResultsPath = filepath.Join(RootDir, ResultsDir)
ScreenShotsPath = filepath.Join(ResultsPath, ScreenshotsDirName)
ActionLogFilePath = filepath.Join(ResultsDir, ActionLogDirName)
DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
var err error
cfg.RootDir, err = os.Getwd()
if err != nil {
panic(err)
}
// create results directory
if err := builtin.EnsureFolderExists(ResultsPath); err != nil {
log.Fatal().Err(err).Msg("create results directory failed")
}
if err := builtin.EnsureFolderExists(ScreenShotsPath); err != nil {
log.Fatal().Err(err).Msg("create screenshots directory failed")
}
startTimeStr := cfg.StartTime.Format("20060102150405")
cfg.ResultsDir = filepath.Join(ResultsDirName, startTimeStr)
cfg.ResultsPath = filepath.Join(cfg.RootDir, cfg.ResultsDir)
cfg.DownloadsPath = filepath.Join(cfg.RootDir, filepath.Join(DownloadsDirName, startTimeStr))
cfg.ScreenShotsPath = filepath.Join(cfg.ResultsPath, ScreenshotsDirName)
cfg.ActionLogFilePath = filepath.Join(cfg.ResultsDir, ActionLogDirName)
cfg.DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
// create results directory
if err := builtin.EnsureFolderExists(cfg.ResultsPath); err != nil {
log.Fatal().Err(err).Msg("create results directory failed")
}
if err := builtin.EnsureFolderExists(cfg.DownloadsPath); err != nil {
log.Fatal().Err(err).Msg("create downloads directory failed")
}
if err := builtin.EnsureFolderExists(cfg.ScreenShotsPath); err != nil {
log.Fatal().Err(err).Msg("create screenshots directory failed")
}
globalConfig = cfg
})
return globalConfig
}

View File

@ -1,19 +1,17 @@
package demo
package httpstat
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/httprunner/httprunner/v5/pkg/httpstat"
)
func TestMain(t *testing.T) {
var httpStat httpstat.Stat
var httpStat Stat
req, _ := http.NewRequest("GET", "https://httprunner.com", nil)
ctx := httpstat.WithHTTPStat(req, &httpStat)
ctx := WithHTTPStat(req, &httpStat)
client := &http.Client{
Timeout: time.Second * 10,

View File

@ -1 +1 @@
v5.0.0+2502192152
v5.0.0-beta-2503051939

View File

@ -12,6 +12,7 @@ import (
"gopkg.in/yaml.v2"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/json"
)
@ -85,7 +86,7 @@ func LoadTestCases(tests ...ITestCase) ([]*TestCase, error) {
// 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)
file, err := builtin.LoadFile(path)
if err != nil {
return errors.Wrap(err, "read file failed")
}
@ -145,19 +146,3 @@ func parseEnvContent(file []byte, obj interface{}) error {
}
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
}

View File

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

View File

@ -13,7 +13,7 @@ import (
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
Strategies map[string]IteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
}
@ -35,13 +35,13 @@ const (
*/
type Parameters []map[string]interface{}
type iteratorStrategy struct {
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)
func (p *Parser) InitParametersIterator(cfg *TConfig) (*ParametersIterator, error) {
parameters, err := p.LoadParameters(cfg.Parameters, cfg.Variables)
if err != nil {
return nil, err
}
@ -57,13 +57,13 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf
hasNext: true,
sequentialParameters: nil,
randomParameterNames: nil,
limit: config.Limit,
index: 0,
Limit: config.Limit,
Index: 0,
}
if len(parameters) == 0 {
iterator.data = map[string]Parameters{}
iterator.limit = 1
iterator.Limit = 1
return iterator
}
@ -85,24 +85,24 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf
}
// generate cartesian product for sequential parameters
iterator.sequentialParameters = genCartesianProduct(parametersList)
iterator.sequentialParameters = GenCartesianProduct(parametersList)
if iterator.limit < 0 {
if iterator.Limit < 0 {
log.Warn().Msg("parameters unlimited mode is only supported for load testing")
iterator.limit = 0
iterator.Limit = 0
}
if 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)
iterator.Limit = len(iterator.sequentialParameters)
} else {
// all parameters are selected by random
// only run once
iterator.limit = 1
iterator.Limit = 1
}
} else { // limit > 0
log.Info().Int("limit", iterator.limit).Msg("set limit for parameters")
log.Info().Int("limit", iterator.Limit).Msg("set limit for parameters")
}
return iterator
@ -114,14 +114,14 @@ type ParametersIterator struct {
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
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
iter.Limit = -1
}
func (iter *ParametersIterator) HasNext() bool {
@ -130,12 +130,12 @@ func (iter *ParametersIterator) HasNext() bool {
}
// unlimited mode
if iter.limit == -1 {
if iter.Limit == -1 {
return true
}
// reached limit
if iter.index >= iter.limit {
if iter.Index >= iter.Limit {
// cache query result
iter.hasNext = false
return false
@ -155,11 +155,11 @@ func (iter *ParametersIterator) Next() map[string]interface{} {
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 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)
index := iter.Index % len(iter.sequentialParameters)
selectedParameters = iter.sequentialParameters[index]
}
@ -172,8 +172,8 @@ func (iter *ParametersIterator) Next() map[string]interface{} {
}
}
iter.index++
if iter.limit > 0 && iter.index >= iter.limit {
iter.Index++
if iter.Limit > 0 && iter.Index >= iter.Limit {
iter.hasNext = false
}
@ -188,7 +188,7 @@ func (iter *ParametersIterator) Data() map[string]interface{} {
return res
}
func genCartesianProduct(multiParameters []Parameters) Parameters {
func GenCartesianProduct(multiParameters []Parameters) Parameters {
if len(multiParameters) == 0 {
return nil
}
@ -208,7 +208,7 @@ func genCartesianProduct(multiParameters []Parameters) Parameters {
}
/*
loadParameters loads parameters from multiple sources.
LoadParameters loads parameters from multiple sources.
parameter value may be in three types:
@ -240,7 +240,7 @@ parameter value may be in three types:
]
}
*/
func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
func (p *Parser) LoadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
map[string]Parameters, error) {
if len(configParameters) == 0 {
@ -291,7 +291,7 @@ func (p *Parser) loadParameters(configParameters map[string]interface{}, variabl
return nil, errors.New("config parameters raw value format error")
}
parameterSlice, err := convertParameters(k, parametersRawList)
parameterSlice, err := ConvertParameters(k, parametersRawList)
if err != nil {
return nil, err
}
@ -320,7 +320,7 @@ case 3:
key = "username-password"
parametersRawList = [["test1", "111111"], ["test2", "222222"]]
*/
func convertParameters(key string, parametersRawList interface{}) (parameterSlice []map[string]interface{}, err error) {
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")

View File

@ -10,17 +10,17 @@ import (
"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/funplugin"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
)
func newParser() *Parser {
func NewParser() *Parser {
return &Parser{}
}

View File

@ -1,8 +1,11 @@
package hrp
import (
"io"
"net/http"
"net/url"
"sort"
"strings"
"testing"
"time"
@ -208,7 +211,7 @@ func TestParseDataStringWithVariables(t *testing.T) {
{"abc$var_5", "abctrue"}, // "abcTrue"
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
parsedData, err := parser.Parse(data.expr, variablesMapping)
if !assert.NoError(t, err) {
@ -233,7 +236,7 @@ func TestParseDataStringWithUndefinedVariables(t *testing.T) {
{"/api/$SECRET_KEY", "/api/$SECRET_KEY"}, // raise error
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
parsedData, err := parser.Parse(data.expr, variablesMapping)
if !assert.Error(t, err) {
@ -278,7 +281,7 @@ func TestParseDataStringWithVariablesAbnormal(t *testing.T) {
{"ABC$var_1{}a", "ABCabc{}a"}, // {}
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
parsedData, err := parser.Parse(data.expr, variablesMapping)
if !assert.NoError(t, err) {
@ -309,7 +312,7 @@ func TestParseDataMapWithVariables(t *testing.T) {
{map[string]interface{}{"$var2": "$val1"}, map[string]interface{}{"123": 200}},
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
parsedData, err := parser.Parse(data.expr, variablesMapping)
if !assert.NoError(t, err) {
@ -343,7 +346,7 @@ func TestParseHeaders(t *testing.T) {
{map[string]string{"$var2": "$val2"}, map[string]string{"123": "<nil>"}},
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
parsedHeaders, err := parser.ParseHeaders(data.rawHeaders, variablesMapping)
if !assert.NoError(t, err) {
@ -488,7 +491,7 @@ func TestMergeValidators(t *testing.T) {
}
func TestCallBuiltinFunction(t *testing.T) {
parser := newParser()
parser := NewParser()
// call function without arguments
_, err := parser.callFunc("get_timestamp")
@ -601,7 +604,7 @@ func TestParseDataStringWithFunctions(t *testing.T) {
{"123${gen_random_string($n)}abc", 11},
}
parser := newParser()
parser := NewParser()
for _, data := range testData1 {
value, err := parser.Parse(data.expr, variablesMapping)
if !assert.NoError(t, err) {
@ -670,7 +673,7 @@ func TestParseVariables(t *testing.T) {
},
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
value, err := parser.ParseVariables(data.rawVars)
if !assert.NoError(t, err) {
@ -701,7 +704,7 @@ func TestParseVariablesAbnormal(t *testing.T) {
},
}
parser := newParser()
parser := NewParser()
for _, data := range testData {
value, err := parser.ParseVariables(data.rawVars)
if !assert.Error(t, err) {
@ -784,3 +787,193 @@ func TestFindallVariables(t *testing.T) {
}
}
}
func TestSearchJmespath(t *testing.T) {
testText := `{"a": {"b": "foo"}, "c": "bar", "d": {"e": [{"f": "foo"}, {"f": "bar"}]}}`
testData := []struct {
raw string
expected string
}{
{"body.a.b", "foo"},
{"body.c", "bar"},
{"body.d.e[0].f", "foo"},
{"body.d.e[1].f", "bar"},
}
resp := http.Response{}
resp.Body = io.NopCloser(strings.NewReader(testText))
respObj, err := newHttpResponseObject(t, NewParser(), &resp)
if err != nil {
t.Fatal()
}
for _, data := range testData {
if !assert.Equal(t, data.expected, respObj.searchJmespath(data.raw)) {
t.Fatal()
}
}
}
func TestSearchRegexp(t *testing.T) {
testText := `
<ul class="nav navbar-nav navbar-right">
<li><a href="/order/addToCart" style="color: white"><i class="fa fa-shopping-cart fa-2x"></i><span class="badge">0</span></a></li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#" style="color: white">
Leo <i class="fa fa-cog fa-2x"></i><span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/user/changePassword">Change Password</a></li>
<li><a href="/user/addAddress">Shipping</a></li>
<li><a href="/user/addCard">Payment</a></li>
<li><a href="/order/orderHistory">Order History</a></li>
<li><a href="/user/signOut">Sign Out</a></li>
</ul>
</li>
<li>&nbsp;&nbsp;&nbsp;</li>
<li><a href="/user/signOut" style="color: white"><i class="fa fa-sign-out fa-2x"></i>
Sign Out</a></li>
</ul>
`
testData := []struct {
raw string
expected string
}{
{"/user/signOut\">(.*)</a></li>", "Sign Out"},
{"<li><a href=\"/user/(.*)\" style", "signOut"},
{" (.*) <i class=\"fa fa-cog fa-2x\"></i>", "Leo"},
}
// new response object
resp := http.Response{}
resp.Body = io.NopCloser(strings.NewReader(testText))
respObj, err := newHttpResponseObject(t, NewParser(), &resp)
if err != nil {
t.Fatal()
}
for _, data := range testData {
if !assert.Equal(t, data.expected, respObj.searchRegexp(data.raw)) {
t.Fatal()
}
}
}
func TestConvertCheckExpr(t *testing.T) {
exprs := []struct {
before string
after string
}{
// normal check expression
{"a.b.c", "a.b.c"},
{"a.\"b-c\".d", "a.\"b-c\".d"},
{"a.b-c.d", "a.b-c.d"},
{"body.args.a[-1]", "body.args.a[-1]"},
// check expression using regex
{"covering (.*) testing,", "covering (.*) testing,"},
{" (.*) a-b-c", " (.*) a-b-c"},
// abnormal check expression
{"headers.Content-Type", "headers.\"Content-Type\""},
{"headers.\"Content-Type", "headers.\"Content-Type\""},
{"headers.Content-Type\"", "headers.\"Content-Type\""},
{"headers.User-Agent", "headers.\"User-Agent\""},
}
for _, expr := range exprs {
assert.Equal(t, expr.after, convertJmespathExpr(expr.before))
}
}
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()
}
}

View File

@ -640,6 +640,7 @@ func (d *Device) InstallAPK(apkPath string, args ...string) (string, error) {
haserr := func(ret string) bool {
return strings.Contains(ret, "Failure")
}
// 该方法掉线不会返回error。导致误认为安装成功
if d.HasFeature(FeatAbbExec) {
raw, err := d.installViaABBExec(apkFile)
if err != nil {
@ -734,6 +735,11 @@ func (d *Device) ScreenCap() ([]byte, error) {
return nil, err
}
// remove temp file
defer func() {
go d.RunShellCommand("rm", tempPath)
}()
buffer := bytes.NewBuffer(nil)
err = d.Pull(tempPath, buffer)
return buffer.Bytes(), err

View File

@ -1,71 +0,0 @@
package driver_ext
import (
"fmt"
"time"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
)
type IStubDriver interface {
uixt.IDriver
LoginNoneUI(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error)
LogoutNoneUI(packageName string) error
}
func NewXTDriver(driver uixt.IDriver, opts ...ai.AIServiceOption) *XTDriver {
services := ai.NewAIService(opts...)
driverExt := &XTDriver{
XTDriver: &uixt.XTDriver{
IDriver: driver,
CVService: services.ICVService,
LLMService: services.ILLMService,
},
}
return driverExt
}
type XTDriver struct {
*uixt.XTDriver
}
func (dExt *XTDriver) InstallByUrl(url string, opts ...option.InstallOption) error {
appPath, err := builtin.DownloadFileByUrl(url)
if err != nil {
return err
}
err = dExt.Install(appPath, opts...)
if err != nil {
return err
}
return nil
}
func (dExt *XTDriver) Install(filePath string, opts ...option.InstallOption) error {
if _, ok := dExt.GetDevice().(*uixt.AndroidDevice); ok {
stopChan := make(chan struct{})
go func() {
ticker := time.NewTicker(8 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = dExt.TapByOCR("^(.*无视风险安装|正在扫描.*|我知道了|稍后继续|稍后提醒|继续安装|知道了|确定|继续|完成|点击继续安装|继续安装旧版本|替换|.*正在安装|安装|授权本次安装|重新安装|仍要安装|更多详情|我知道了|已了解此应用未经检测.)$", option.WithRegex(true), option.WithIgnoreNotFoundError(true))
case <-stopChan:
fmt.Println("Ticker stopped")
return
}
}
}()
defer func() {
close(stopChan)
}()
}
return dExt.GetDevice().Install(filePath, opts...)
}

View File

@ -1,286 +0,0 @@
package driver_ext
import (
"encoding/json"
"fmt"
"net/url"
"time"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type StubIOSDriver struct {
*uixt.WDADriver
timeout time.Duration
douyinUrlPrefix string
douyinLiteUrlPrefix string
}
const (
IOSDouyinPort = 32921
IOSDouyinLitePort = 33461
defaultBightInsightPort = 8000
)
func NewStubIOSDriver(dev *uixt.IOSDevice) (*StubIOSDriver, error) {
// lazy setup WDA
dev.Options.LazySetup = true
wdaDriver, err := uixt.NewWDADriver(dev)
if err != nil {
return nil, err
}
driver := &StubIOSDriver{
WDADriver: wdaDriver,
timeout: 10 * time.Second,
}
// setup driver
if err := driver.Setup(); err != nil {
return nil, err
}
// register driver session reset handler
driver.Session.RegisterResetHandler(driver.Setup)
return driver, nil
}
func (s *StubIOSDriver) Setup() error {
localPort, err := s.getLocalPort()
if err != nil {
return err
}
err = s.Session.SetupPortForward(localPort)
if err != nil {
return err
}
s.Session.SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", localPort))
localDouyinPort, err := builtin.GetFreePort()
if err != nil {
return errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("get free port failed: %v", err))
}
if err = s.Device.Forward(localDouyinPort, IOSDouyinPort); err != nil {
return errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("forward tcp port failed: %v", err))
}
s.douyinUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinPort)
localDouyinLitePort, err := builtin.GetFreePort()
if err != nil {
return errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("get free port failed: %v", err))
}
if err = s.Device.Forward(localDouyinLitePort, IOSDouyinLitePort); err != nil {
return errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("forward tcp port failed: %v", err))
}
s.douyinLiteUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinLitePort)
return nil
}
func (s *StubIOSDriver) getLocalPort() (int, error) {
localStubPort, err := builtin.GetFreePort()
if err != nil {
return 0, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("get free port failed: %v", err))
}
if err = s.Device.Forward(localStubPort, defaultBightInsightPort); err != nil {
return 0, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("forward tcp port failed: %v", err))
}
return localStubPort, nil
}
func (s *StubIOSDriver) Source(srcOpt ...option.SourceOption) (string, error) {
resp, err := s.Session.GET("/source?format=json&onlyWeb=false")
if err != nil {
log.Error().Err(err).Msg("get source err")
return "", nil
}
return string(resp), nil
}
func (s *StubIOSDriver) OpenUrl(urlStr string, options ...option.ActionOption) (err error) {
targetUrl := fmt.Sprintf("/openURL?url=%s", url.QueryEscape(urlStr))
_, err = s.Session.GET(targetUrl)
if err != nil {
log.Error().Err(err).Msg("get source err")
return nil
}
return nil
}
func (s *StubIOSDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
appInfo, err := s.ForegroundInfo()
if err != nil {
return info, err
}
if appInfo.BundleId == "com.ss.iphone.ugc.AwemeInhouse" || appInfo.BundleId == "com.ss.iphone.ugc.awemeinhouse.lite" {
return s.LoginDouyin(appInfo.BundleId, phoneNumber, captcha, password)
} else if appInfo.BundleId == "com.ss.iphone.InHouse.article.Video" {
return s.LoginXigua(appInfo.BundleId, phoneNumber, captcha, password)
} else {
return info, fmt.Errorf("not support app")
}
}
func (s *StubIOSDriver) LoginXigua(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
loginSchema := ""
if captcha != "" {
loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=1&account=%s&smscode=%s", phoneNumber, captcha)
} else if password != "" {
loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=2&account=%s&password=%s", phoneNumber, password)
} else {
return info, fmt.Errorf("password and capcha is empty")
}
info.IsLogin = true
return info, s.OpenUrl(loginSchema)
}
func (s *StubIOSDriver) LoginDouyin(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
params := map[string]interface{}{
"phone": phoneNumber,
}
if captcha != "" {
params["captcha"] = captcha
} else if password != "" {
params["password"] = password
} else {
return info, fmt.Errorf("password and capcha is empty")
}
bsJSON, err := json.Marshal(params)
if err != nil {
return info, err
}
urlPrefix, err := s.getUrlPrefix(packageName)
if err != nil {
return info, err
}
fullUrl := urlPrefix + "/host/login/account/" + urlPrefix
resp, err := s.Session.POST(bsJSON, fullUrl)
if err != nil {
return info, err
}
res, err := resp.ValueConvertToJsonObject()
if err != nil {
return info, err
}
log.Info().Msgf("%v", res)
// {'isSuccess': True, 'data': '登录成功', 'code': 0}
if res["isSuccess"] != true {
err = fmt.Errorf("falied to logout %s", res["data"])
log.Err(err).Msgf("%v", res)
return info, err
}
time.Sleep(20 * time.Second)
info, err = s.getLoginAppInfo(packageName)
if err != nil || !info.IsLogin {
return info, fmt.Errorf("falied to login %v", info)
}
return info, nil
}
func (s *StubIOSDriver) LogoutNoneUI(packageName string) error {
urlPrefix, err := s.getUrlPrefix(packageName)
if err != nil {
return err
}
fullUrl := urlPrefix + "/host/loginout/"
resp, err := s.Session.GET(fullUrl)
if err != nil {
return err
}
res, err := resp.ValueConvertToJsonObject()
if err != nil {
return err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("falied to logout %s", res["data"])
log.Err(err).Msgf("%v", res)
return err
}
time.Sleep(10 * time.Second)
return nil
}
func (s *StubIOSDriver) EnableDevtool(packageName string, enable bool) (err error) {
urlPrefix, err := s.getUrlPrefix(packageName)
if err != nil {
return err
}
fullUrl := urlPrefix + "/host/devtool/enable"
params := map[string]interface{}{
"enable": enable,
}
bsJSON, err := json.Marshal(params)
if err != nil {
return err
}
resp, err := s.Session.POST(bsJSON, fullUrl)
if err != nil {
return err
}
res, err := resp.ValueConvertToJsonObject()
if err != nil {
return err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("falied to enable devtool %s", res["data"])
log.Err(err).Msgf("%v", res)
return err
}
return nil
}
func (s *StubIOSDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) {
urlPrefix, err := s.getUrlPrefix(packageName)
if err != nil {
return info, err
}
fullUrl := urlPrefix + "/host/app/info/"
resp, err := s.Session.GET(fullUrl)
if err != nil {
return info, err
}
res, err := resp.ValueConvertToJsonObject()
if err != nil {
return info, err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("falied to get is login %s", res["data"])
log.Err(err).Msgf("%v", res)
return info, err
}
err = json.Unmarshal([]byte(res["data"].(string)), &info)
if err != nil {
return info, err
}
return info, nil
}
func (s *StubIOSDriver) getUrlPrefix(packageName string) (urlPrefix string, err error) {
if packageName == "com.ss.iphone.ugc.AwemeInhouse" {
urlPrefix = s.douyinUrlPrefix
} else if packageName == "com.ss.iphone.ugc.awemeinhouse.lite" {
urlPrefix = s.douyinLiteUrlPrefix
} else {
return "", fmt.Errorf("not support app %s", packageName)
}
return urlPrefix, nil
}

View File

@ -6,11 +6,11 @@ import (
"strings"
"sync"
"github.com/httprunner/funplugin"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/funplugin"
"github.com/httprunner/funplugin/myexec"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/config"
"github.com/httprunner/httprunner/v5/internal/sdk"
@ -37,12 +37,12 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
Bool("logOn", logOn).Msg("init plugin")
// plugin file not found
if path == "" {
return nil, nil
return nil, errors.New("testcase path not specified")
}
pluginPath, err := locatePlugin(path)
pluginPath, err := LocatePlugin(path)
if err != nil {
log.Warn().Str("path", path).Msg("locate plugin failed")
return nil, nil
return nil, errors.Wrap(err, "locate plugin failed")
}
pluginMutex.Lock()
@ -100,21 +100,21 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
return
}
func locatePlugin(path string) (pluginPath string, err error) {
func LocatePlugin(path string) (pluginPath string, err error) {
log.Info().Str("path", path).Msg("locate plugin")
// priority: hashicorp plugin (debugtalk.bin > debugtalk.py) > go plugin (debugtalk.so)
pluginPath, err = locateFile(path, PluginHashicorpGoBuiltFile)
pluginPath, err = LocateFile(path, PluginHashicorpGoBuiltFile)
if err == nil {
return
}
pluginPath, err = locateFile(path, PluginPySourceFile)
pluginPath, err = LocateFile(path, PluginPySourceFile)
if err == nil {
return
}
pluginPath, err = locateFile(path, PluginGoBuiltFile)
pluginPath, err = LocateFile(path, PluginGoBuiltFile)
if err == nil {
return
}
@ -122,9 +122,9 @@ func locatePlugin(path string) (pluginPath string, err error) {
return "", errors.New("plugin file not found")
}
// locateFile searches destFile upward recursively until system root dir
// LocateFile searches destFile upward recursively until system root dir
// if not found, then searches in hrp executable dir
func locateFile(startPath string, destFile string) (pluginPath string, err error) {
func LocateFile(startPath string, destFile string) (pluginPath string, err error) {
stat, err := os.Stat(startPath)
if os.IsNotExist(err) {
return "", errors.Wrap(err, "start path not exists")
@ -153,7 +153,7 @@ func locateFile(startPath string, destFile string) (pluginPath string, err error
return "", errors.New("searched to system root dir, plugin file not found")
}
return locateFile(parentDir, destFile)
return LocateFile(parentDir, destFile)
}
// locateExecutable finds destFile in hrp executable dir
@ -173,13 +173,13 @@ func locateExecutable(destFile string) (string, error) {
}
func GetProjectRootDirPath(path string) (rootDir string, err error) {
pluginPath, err := locatePlugin(path)
pluginPath, err := LocatePlugin(path)
if err == nil {
rootDir = filepath.Dir(pluginPath)
return
}
// fix: no debugtalk file in project but having proj.json created by startproject
projPath, err := locateFile(path, projectInfoFile)
projPath, err := LocateFile(path, projectInfoFile)
if err == nil {
rootDir = filepath.Dir(projPath)
return
@ -188,5 +188,5 @@ func GetProjectRootDirPath(path string) (rootDir string, err error) {
// failed to locate project root dir
// maybe project plugin debugtalk.xx and proj.json are not exist
// use current dir instead
return config.RootDir, nil
return config.GetConfig().RootDir, nil
}

View File

@ -1,54 +0,0 @@
package hrp
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLocateFile(t *testing.T) {
// specify target file path
_, err := locateFile(tmpl("plugin/debugtalk.go"), PluginGoSourceFile)
if !assert.Nil(t, err) {
t.Fatal()
}
// specify path with the same dir
_, err = locateFile(tmpl("plugin/debugtalk.py"), PluginGoSourceFile)
if !assert.Nil(t, err) {
t.Fatal()
}
// specify target file path dir
_, err = locateFile(tmpl("plugin/"), PluginGoSourceFile)
if !assert.Nil(t, err) {
t.Fatal()
}
// specify wrong path
_, err = locateFile(".", PluginGoSourceFile)
if !assert.Error(t, err) {
t.Fatal()
}
_, err = locateFile("/abc", PluginGoSourceFile)
if !assert.Error(t, err) {
t.Fatal()
}
}
func TestLocatePythonPlugin(t *testing.T) {
_, err := locatePlugin(tmpl("plugin/debugtalk.py"))
if !assert.Nil(t, err) {
t.Fatal()
}
}
func TestLocateGoPlugin(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
_, err := locatePlugin(tmpl("debugtalk.bin"))
if !assert.Nil(t, err) {
t.Fatal()
}
}

View File

@ -17,17 +17,18 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/httprunner/funplugin"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"golang.org/x/net/http2"
"github.com/httprunner/funplugin"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
)
// Run starts to run testcase with default configs.
@ -281,7 +282,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
caseRunner := &CaseRunner{
TestCase: testcase,
hrpRunner: r,
parser: newParser(),
parser: NewParser(),
uixtDrivers: make(map[string]*uixt.XTDriver),
}
config := testcase.Config.Get()
@ -296,7 +297,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
// load plugin info to testcase config
pluginPath := plugin.Path()
pluginContent, err := readFile(pluginPath)
pluginContent, err := builtin.LoadFile(pluginPath)
if err != nil {
return nil, err
}
@ -407,7 +408,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
parsedConfig.WebSocketSetting.checkWebSocket()
// parse testcase config parameters
parametersIterator, err := r.parser.initParametersIterator(parsedConfig)
parametersIterator, err := r.parser.InitParametersIterator(parsedConfig)
if err != nil {
log.Error().Err(err).
Interface("parameters", parsedConfig.Parameters).

View File

@ -2,11 +2,13 @@ package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt"
)
func foregroundAppHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *Router) foregroundAppHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -18,13 +20,13 @@ func foregroundAppHandler(c *gin.Context) {
RenderSuccess(c, appInfo)
}
func appInfoHandler(c *gin.Context) {
func (r *Router) appInfoHandler(c *gin.Context) {
var appInfoReq AppInfoRequest
if err := c.ShouldBindQuery(&appInfoReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
device, err := GetDevice(c)
device, err := r.GetDevice(c)
if err != nil {
return
}
@ -47,18 +49,18 @@ func appInfoHandler(c *gin.Context) {
}
}
func clearAppHandler(c *gin.Context) {
func (r *Router) clearAppHandler(c *gin.Context) {
var appClearReq AppClearRequest
if err := c.ShouldBindJSON(&appClearReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.IDriver.(*uixt.ADBDriver).AppClear(appClearReq.PackageName)
err = driver.AppClear(appClearReq.PackageName)
if err != nil {
RenderError(c, err)
return
@ -66,13 +68,13 @@ func clearAppHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func launchAppHandler(c *gin.Context) {
func (r *Router) launchAppHandler(c *gin.Context) {
var appLaunchReq AppLaunchRequest
if err := c.ShouldBindJSON(&appLaunchReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -84,13 +86,13 @@ func launchAppHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func terminalAppHandler(c *gin.Context) {
func (r *Router) terminalAppHandler(c *gin.Context) {
var appTerminalReq AppTerminalRequest
if err := c.ShouldBindJSON(&appTerminalReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -102,20 +104,19 @@ func terminalAppHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func uninstallAppHandler(c *gin.Context) {
func (r *Router) uninstallAppHandler(c *gin.Context) {
var appUninstallReq AppUninstallRequest
if err := c.ShouldBindJSON(&appUninstallReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.GetDevice().Uninstall(appUninstallReq.PackageName)
if err != nil {
RenderError(c, err)
return
log.Err(err).Msg("failed to uninstall app")
}
RenderSuccess(c, true)
}

View File

@ -9,17 +9,17 @@ import (
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) {
deviceObj, exists := c.Get("device")
func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) {
var device uixt.IDevice
var driver uixt.IDriver
deviceObj, exists := c.Get("device")
if !exists {
device, err = GetDevice(c)
device, err = r.GetDevice(c)
if err != nil {
return nil, err
}
@ -32,14 +32,14 @@ func GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) {
RenderErrorInitDriver(c, err)
return
}
c.Set("driver", driver)
driverExt = uixt.NewXTDriver(driver,
ai.WithCVService(ai.CVServiceTypeVEDEM))
c.Set("driver", driverExt)
return driverExt, nil
}
func GetDevice(c *gin.Context) (device uixt.IDevice, err error) {
func (r *Router) GetDevice(c *gin.Context) (device uixt.IDevice, err error) {
platform := c.Param("platform")
serial := c.Param("serial")
if serial == "" {
@ -54,7 +54,6 @@ func GetDevice(c *gin.Context) (device uixt.IDevice, err error) {
RenderErrorInitDriver(c, err)
return
}
_ = device.Setup()
case "ios":
device, err = uixt.NewIOSDevice(
option.WithUDID(serial),
@ -66,10 +65,20 @@ func GetDevice(c *gin.Context) (device uixt.IDevice, err error) {
RenderErrorInitDriver(c, err)
return
}
case "browser":
device, err = uixt.NewBrowserDevice(option.WithBrowserID(serial))
if err != nil {
RenderErrorInitDriver(c, err)
return
}
default:
err = fmt.Errorf("[%s]: invalid platform", c.HandlerName())
return
}
err = device.Setup()
if err != nil {
log.Error().Err(err).Msg("setup device failed")
}
c.Set("device", device)
return device, nil
}
@ -87,7 +96,7 @@ func RenderError(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError,
HttpResponse{
Code: code.GetErrorCode(err),
Message: err.Error(),
Message: "grey " + err.Error(),
},
)
c.Abort()
@ -102,7 +111,7 @@ func RenderErrorInitDriver(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError,
HttpResponse{
Code: errCode,
Message: "init driver failed",
Message: "grey init driver failed",
},
)
c.Abort()
@ -112,7 +121,7 @@ func RenderErrorValidateRequest(c *gin.Context, err error) {
log.Error().Err(err).Msg("validate request failed")
c.JSON(http.StatusBadRequest, HttpResponse{
Code: code.GetErrorCode(code.InvalidParamError),
Message: fmt.Sprintf("validate request param failed: %s", err.Error()),
Message: fmt.Sprintf("grey validate request param failed: %s", err.Error()),
})
c.Abort()
}

View File

@ -7,14 +7,14 @@ import (
"github.com/Masterminds/semver"
"github.com/danielpaulus/go-ios/ios"
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/gadb"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/pkg/gadb"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func listDeviceHandler(c *gin.Context) {
func (r *Router) listDeviceHandler(c *gin.Context) {
var deviceList []interface{}
client, err := gadb.NewClient()
if err == nil {
@ -88,17 +88,47 @@ func listDeviceHandler(c *gin.Context) {
RenderSuccess(c, deviceList)
}
func pushImageHandler(c *gin.Context) {
func createBrowserHandler(c *gin.Context) {
var createBrowserReq CreateBrowserRequest
if err := c.ShouldBindJSON(&createBrowserReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
browserInfo, err := uixt.CreateBrowser(createBrowserReq.Timeout)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, browserInfo)
return
}
func (r *Router) deleteBrowserHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.DeleteSession()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) pushImageHandler(c *gin.Context) {
var pushMediaReq PushMediaRequest
if err := c.ShouldBindJSON(&pushMediaReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
imagePath, err := builtin.DownloadFileByUrl(pushMediaReq.ImageUrl)
imagePath, err := uixt.DownloadFileByUrl(pushMediaReq.ImageUrl)
if path.Ext(imagePath) == "" {
err = os.Rename(imagePath, imagePath+".png")
if err != nil {
@ -122,8 +152,8 @@ func pushImageHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func clearImageHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *Router) clearImageHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -135,6 +165,6 @@ func clearImageHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func videoHandler(c *gin.Context) {
func (r *Router) videoHandler(c *gin.Context) {
RenderSuccess(c, "")
}

View File

@ -6,18 +6,17 @@ import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/server"
"github.com/httprunner/httprunner/v5/uixt"
)
func installAppHandler(c *gin.Context) {
func (r *RouterExt) installAppHandler(c *gin.Context) {
var appInstallReq AppInstallRequest
if err := c.ShouldBindJSON(&appInstallReq); err != nil {
server.RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -32,7 +31,7 @@ func installAppHandler(c *gin.Context) {
server.RenderSuccess(c, true)
return
}
localMappingPath, err := builtin.DownloadFileByUrl(appInstallReq.MappingUrl)
localMappingPath, err := uixt.DownloadFileByUrl(appInstallReq.MappingUrl)
if err != nil {
server.RenderError(c, err)
}
@ -45,7 +44,7 @@ func installAppHandler(c *gin.Context) {
server.RenderError(c, err)
return
}
localResourceMappingPath, err := builtin.DownloadFileByUrl(
localResourceMappingPath, err := uixt.DownloadFileByUrl(
appInstallReq.ResourceMappingUrl)
if err != nil {
server.RenderError(c, err)

View File

@ -5,37 +5,40 @@ import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/ai"
"github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext"
"github.com/httprunner/httprunner/v5/server"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/driver_ext"
)
func GetDriver(c *gin.Context) (driverExt *driver_ext.XTDriver, err error) {
platform := c.Param("platform")
deviceObj, exists := c.Get("device")
func (r *RouterExt) GetDriver(c *gin.Context) (driverExt *driver_ext.StubXTDriver, err error) {
var device uixt.IDevice
var driver uixt.IDriver
var driver driver_ext.IStubDriver
deviceObj, exists := c.Get("device")
if !exists {
device, err = server.GetDevice(c)
device, err = r.GetDevice(c)
if err != nil {
return nil, err
}
} else {
device = deviceObj.(uixt.IDevice)
}
platform := c.Param("platform")
switch strings.ToLower(platform) {
case "android":
driver, err = driver_ext.NewStubAndroidDriver(device.(*uixt.AndroidDevice))
case "ios":
driver, err = driver_ext.NewStubIOSDriver(device.(*uixt.IOSDevice))
case "browser":
driver, err = driver_ext.NewStubBrowserDriver(device.(*uixt.BrowserDevice))
}
if err != nil {
server.RenderErrorInitDriver(c, err)
return
}
c.Set("driver", driver)
driverExt = driver_ext.NewXTDriver(driver,
driverExt = driver_ext.NewStubXTDriver(driver,
ai.WithCVService(ai.CVServiceTypeVEDEM))
c.Set("driver", driverExt)
return driverExt, nil
}

View File

@ -1,27 +1,28 @@
package server_ext
import (
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext"
"github.com/httprunner/httprunner/v5/server"
)
func loginHandler(c *gin.Context) {
func (r *RouterExt) loginHandler(c *gin.Context) {
var loginReq LoginRequest
if err := c.ShouldBindJSON(&loginReq); err != nil {
server.RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
info, err := driver.IDriver.(driver_ext.IStubDriver).
LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber,
loginReq.Captcha, loginReq.Password)
info, err := driver.LoginNoneUI(
loginReq.PackageName, loginReq.PhoneNumber,
loginReq.Captcha, loginReq.Password)
if err != nil {
server.RenderError(c, err)
return
@ -29,19 +30,18 @@ func loginHandler(c *gin.Context) {
server.RenderSuccess(c, info)
}
func logoutHandler(c *gin.Context) {
func (r *RouterExt) logoutHandler(c *gin.Context) {
var logoutReq LogoutRequest
if err := c.ShouldBindJSON(&logoutReq); err != nil {
server.RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.IDriver.(driver_ext.IStubDriver).
LogoutNoneUI(logoutReq.PackageName)
err = driver.LogoutNoneUI(logoutReq.PackageName)
if err != nil {
server.RenderError(c, err)
return
@ -49,8 +49,8 @@ func logoutHandler(c *gin.Context) {
server.RenderSuccess(c, true)
}
func sourceHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *RouterExt) sourceHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -58,5 +58,9 @@ func sourceHandler(c *gin.Context) {
if err != nil {
log.Warn().Err(err).Msg("get source failed")
}
if source == "{}" || source == "" {
time.Sleep(1 * time.Second)
source, err = driver.Source()
}
server.RenderSuccess(c, source)
}

View File

@ -4,13 +4,23 @@ import (
"github.com/httprunner/httprunner/v5/server"
)
func NewExtRouter() *server.Router {
router := server.NewRouter()
apiV1PlatformSerial := router.Group("/api/v1").Group("/:platform").Group("/:serial")
type RouterExt struct {
*server.Router
}
apiV1PlatformSerial.GET("/stub/source", sourceHandler)
apiV1PlatformSerial.POST("/stub/login", loginHandler)
apiV1PlatformSerial.POST("/stub/logout", logoutHandler)
apiV1PlatformSerial.POST("/app/install", installAppHandler)
func NewExtRouter() *RouterExt {
router := &RouterExt{
Router: server.NewRouter(),
}
router.Init()
return router
}
func (r *RouterExt) Init() {
apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial")
apiV1PlatformSerial.GET("/stub/source", r.sourceHandler)
apiV1PlatformSerial.POST("/stub/login", r.loginHandler)
apiV1PlatformSerial.POST("/stub/logout", r.logoutHandler)
apiV1PlatformSerial.POST("/app/install", r.installAppHandler)
}

View File

@ -9,7 +9,7 @@ type AppInstallRequest struct {
type LoginRequest struct {
PackageName string `json:"packageName"`
PhoneNumber string `json:"phoneNumber" binding:"required"`
PhoneNumber string `json:"phoneNumber"`
Captcha string `json:"captcha" binding:"required_without=Password"`
Password string `json:"password" binding:"required_without=Captcha"`
}
@ -19,7 +19,7 @@ type LogoutRequest struct {
}
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Code int `json:"errorCode"`
Message string `json:"errorMsg"`
Result interface{} `json:"result,omitempty"`
}

View File

@ -3,11 +3,11 @@ package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
)
func unlockHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *Router) unlockHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -19,8 +19,8 @@ func unlockHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func homeHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *Router) homeHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -32,7 +32,7 @@ func homeHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func backspaceHandler(c *gin.Context) {
func (r *Router) backspaceHandler(c *gin.Context) {
var deleteReq DeleteRequest
if err := c.ShouldBindJSON(&deleteReq); err != nil {
RenderErrorValidateRequest(c, err)
@ -41,7 +41,7 @@ func backspaceHandler(c *gin.Context) {
if deleteReq.Count == 0 {
deleteReq.Count = 20
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -53,13 +53,13 @@ func backspaceHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func keycodeHandler(c *gin.Context) {
func (r *Router) keycodeHandler(c *gin.Context) {
var keycodeReq KeycodeRequest
if err := c.ShouldBindJSON(&keycodeReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}

View File

@ -4,14 +4,16 @@ import (
"fmt"
"time"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
func NewRouter() *Router {
router := &Router{}
router := &Router{
Engine: gin.Default(),
}
router.Init()
return router
}
@ -21,46 +23,51 @@ type Router struct {
}
func (r *Router) Init() {
r.Engine = gin.Default()
r.Engine.Use(teardown())
r.Engine.GET("/ping", pingHandler)
r.Engine.GET("/", pingHandler)
r.Engine.POST("/", pingHandler)
r.Engine.GET("/api/v1/devices", listDeviceHandler)
r.Engine.Use(r.teardown())
r.Engine.GET("/ping", r.pingHandler)
r.Engine.GET("/", r.pingHandler)
r.Engine.POST("/", r.pingHandler)
r.Engine.GET("/api/v1/devices", r.listDeviceHandler)
r.Engine.POST("/api/v1/browser/create_browser", createBrowserHandler)
apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial")
// UI operations
apiV1PlatformSerial.POST("/ui/tap", tapHandler)
apiV1PlatformSerial.POST("/ui/double_tap", doubleTapHandler)
apiV1PlatformSerial.POST("/ui/drag", dragHandler)
apiV1PlatformSerial.POST("/ui/input", inputHandler)
apiV1PlatformSerial.POST("/ui/home", homeHandler)
apiV1PlatformSerial.POST("/ui/tap", r.tapHandler)
apiV1PlatformSerial.POST("/ui/right_click", r.rightClickHandler)
apiV1PlatformSerial.POST("/ui/double_tap", r.doubleTapHandler)
apiV1PlatformSerial.POST("/ui/drag", r.dragHandler)
apiV1PlatformSerial.POST("/ui/input", r.inputHandler)
apiV1PlatformSerial.POST("/ui/home", r.homeHandler)
apiV1PlatformSerial.POST("/ui/upload", r.uploadHandler)
apiV1PlatformSerial.POST("/ui/hover", r.hoverHandler)
apiV1PlatformSerial.POST("/ui/scroll", r.scrollHandler)
// Key operations
apiV1PlatformSerial.POST("/key/unlock", unlockHandler)
apiV1PlatformSerial.POST("/key/home", homeHandler)
apiV1PlatformSerial.POST("/key/backspace", backspaceHandler)
apiV1PlatformSerial.POST("/key", keycodeHandler)
apiV1PlatformSerial.POST("/key/unlock", r.unlockHandler)
apiV1PlatformSerial.POST("/key/home", r.homeHandler)
apiV1PlatformSerial.POST("/key/backspace", r.backspaceHandler)
apiV1PlatformSerial.POST("/key", r.keycodeHandler)
// APP operations
apiV1PlatformSerial.GET("/app/foreground", foregroundAppHandler)
apiV1PlatformSerial.GET("/app/appInfo", appInfoHandler)
apiV1PlatformSerial.POST("/app/clear", clearAppHandler)
apiV1PlatformSerial.POST("/app/launch", launchAppHandler)
apiV1PlatformSerial.POST("/app/terminal", terminalAppHandler)
apiV1PlatformSerial.POST("/app/uninstall", uninstallAppHandler)
apiV1PlatformSerial.GET("/app/foreground", r.foregroundAppHandler)
apiV1PlatformSerial.GET("/app/appInfo", r.appInfoHandler)
apiV1PlatformSerial.POST("/app/clear", r.clearAppHandler)
apiV1PlatformSerial.POST("/app/launch", r.launchAppHandler)
apiV1PlatformSerial.POST("/app/terminal", r.terminalAppHandler)
apiV1PlatformSerial.POST("/app/uninstall", r.uninstallAppHandler)
// Device operations
apiV1PlatformSerial.GET("/screenshot", screenshotHandler)
apiV1PlatformSerial.GET("/video", videoHandler)
apiV1PlatformSerial.POST("/device/push_image", pushImageHandler)
apiV1PlatformSerial.POST("/device/clear_image", clearImageHandler)
apiV1PlatformSerial.GET("/adb/source", adbSourceHandler)
apiV1PlatformSerial.GET("/screenshot", r.screenshotHandler)
apiV1PlatformSerial.DELETE("/close_browser", r.deleteBrowserHandler)
apiV1PlatformSerial.GET("/video", r.videoHandler)
apiV1PlatformSerial.POST("/device/push_image", r.pushImageHandler)
apiV1PlatformSerial.POST("/device/clear_image", r.clearImageHandler)
apiV1PlatformSerial.GET("/adb/source", r.adbSourceHandler)
// uixt operations
apiV1PlatformSerial.POST("/uixt/action", uixtActionHandler)
apiV1PlatformSerial.POST("/uixt/actions", uixtActionsHandler)
apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler)
apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler)
}
func (r *Router) Run(port int) error {
@ -72,11 +79,11 @@ func (r *Router) Run(port int) error {
return nil
}
func pingHandler(c *gin.Context) {
func (r *Router) pingHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func teardown() gin.HandlerFunc {
func (r *Router) teardown() gin.HandlerFunc {
return func(c *gin.Context) {
logID := c.Request.Header.Get("x-tt-logid")
startTime := time.Now()
@ -99,7 +106,7 @@ func teardown() gin.HandlerFunc {
deviceObj, exists := c.Get("device")
if exists {
if device, ok := deviceObj.(*uixt.IOSDevice); ok {
if device, ok := deviceObj.(uixt.IDevice); ok {
err := device.Teardown()
if err != nil {
log.Error().Err(err)

View File

@ -1,7 +1,7 @@
package server
import (
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
type TapRequest struct {
@ -11,6 +11,13 @@ type TapRequest struct {
Options *option.ActionOptions `json:"options,omitempty"`
}
type uploadRequest struct {
X float64 `json:"x"`
Y float64 `json:"y"`
FileUrl string `json:"file_url"`
FileFormat string `json:"file_format"`
}
type DragRequest struct {
FromX float64 `json:"from_x" binding:"required"`
FromY float64 `json:"from_y" binding:"required"`
@ -71,8 +78,8 @@ type OperateRequest struct {
}
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Code int `json:"errorCode"`
Message string `json:"errorMsg"`
Result interface{} `json:"result,omitempty"`
}

View File

@ -6,11 +6,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func screenshotHandler(c *gin.Context) {
driver, err := GetDriver(c)
func (r *Router) screenshotHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -23,8 +23,8 @@ func screenshotHandler(c *gin.Context) {
RenderSuccess(c, base64.StdEncoding.EncodeToString(raw.Bytes()))
}
func screenResultHandler(c *gin.Context) {
dExt, err := GetDriver(c)
func (r *Router) screenResultHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
@ -40,7 +40,7 @@ func screenResultHandler(c *gin.Context) {
actionOptions = screenReq.Options.Options()
}
screenResult, err := dExt.GetScreenResult(actionOptions...)
screenResult, err := driver.GetScreenResult(actionOptions...)
if err != nil {
log.Err(err).Msg("get screen result failed")
RenderError(c, err)
@ -49,8 +49,8 @@ func screenResultHandler(c *gin.Context) {
RenderSuccess(c, screenResult)
}
func adbSourceHandler(c *gin.Context) {
dExt, err := GetDriver(c)
func (r *Router) adbSourceHandler(c *gin.Context) {
dExt, err := r.GetDriver(c)
if err != nil {
return
}

View File

@ -2,25 +2,25 @@ package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func tapHandler(c *gin.Context) {
func (r *Router) tapHandler(c *gin.Context) {
var tapReq TapRequest
if err := c.ShouldBindJSON(&tapReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
if tapReq.Duration > 0 {
err = driver.Drag(tapReq.X, tapReq.Y, tapReq.X, tapReq.Y,
option.WithDuration(tapReq.Duration),
option.WithAbsoluteCoordinate(true))
option.WithDuration(tapReq.Duration))
} else {
err = driver.TapAbsXY(tapReq.X, tapReq.Y)
err = driver.TapXY(tapReq.X, tapReq.Y)
}
if err != nil {
RenderError(c, err)
@ -29,25 +29,106 @@ func tapHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func doubleTapHandler(c *gin.Context) {
func (r *Router) rightClickHandler(c *gin.Context) {
var rightClickReq TapRequest
if err := c.ShouldBindJSON(&rightClickReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
RightClick(rightClickReq.X, rightClickReq.Y)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) uploadHandler(c *gin.Context) {
var uploadRequest uploadRequest
if err := c.ShouldBindJSON(&uploadRequest); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
UploadFile(uploadRequest.X, uploadRequest.Y,
uploadRequest.FileUrl, uploadRequest.FileFormat)
if err != nil {
c.Abort()
return
}
RenderSuccess(c, true)
}
func (r *Router) hoverHandler(c *gin.Context) {
var hoverReq HoverRequest
if err := c.ShouldBindJSON(&hoverReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
Hover(hoverReq.X, hoverReq.Y)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) scrollHandler(c *gin.Context) {
var scrollReq ScrollRequest
if err := c.ShouldBindJSON(&scrollReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
Scroll(scrollReq.Delta)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) doubleTapHandler(c *gin.Context) {
var tapReq TapRequest
if err := c.ShouldBindJSON(&tapReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
if tapReq.X < 1 && tapReq.Y < 1 {
err = driver.DoubleTapXY(tapReq.X, tapReq.Y)
} else {
err = driver.DoubleTapXY(tapReq.X, tapReq.Y,
option.WithAbsoluteCoordinate(true))
}
err = driver.DoubleTap(tapReq.X, tapReq.Y)
if err != nil {
RenderError(c, err)
return
@ -55,7 +136,7 @@ func doubleTapHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func dragHandler(c *gin.Context) {
func (r *Router) dragHandler(c *gin.Context) {
var dragReq DragRequest
if err := c.ShouldBindJSON(&dragReq); err != nil {
RenderErrorValidateRequest(c, err)
@ -64,14 +145,14 @@ func dragHandler(c *gin.Context) {
if dragReq.Duration == 0 {
dragReq.Duration = 1
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Drag(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY,
option.WithDuration(dragReq.Duration), option.WithPressDuration(dragReq.PressDuration),
option.WithAbsoluteCoordinate(true))
option.WithDuration(dragReq.Duration),
option.WithPressDuration(dragReq.PressDuration))
if err != nil {
RenderError(c, err)
return
@ -79,13 +160,13 @@ func dragHandler(c *gin.Context) {
RenderSuccess(c, true)
}
func inputHandler(c *gin.Context) {
func (r *Router) inputHandler(c *gin.Context) {
var inputReq InputRequest
if err := c.ShouldBindJSON(&inputReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := GetDriver(c)
driver, err := r.GetDriver(c)
if err != nil {
return
}

73
server/ui_test.go Normal file
View File

@ -0,0 +1,73 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTapHandler(t *testing.T) {
router := NewRouter()
tests := []struct {
name string
path string
tapReq TapRequest
wantStatus int
wantResp HttpResponse
}{
{
name: "tap abs xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
tapReq: TapRequest{
X: 500,
Y: 800,
Duration: 0,
},
wantStatus: http.StatusOK,
wantResp: HttpResponse{
Code: 0,
Message: "success",
Result: true,
},
},
{
name: "tap relative xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
tapReq: TapRequest{
X: 0.5,
Y: 0.6,
Duration: 0,
},
wantStatus: http.StatusOK,
wantResp: HttpResponse{
Code: 0,
Message: "success",
Result: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reqBody, _ := json.Marshal(tt.tapReq)
req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
var got HttpResponse
err := json.Unmarshal(w.Body.Bytes(), &got)
assert.NoError(t, err)
assert.Equal(t, tt.wantResp, got)
})
}
}

View File

@ -2,13 +2,13 @@ package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/rs/zerolog/log"
)
// exec a single uixt action
func uixtActionHandler(c *gin.Context) {
dExt, err := GetDriver(c)
func (r *Router) uixtActionHandler(c *gin.Context) {
dExt, err := r.GetDriver(c)
if err != nil {
return
}
@ -29,8 +29,8 @@ func uixtActionHandler(c *gin.Context) {
}
// exec multiple uixt actions
func uixtActionsHandler(c *gin.Context) {
dExt, err := GetDriver(c)
func (r *Router) uixtActionsHandler(c *gin.Context) {
dExt, err := r.GetDriver(c)
if err != nil {
return
}

View File

@ -1,6 +1,6 @@
package hrp
import "github.com/httprunner/httprunner/v5/pkg/uixt"
import "github.com/httprunner/httprunner/v5/uixt"
type StepType string

View File

@ -5,9 +5,10 @@ import (
"os"
"time"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt"
)
// StepFunction implements IStep interface.

View File

@ -22,21 +22,21 @@ import (
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/httpstat"
"github.com/httprunner/httprunner/v5/internal/json"
"github.com/httprunner/httprunner/v5/pkg/httpstat"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/option"
)
type HTTPMethod string
const (
httpGET HTTPMethod = "GET"
httpHEAD HTTPMethod = "HEAD"
httpPOST HTTPMethod = "POST"
httpPUT HTTPMethod = "PUT"
httpDELETE HTTPMethod = "DELETE"
httpOPTIONS HTTPMethod = "OPTIONS"
httpPATCH HTTPMethod = "PATCH"
HTTP_GET HTTPMethod = "GET"
HTTP_HEAD HTTPMethod = "HEAD"
HTTP_POST HTTPMethod = "POST"
HTTP_PUT HTTPMethod = "PUT"
HTTP_DELETE HTTPMethod = "DELETE"
HTTP_OPTIONS HTTPMethod = "OPTIONS"
HTTP_PATCH HTTPMethod = "PATCH"
)
// Request represents HTTP request data structure.
@ -566,11 +566,11 @@ func (s *StepRequest) Loop(times int) *StepRequest {
// GET makes a HTTP GET request.
func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpGET
s.Request.Method = HTTP_GET
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpGET,
Method: HTTP_GET,
URL: url,
}
}
@ -582,11 +582,11 @@ func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs {
// HEAD makes a HTTP HEAD request.
func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpHEAD
s.Request.Method = HTTP_HEAD
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpHEAD,
Method: HTTP_HEAD,
URL: url,
}
}
@ -598,11 +598,11 @@ func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs {
// POST makes a HTTP POST request.
func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpPOST
s.Request.Method = HTTP_POST
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpPOST,
Method: HTTP_POST,
URL: url,
}
}
@ -614,11 +614,11 @@ func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs {
// PUT makes a HTTP PUT request.
func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpPUT
s.Request.Method = HTTP_PUT
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpPUT,
Method: HTTP_PUT,
URL: url,
}
}
@ -630,11 +630,11 @@ func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs {
// DELETE makes a HTTP DELETE request.
func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpDELETE
s.Request.Method = HTTP_DELETE
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpDELETE,
Method: HTTP_DELETE,
URL: url,
}
}
@ -646,11 +646,11 @@ func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs {
// OPTIONS makes a HTTP OPTIONS request.
func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpOPTIONS
s.Request.Method = HTTP_OPTIONS
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpOPTIONS,
Method: HTTP_OPTIONS,
URL: url,
}
}
@ -662,11 +662,11 @@ func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs {
// PATCH makes a HTTP PATCH request.
func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs {
if s.Request != nil {
s.Request.Method = httpPATCH
s.Request.Method = HTTP_PATCH
s.Request.URL = url
} else {
s.Request = &Request{
Method: httpPATCH,
Method: HTTP_PATCH,
URL: url,
}
}

View File

@ -1 +0,0 @@
package hrp

View File

@ -42,14 +42,14 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
cfg := r.caseRunner.Config.Get().ThinkTimeSetting
if cfg == nil {
cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
cfg = &ThinkTimeConfig{ThinkTimeDefault, nil, 0}
}
var tt time.Duration
switch cfg.Strategy {
case thinkTimeDefault:
case ThinkTimeDefault:
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
case thinkTimeRandomPercentage:
case ThinkTimeRandomPercentage:
// e.g. {"min_percentage": 0.5, "max_percentage": 1.5}
m, ok := cfg.Setting.(map[string]float64)
if !ok {
@ -58,13 +58,13 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
}
res := builtin.GetRandomNumber(int(thinkTime.Time*m["min_percentage"]*1000), int(thinkTime.Time*m["max_percentage"]*1000))
tt = time.Duration(res) * time.Millisecond
case thinkTimeMultiply:
case ThinkTimeMultiply:
value, ok := cfg.Setting.(float64) // e.g. 0.5
if !ok || value <= 0 {
value = thinkTimeDefaultMultiply
}
tt = time.Duration(thinkTime.Time*value*1000) * time.Millisecond
case thinkTimeIgnore:
case ThinkTimeIgnore:
// nothing to do
}

View File

@ -10,8 +10,8 @@ import (
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/sdk"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
type MobileUI struct {
@ -103,7 +103,9 @@ func (s *StepMobile) Home() *StepMobile {
return s
}
// TapXY taps the point {X,Y}, X & Y is percentage of coordinates
// TapXY taps the point {X,Y}
// if X<1 & Y<1, {X,Y} will be considered as percentage
// else, X & Y will be considered as absolute coordinates
func (s *StepMobile) TapXY(x, y float64, opts ...option.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_TapXY,

View File

@ -220,28 +220,130 @@ func (s *StepWebSocket) WithCloseStatus(closeStatus int64) *StepWebSocket {
}
// Validate switches to step validation.
func (s *StepWebSocket) Validate() *StepRequestValidation {
return &StepRequestValidation{
StepRequestWithOptionalArgs: &StepRequestWithOptionalArgs{
StepRequest: &StepRequest{
StepConfig: s.StepConfig,
},
},
func (s *StepWebSocket) Validate() *StepWebSocketValidation {
return &StepWebSocketValidation{
StepWebSocket: s,
}
}
// Extract switches to step extraction.
func (s *StepWebSocket) Extract() *StepRequestExtraction {
func (s *StepWebSocket) Extract() *StepWebSocketExtraction {
s.StepConfig.Extract = make(map[string]string)
return &StepRequestExtraction{
StepRequestWithOptionalArgs: &StepRequestWithOptionalArgs{
StepRequest: &StepRequest{
StepConfig: s.StepConfig,
},
},
return &StepWebSocketExtraction{
StepWebSocket: s,
}
}
// StepWebSocketExtraction implements IStep interface.
type StepWebSocketExtraction struct {
*StepWebSocket
}
// WithJmesPath sets the JMESPath expression to extract from the response.
func (s *StepWebSocketExtraction) WithJmesPath(jmesPath string, varName string) *StepWebSocketExtraction {
s.StepConfig.Extract[varName] = jmesPath
return s
}
// Validate switches to step validation.
func (s *StepWebSocketExtraction) Validate() *StepWebSocketValidation {
return &StepWebSocketValidation{
StepWebSocket: s.StepWebSocket,
}
}
func (s *StepWebSocketExtraction) Name() string {
return s.StepName
}
func (s *StepWebSocketExtraction) Type() StepType {
stepType := StepType(fmt.Sprintf("websocket-%v", s.WebSocket.Type))
return stepType + stepTypeSuffixExtraction
}
func (s *StepWebSocketExtraction) Struct() *StepConfig {
return &s.StepConfig
}
func (s *StepWebSocketExtraction) Run(r *SessionRunner) (*StepResult, error) {
if s.WebSocket != nil {
return runStepWebSocket(r, s.StepWebSocket)
}
return nil, errors.New("unexpected protocol type")
}
// StepWebSocketValidation implements IStep interface.
type StepWebSocketValidation struct {
*StepWebSocket
}
func (s *StepWebSocketValidation) Name() string {
if s.StepName != "" {
return s.StepName
}
return fmt.Sprintf("%s %s", s.WebSocket.Type, s.WebSocket.URL)
}
func (s *StepWebSocketValidation) Type() StepType {
stepType := StepType(fmt.Sprintf("websocket-%v", s.WebSocket.Type))
return stepType + stepTypeSuffixValidation
}
func (s *StepWebSocketValidation) Config() *StepConfig {
return &s.StepConfig
}
func (s *StepWebSocketValidation) Run(r *SessionRunner) (*StepResult, error) {
if s.WebSocket != nil {
return runStepWebSocket(r, s.StepWebSocket)
}
return nil, errors.New("unexpected protocol type")
}
func (s *StepWebSocketValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation {
v := Validator{
Check: jmesPath,
Assert: "equals",
Expect: expected,
Message: msg,
}
s.Validators = append(s.Validators, v)
return s
}
func (s *StepWebSocketValidation) AssertEqualFold(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation {
v := Validator{
Check: jmesPath,
Assert: "equal_fold",
Expect: expected,
Message: msg,
}
s.Validators = append(s.Validators, v)
return s
}
func (s *StepWebSocketValidation) AssertLengthEqual(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation {
v := Validator{
Check: jmesPath,
Assert: "length_equals",
Expect: expected,
Message: msg,
}
s.Validators = append(s.Validators, v)
return s
}
func (s *StepWebSocketValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation {
v := Validator{
Check: jmesPath,
Assert: "contains",
Expect: expected,
Message: msg,
}
s.Validators = append(s.Validators, v)
return s
}
type WebSocketAction struct {
Type WSActionType `json:"type" yaml:"type"`
URL string `json:"url" yaml:"url"`

View File

@ -15,7 +15,7 @@ import (
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/config"
"github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/pkg/uixt"
"github.com/httprunner/httprunner/v5/uixt"
)
func NewSummary() *Summary {
@ -32,7 +32,7 @@ func NewSummary() *Summary {
},
},
Time: &TestCaseTime{
StartAt: config.StartTime,
StartAt: config.GetConfig().StartTime,
},
Platform: platForm,
}
@ -67,7 +67,7 @@ func (s *Summary) AddCaseSummary(caseSummary *TestCaseSummary) {
s.rootDir = caseSummary.RootDir
} else if s.rootDir != caseSummary.RootDir {
// if multiple testcases have different root path, use current working dir
s.rootDir = config.RootDir
s.rootDir = config.GetConfig().RootDir
}
// merge action stats
@ -80,7 +80,7 @@ func (s *Summary) AddCaseSummary(caseSummary *TestCaseSummary) {
}
func (s *Summary) SetupDirPath() (path string, err error) {
dirPath := filepath.Join(s.rootDir, config.ResultsDir)
dirPath := filepath.Join(s.rootDir, config.GetConfig().ResultsDir)
err = builtin.EnsureFolderExists(dirPath)
if err != nil {
return "", err

32
tests/build_test.go Normal file
View File

@ -0,0 +1,32 @@
package tests
import (
"path/filepath"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/internal/builtin"
)
func TestRun(t *testing.T) {
err := hrp.BuildPlugin(tmpl("plugin/debugtalk.go"), "./debugtalk.bin")
assert.Nil(t, err)
genDebugTalkPyPath := filepath.Join(tmpl("plugin/"), hrp.PluginPySourceGenFile)
err = hrp.BuildPlugin(tmpl("plugin/debugtalk.py"), genDebugTalkPyPath)
assert.Nil(t, err)
contentBytes, err := builtin.LoadFile(genDebugTalkPyPath)
assert.Nil(t, err)
content := string(contentBytes)
assert.Contains(t, content, "import funppy")
assert.Contains(t, content, "funppy.register")
reg, _ := regexp.Compile(`funppy\.register`)
matchedSlice := reg.FindAllStringSubmatch(content, -1)
assert.Len(t, matchedSlice, 10)
}

65
tests/loader_test.go Normal file
View File

@ -0,0 +1,65 @@
package tests
import (
"testing"
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
)
func TestLoadTestCases(t *testing.T) {
// load test cases from folder path
tc := hrp.TestCasePath("../examples/demo-with-py-plugin/testcases/")
testCases, err := hrp.LoadTestCases(&tc)
assert.Nil(t, err)
assert.Equal(t, 3, len(testCases))
// load test cases from folder path, including sub folders
tc = hrp.TestCasePath("../examples/demo-with-py-plugin/")
testCases, err = hrp.LoadTestCases(&tc)
assert.Nil(t, err)
assert.Equal(t, 3, len(testCases))
// load test cases from single file path
tc = hrp.TestCasePath(demoTestCaseWithPluginJSONPath)
testCases, err = hrp.LoadTestCases(&tc)
assert.Nil(t, err)
assert.Equal(t, 1, len(testCases))
// load test cases from TestCase instance
testcase := &hrp.TestCase{
Config: hrp.NewConfig("TestCase").SetWeight(3),
}
testCases, err = hrp.LoadTestCases(testcase)
assert.Nil(t, err)
assert.Equal(t, len(testCases), 1)
// load test cases from TestCaseJSON
testcaseJSON := hrp.TestCaseJSON(`
{
"config":{"name":"TestCaseJSON"},
"teststeps":[
{"name": "step1", "request":{"url": "https://httpbin.org/get"}},
{"name": "step2", "shell":{"string": "ls -l"}}
]
}`)
testCases, err = hrp.LoadTestCases(&testcaseJSON)
assert.Nil(t, err)
assert.Equal(t, len(testCases), 1)
}
func TestLoadCase(t *testing.T) {
tcJSON := &hrp.TestCaseDef{}
tcYAML := &hrp.TestCaseDef{}
err := hrp.LoadFileObject(demoTestCaseWithPluginJSONPath, tcJSON)
assert.Nil(t, err)
err = hrp.LoadFileObject(demoTestCaseWithPluginYAMLPath, tcYAML)
assert.Nil(t, err)
assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name)
assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL)
assert.Equal(t, tcJSON.Steps[1].StepName, tcYAML.Steps[1].StepName)
assert.Equal(t, tcJSON.Steps[1].Request, tcJSON.Steps[1].Request)
}

View File

@ -1,22 +1,24 @@
package hrp
package tests
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
)
func TestLoadParameters(t *testing.T) {
testData := []struct {
configParameters map[string]interface{}
loadedParameters map[string]Parameters
loadedParameters map[string]hrp.Parameters
}{
{
map[string]interface{}{
"username-password": fmt.Sprintf("${parameterize(%s/$file)}", hrpExamplesDir),
},
map[string]Parameters{
map[string]hrp.Parameters{
"username-password": {
{"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"},
@ -33,7 +35,7 @@ func TestLoadParameters(t *testing.T) {
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
"app_version": []interface{}{4.0},
},
map[string]Parameters{
map[string]hrp.Parameters{
"username-password": {
{"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"},
@ -54,7 +56,7 @@ func TestLoadParameters(t *testing.T) {
[]interface{}{"test2", "222222"},
},
},
map[string]Parameters{
map[string]hrp.Parameters{
"username-password": {
{"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"},
@ -74,15 +76,11 @@ func TestLoadParameters(t *testing.T) {
variablesMapping := map[string]interface{}{
"file": "account.csv",
}
parser := newParser()
parser := hrp.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()
}
value, err := parser.LoadParameters(data.configParameters, variablesMapping)
assert.Nil(t, err)
assert.Equal(t, data.loadedParameters, value)
}
}
@ -109,12 +107,10 @@ func TestLoadParametersError(t *testing.T) {
},
},
}
parser := newParser()
parser := hrp.NewParser()
for _, data := range testData {
_, err := parser.loadParameters(data.configParameters, map[string]interface{}{})
if !assert.Error(t, err) {
t.Fatal()
}
_, err := parser.LoadParameters(data.configParameters, map[string]interface{}{})
assert.Error(t, err)
}
}
@ -125,28 +121,28 @@ func TestInitParametersIteratorCount(t *testing.T) {
"app_version": []interface{}{4.0}, // 1
}
testData := []struct {
cfg *TConfig
cfg *hrp.TConfig
expectLimit int
}{
// default, no parameters setting
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{},
ParametersSetting: &hrp.TParamsConfig{},
},
6, // 3 * 2 * 1
},
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
},
6, // 3 * 2 * 1
},
// default equals to set overall parameters pick-order to "sequential"
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
ParametersSetting: &hrp.TParamsConfig{
PickOrder: "sequential",
},
},
@ -154,10 +150,10 @@ func TestInitParametersIteratorCount(t *testing.T) {
},
// default equals to set each individual parameters pick-order to "sequential"
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{
ParametersSetting: &hrp.TParamsConfig{
Strategies: map[string]hrp.IteratorStrategy{
"username-password": {Name: "user-info", PickOrder: "sequential"},
"user_agent": {Name: "user-identity", PickOrder: "sequential"},
"app_version": {Name: "app-version", PickOrder: "sequential"},
@ -167,10 +163,10 @@ func TestInitParametersIteratorCount(t *testing.T) {
6, // 3 * 2 * 1
},
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{
ParametersSetting: &hrp.TParamsConfig{
Strategies: map[string]hrp.IteratorStrategy{
"user_agent": {Name: "user-identity", PickOrder: "sequential"},
"app_version": {Name: "app-version", PickOrder: "sequential"},
},
@ -182,9 +178,9 @@ func TestInitParametersIteratorCount(t *testing.T) {
// set overall parameters overall pick-order to "random"
// each random parameters only select one item
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
ParametersSetting: &hrp.TParamsConfig{
PickOrder: "random",
},
},
@ -193,10 +189,10 @@ func TestInitParametersIteratorCount(t *testing.T) {
// set some individual parameters pick-order to "random"
// this will override overall strategy
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{
ParametersSetting: &hrp.TParamsConfig{
Strategies: map[string]hrp.IteratorStrategy{
"user_agent": {Name: "user-identity", PickOrder: "random"},
},
},
@ -204,10 +200,10 @@ func TestInitParametersIteratorCount(t *testing.T) {
3, // 3 * 1 * 1
},
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{
ParametersSetting: &hrp.TParamsConfig{
Strategies: map[string]hrp.IteratorStrategy{
"username-password": {Name: "user-info", PickOrder: "random"},
},
},
@ -217,18 +213,18 @@ func TestInitParametersIteratorCount(t *testing.T) {
// set limit for parameters
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters, // total: 6 = 3 * 2 * 1
ParametersSetting: &TParamsConfig{
ParametersSetting: &hrp.TParamsConfig{
Limit: 4, // limit could be less than total
},
},
4,
},
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters, // total: 6 = 3 * 2 * 1
ParametersSetting: &TParamsConfig{
ParametersSetting: &hrp.TParamsConfig{
Limit: 9, // limit could also be greater than total
},
},
@ -238,33 +234,25 @@ func TestInitParametersIteratorCount(t *testing.T) {
// no parameters
// also will generate one empty item
{
&TConfig{
&hrp.TConfig{
Parameters: nil,
ParametersSetting: nil,
},
1,
},
}
parser := newParser()
parser := hrp.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()
}
iterator, err := parser.InitParametersIterator(data.cfg)
assert.Nil(t, err)
assert.Equal(t, data.expectLimit, iterator.Limit)
for i := 0; i < data.expectLimit; i++ {
if !assert.True(t, iterator.HasNext()) {
t.Fatal()
}
assert.True(t, iterator.HasNext())
iterator.Next() // consume next parameters
}
// should not have next
if !assert.False(t, iterator.HasNext()) {
t.Fatal()
}
assert.False(t, iterator.HasNext())
}
}
@ -275,50 +263,40 @@ func TestInitParametersIteratorUnlimitedCount(t *testing.T) {
"app_version": []interface{}{4.0}, // 1
}
testData := []struct {
cfg *TConfig
cfg *hrp.TConfig
}{
// default, no parameters setting
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
ParametersSetting: &TParamsConfig{},
ParametersSetting: &hrp.TParamsConfig{},
},
},
// no parameters
// also will generate one empty item
{
&TConfig{
&hrp.TConfig{
Parameters: nil,
ParametersSetting: nil,
},
},
}
parser := newParser()
parser := hrp.NewParser()
for _, data := range testData {
iterator, err := parser.initParametersIterator(data.cfg)
if !assert.Nil(t, err) {
t.Fatal()
}
iterator, err := parser.InitParametersIterator(data.cfg)
assert.Nil(t, err)
// set unlimited mode
iterator.SetUnlimitedMode()
if !assert.Equal(t, -1, iterator.limit) {
t.Fatal()
}
assert.Equal(t, -1, iterator.Limit)
for i := 0; i < 100; i++ {
if !assert.True(t, iterator.HasNext()) {
t.Fatal()
}
assert.True(t, iterator.HasNext())
iterator.Next() // consume next parameters
}
if !assert.Equal(t, 100, iterator.index) {
t.Fatal()
}
assert.Equal(t, 100, iterator.Index)
// should also have next
if !assert.True(t, iterator.HasNext()) {
t.Fatal()
}
assert.True(t, iterator.HasNext())
}
}
@ -329,13 +307,13 @@ func TestInitParametersIteratorContent(t *testing.T) {
"app_version": []interface{}{4.0}, // 1
}
testData := []struct {
cfg *TConfig
cfg *hrp.TConfig
checkIndex int
expectParameters map[string]interface{}
}{
// default, no parameters setting
{
&TConfig{
&hrp.TConfig{
Parameters: configParameters,
},
0, // check first item
@ -346,16 +324,16 @@ func TestInitParametersIteratorContent(t *testing.T) {
// set limit for parameters
{
&TConfig{
&hrp.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{
ParametersSetting: &hrp.TParamsConfig{
Limit: 5, // limit could also be greater than total
Strategies: map[string]iteratorStrategy{
Strategies: map[string]hrp.IteratorStrategy{
"username-password": {Name: "user-info", PickOrder: "random"},
},
},
@ -369,7 +347,7 @@ func TestInitParametersIteratorContent(t *testing.T) {
// no parameters
// also will generate one empty item
{
&TConfig{
&hrp.TConfig{
Parameters: nil,
ParametersSetting: nil,
},
@ -377,35 +355,29 @@ func TestInitParametersIteratorContent(t *testing.T) {
map[string]interface{}{},
},
}
parser := newParser()
parser := hrp.NewParser()
for _, data := range testData {
iterator, err := parser.initParametersIterator(data.cfg)
if !assert.Nil(t, err) {
t.Fatal()
}
iterator, err := parser.InitParametersIterator(data.cfg)
assert.Nil(t, err)
// get expected parameters item
for i := 0; i < data.checkIndex; i++ {
if !assert.True(t, iterator.HasNext()) {
t.Fatal()
}
assert.True(t, iterator.HasNext())
iterator.Next() // consume next parameters
}
parametersItem := iterator.Next()
if !assert.Equal(t, data.expectParameters, parametersItem) {
t.Fatal()
}
assert.Equal(t, data.expectParameters, parametersItem)
}
}
func TestGenCartesianProduct(t *testing.T) {
testData := []struct {
multiParameters []Parameters
expect Parameters
multiParameters []hrp.Parameters
expect hrp.Parameters
}{
{
[]Parameters{
[]hrp.Parameters{
{
{"app_version": 4.0},
},
@ -418,7 +390,7 @@ func TestGenCartesianProduct(t *testing.T) {
{"user_agent": "iOS/10.2"},
},
},
Parameters{
hrp.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"},
@ -430,16 +402,14 @@ func TestGenCartesianProduct(t *testing.T) {
nil,
},
{
[]Parameters{},
[]hrp.Parameters{},
nil,
},
}
for _, data := range testData {
parameters := genCartesianProduct(data.multiParameters)
if !assert.Equal(t, data.expect, parameters) {
t.Fatal()
}
parameters := hrp.GenCartesianProduct(data.multiParameters)
assert.Equal(t, data.expect, parameters)
}
}
@ -490,13 +460,9 @@ func TestConvertParameters(t *testing.T) {
}
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()
}
value, err := hrp.ConvertParameters(data.key, data.parametersRawList)
assert.Nil(t, err)
assert.Equal(t, data.expect, value)
}
}
@ -530,9 +496,7 @@ func TestConvertParametersError(t *testing.T) {
}
for _, data := range testData {
_, err := convertParameters(data.key, data.parametersRawList)
if !assert.Error(t, err) {
t.Fatal()
}
_, err := hrp.ConvertParameters(data.key, data.parametersRawList)
assert.Error(t, err)
}
}

42
tests/plugin_test.go Normal file
View File

@ -0,0 +1,42 @@
package tests
import (
"testing"
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
)
func TestLocateFile(t *testing.T) {
// specify target file path
_, err := hrp.LocateFile(tmpl("plugin/debugtalk.go"), hrp.PluginGoSourceFile)
assert.Nil(t, err)
// specify path with the same dir
_, err = hrp.LocateFile(tmpl("plugin/debugtalk.py"), hrp.PluginGoSourceFile)
assert.Nil(t, err)
// specify target file path dir
_, err = hrp.LocateFile(tmpl("plugin/"), hrp.PluginGoSourceFile)
assert.Nil(t, err)
// specify wrong path
_, err = hrp.LocateFile(".", hrp.PluginGoSourceFile)
assert.Error(t, err)
_, err = hrp.LocateFile("/abc", hrp.PluginGoSourceFile)
assert.Error(t, err)
}
func TestLocatePythonPlugin(t *testing.T) {
_, err := hrp.LocatePlugin(tmpl("plugin/debugtalk.py"))
assert.Nil(t, err)
}
func TestLocateGoPlugin(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
_, err := hrp.LocatePlugin(tmpl("debugtalk.bin"))
assert.Nil(t, err)
}

View File

@ -1,22 +1,22 @@
package hrp
package tests
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/code"
)
func buildHashicorpGoPlugin() {
log.Info().Msg("[init] build hashicorp go plugin")
err := BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin"))
err := hrp.BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin"))
if err != nil {
log.Error().Err(err).Msg("build hashicorp go plugin failed")
os.Exit(code.GetErrorCode(err))
@ -26,8 +26,6 @@ func buildHashicorpGoPlugin() {
func removeHashicorpGoPlugin() {
log.Info().Msg("[teardown] remove hashicorp go plugin")
os.Remove(tmpl("debugtalk.bin"))
pluginPath, _ := filepath.Abs(tmpl("debugtalk.bin"))
pluginMap.Delete(pluginPath)
}
func buildHashicorpPyPlugin() {
@ -43,8 +41,8 @@ func buildHashicorpPyPlugin() {
func removeHashicorpPyPlugin() {
log.Info().Msg("[teardown] remove hashicorp python plugin")
// on v4.1^, running case will generate .debugtalk_gen.py used by python plugin
os.Remove(tmpl(PluginPySourceFile))
os.Remove(tmpl(PluginPySourceGenFile))
os.Remove(tmpl(hrp.PluginPySourceFile))
os.Remove(tmpl(hrp.PluginPySourceGenFile))
}
func TestRunCaseWithGoPlugin(t *testing.T) {
@ -62,21 +60,22 @@ func TestRunCaseWithPythonPlugin(t *testing.T) {
}
func assertRunTestCases(t *testing.T) {
refCase := TestCasePath(demoTestCaseWithPluginJSONPath)
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("testcase1-step1").
refCase := hrp.TestCasePath(demoTestCaseWithPluginJSONPath)
testcase1 := &hrp.TestCase{
Config: hrp.NewConfig("TestCase1").
SetBaseURL("https://postman-echo.com").
EnablePlugin(), // TODO: FIXME
TestSteps: []hrp.IStep{
hrp.NewStep("testcase1-step1").
GET("/headers").
Validate().
AssertEqual("status_code", 200, "check status code").
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check http response Content-Type"),
NewStep("testcase1-step2").CallRefCase(
&TestCase{
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("ip").
hrp.NewStep("testcase1-step2").CallRefCase(
&hrp.TestCase{
Config: hrp.NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"),
TestSteps: []hrp.IStep{
hrp.NewStep("ip").
GET("/ip").
Validate().
AssertEqual("status_code", 200, "check status code").
@ -84,14 +83,14 @@ func assertRunTestCases(t *testing.T) {
},
},
),
NewStep("testcase1-step3").CallRefCase(&refCase),
hrp.NewStep("testcase1-step3").CallRefCase(&refCase),
},
}
testcase2 := &TestCase{
Config: NewConfig("TestCase2").SetWeight(3),
testcase2 := &hrp.TestCase{
Config: hrp.NewConfig("TestCase2").SetWeight(3),
}
r := NewRunner(t)
r := hrp.NewRunner(t)
r.SetPluginLogOn()
err := r.Run(testcase1, testcase2)
if err != nil {
@ -103,46 +102,46 @@ func TestRunCaseWithThinkTime(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
testcases := []*TestCase{
testcases := []*hrp.TestCase{
{
Config: NewConfig("TestCase1"),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(2),
Config: hrp.NewConfig("TestCase1"),
TestSteps: []hrp.IStep{
hrp.NewStep("thinkTime").SetThinkTime(2),
},
},
{
Config: NewConfig("TestCase2").
SetThinkTime(thinkTimeIgnore, nil, 0),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(0.5),
Config: hrp.NewConfig("TestCase2").
SetThinkTime(hrp.ThinkTimeIgnore, nil, 0),
TestSteps: []hrp.IStep{
hrp.NewStep("thinkTime").SetThinkTime(0.5),
},
},
{
Config: NewConfig("TestCase3").
SetThinkTime(thinkTimeRandomPercentage, nil, 0),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(1),
Config: hrp.NewConfig("TestCase3").
SetThinkTime(hrp.ThinkTimeRandomPercentage, nil, 0),
TestSteps: []hrp.IStep{
hrp.NewStep("thinkTime").SetThinkTime(1),
},
},
{
Config: NewConfig("TestCase4").
SetThinkTime(thinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(1),
Config: hrp.NewConfig("TestCase4").
SetThinkTime(hrp.ThinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5),
TestSteps: []hrp.IStep{
hrp.NewStep("thinkTime").SetThinkTime(1),
},
},
{
Config: NewConfig("TestCase5"),
TestSteps: []IStep{
Config: hrp.NewConfig("TestCase5"),
TestSteps: []hrp.IStep{
// think time: 3s, random pct: {"min_percentage":1, "max_percentage":1.5}, limit: 4s
NewStep("thinkTime").CallRefCase(&demoTestCaseWithThinkTimePath),
hrp.NewStep("thinkTime").CallRefCase(&demoTestCaseWithThinkTimePath),
},
},
}
expectedMinValue := []float64{2, 0, 0.5, 2, 3}
expectedMaxValue := []float64{2.5, 0.5, 2, 3, 10}
for idx, testcase := range testcases {
r := NewRunner(t)
r := hrp.NewRunner(t)
startTime := time.Now()
err := r.Run(testcase)
if err != nil {
@ -158,20 +157,20 @@ func TestRunCaseWithThinkTime(t *testing.T) {
}
func TestRunCaseWithShell(t *testing.T) {
testcase1 := &TestCase{
Config: NewConfig("complex shell with env variables").
testcase1 := &hrp.TestCase{
Config: hrp.NewConfig("complex shell with env variables").
WithVariables(map[string]interface{}{
"SS": "12345",
"ABC": "$SS",
}),
TestSteps: []IStep{
NewStep("shell21").Shell("echo hello world"),
TestSteps: []hrp.IStep{
hrp.NewStep("shell21").Shell("echo hello world"),
// NewStep("shell21").Shell("echo $ABC"),
// NewStep("shell21").Shell("which hrp"),
},
}
r := NewRunner(t)
r := hrp.NewRunner(t)
err := r.Run(testcase1)
if err != nil {
t.Fatal()
@ -193,16 +192,16 @@ func TestRunCaseWithFunction(t *testing.T) {
err3 = errors.New("func3 error")
fmt.Println("call function3 with return value and error")
}
testcase1 := &TestCase{
Config: NewConfig("call function"),
TestSteps: []IStep{
NewStep("fn1").Function(fn1),
NewStep("fn2").Function(fn2),
NewStep("fn3").Function(fn3),
testcase1 := &hrp.TestCase{
Config: hrp.NewConfig("call function"),
TestSteps: []hrp.IStep{
hrp.NewStep("fn1").Function(fn1),
hrp.NewStep("fn2").Function(fn2),
hrp.NewStep("fn3").Function(fn3),
},
}
r := NewRunner(t)
r := hrp.NewRunner(t)
err := r.Run(testcase1)
if err != nil {
t.Fatal()
@ -220,8 +219,9 @@ func TestRunCaseWithPluginJSON(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
testCase := TestCasePath(demoTestCaseWithPluginJSONPath)
err := NewRunner(nil).Run(&testCase) // hrp.Run(testCase)
testCase := hrp.TestCasePath(demoTestCaseWithPluginJSONPath)
// TODO: FIXME, enable plugin
err := hrp.NewRunner(nil).Run(&testCase) // hrp.Run(testCase)
if err != nil {
t.Fatal()
}
@ -243,22 +243,22 @@ func TestRunCaseWithRefAPI(t *testing.T) {
buildHashicorpGoPlugin()
defer removeHashicorpGoPlugin()
testCase := TestCasePath(demoTestCaseWithRefAPIPath)
err := NewRunner(nil).Run(&testCase)
testCase := hrp.TestCasePath(demoTestCaseWithRefAPIPath)
err := hrp.NewRunner(nil).Run(&testCase)
if err != nil {
t.Fatal()
}
refAPI := APIPath(demoAPIGETPath)
testcase := &TestCase{
Config: NewConfig("TestCase").
refAPI := hrp.APIPath(demoAPIGETPath)
testcase := &hrp.TestCase{
Config: hrp.NewConfig("TestCase").
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("run referenced api").CallRefAPI(&refAPI),
TestSteps: []hrp.IStep{
hrp.NewStep("run referenced api").CallRefAPI(&refAPI),
},
}
r := NewRunner(t)
r := hrp.NewRunner(t)
err = r.Run(testcase)
if err != nil {
t.Fatal()
@ -266,15 +266,15 @@ func TestRunCaseWithRefAPI(t *testing.T) {
}
func TestSessionRunner(t *testing.T) {
testcase := TestCase{
Config: NewConfig("TestCase").
testcase := hrp.TestCase{
Config: hrp.NewConfig("TestCase").
WithVariables(map[string]interface{}{
"a": 12.3,
"b": 3.45,
"varFoo": "${max($a, $b)}",
}),
TestSteps: []IStep{
NewStep("check variables").
TestSteps: []hrp.IStep{
hrp.NewStep("check variables").
WithVariables(map[string]interface{}{
"a": 12.3,
"b": 34.5,
@ -287,7 +287,7 @@ func TestSessionRunner(t *testing.T) {
},
}
caseRunner, _ := NewRunner(t).NewCaseRunner(testcase)
caseRunner, _ := hrp.NewRunner(t).NewCaseRunner(testcase)
sessionRunner := caseRunner.NewSession()
step := testcase.TestSteps[0]
if !assert.Equal(t, step.Config().Variables["varFoo"], "${max($a, $b)}") {

View File

@ -1,40 +1,42 @@
package hrp
package tests
import (
"math"
"testing"
hrp "github.com/httprunner/httprunner/v5"
)
func TestRunCaseWithRendezvous(t *testing.T) {
rendezvousBoundaryTestcase := &TestCase{
Config: NewConfig("run request with functions").
rendezvousBoundaryTestcase := &hrp.TestCase{
Config: hrp.NewConfig("run request with functions").
SetBaseURL("https://postman-echo.com").
WithVariables(map[string]interface{}{
"n": 5,
"a": 12.3,
"b": 3.45,
}),
TestSteps: []IStep{
NewStep("test negative number").
TestSteps: []hrp.IStep{
hrp.NewStep("test negative number").
SetRendezvous("test negative number").
WithUserNumber(-1),
NewStep("test overflow number").
hrp.NewStep("test overflow number").
SetRendezvous("test overflow number").
WithUserNumber(1000000),
NewStep("test negative percent").
hrp.NewStep("test negative percent").
SetRendezvous("test very low percent").
WithUserPercent(-0.5),
NewStep("test very low percent").
hrp.NewStep("test very low percent").
SetRendezvous("test very low percent").
WithUserPercent(0.00001),
NewStep("test overflow percent").
hrp.NewStep("test overflow percent").
SetRendezvous("test overflow percent").
WithUserPercent(1.5),
NewStep("test conflict params").
hrp.NewStep("test conflict params").
SetRendezvous("test conflict params").
WithUserNumber(1).
WithUserPercent(0.123),
NewStep("test negative timeout").
hrp.NewStep("test negative timeout").
SetRendezvous("test negative timeout").
WithTimeout(-1000),
},
@ -55,7 +57,7 @@ func TestRunCaseWithRendezvous(t *testing.T) {
{number: 100, percent: 1, timeout: 5000},
}
rendezvousList := InitRendezvous(rendezvousBoundaryTestcase, 100)
rendezvousList := hrp.InitRendezvous(rendezvousBoundaryTestcase, 100)
for i, r := range rendezvousList {
if r.Number != expectedRendezvousParams[i].number {

View File

@ -1,17 +1,15 @@
package hrp
package tests
import (
"io"
"net/http"
"strings"
"testing"
"time"
hrp "github.com/httprunner/httprunner/v5"
"github.com/stretchr/testify/assert"
)
var (
stepGET = NewStep("get with params").
stepGET = hrp.NewStep("get with params").
GET("/get").
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
@ -21,7 +19,7 @@ var (
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
AssertEqual("body.args.foo1", "bar1", "check param foo1").
AssertEqual("body.args.foo2", "bar2", "check param foo2")
stepPOSTData = NewStep("post form data").
stepPOSTData = hrp.NewStep("post form data").
POST("/post").
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
@ -33,7 +31,7 @@ var (
func TestRunRequestGetToStruct(t *testing.T) {
tStep := stepGET
if tStep.Request.Method != httpGET {
if tStep.Request.Method != hrp.HTTP_GET {
t.Fatalf("tStep.Request.Method != GET")
}
if tStep.Request.URL != "/get" {
@ -48,7 +46,7 @@ func TestRunRequestGetToStruct(t *testing.T) {
if tStep.Request.Cookies["user"] != "debugtalk" {
t.Fatalf("tStep.Request.Cookies mismatch")
}
validator, ok := tStep.Validators[0].(Validator)
validator, ok := tStep.Validators[0].(hrp.Validator)
if !ok || validator.Check != "status_code" || validator.Expect != 200 {
t.Fatalf("tStep.Validators mismatch")
}
@ -56,7 +54,7 @@ func TestRunRequestGetToStruct(t *testing.T) {
func TestRunRequestPostDataToStruct(t *testing.T) {
tStep := stepPOSTData
if tStep.Request.Method != httpPOST {
if tStep.Request.Method != hrp.HTTP_POST {
t.Fatalf("tStep.Request.Method != POST")
}
if tStep.Request.URL != "/post" {
@ -74,18 +72,18 @@ func TestRunRequestPostDataToStruct(t *testing.T) {
if tStep.Request.Body != "a=1&b=2" {
t.Fatalf("tStep.Request.Data mismatch")
}
validator, ok := tStep.Validators[0].(Validator)
validator, ok := tStep.Validators[0].(hrp.Validator)
if !ok || validator.Check != "status_code" || validator.Expect != 200 {
t.Fatalf("tStep.Validators mismatch")
}
}
func TestRunRequestStatOn(t *testing.T) {
testcase := TestCase{
Config: NewConfig("test").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{stepGET, stepPOSTData},
testcase := hrp.TestCase{
Config: hrp.NewConfig("test").SetBaseURL("https://postman-echo.com"),
TestSteps: []hrp.IStep{stepGET, stepPOSTData},
}
caseRunner, _ := NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase)
caseRunner, _ := hrp.NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase)
sessionRunner := caseRunner.NewSession()
summary, err := sessionRunner.Start(nil)
if err != nil {
@ -162,15 +160,15 @@ func TestRunRequestStatOn(t *testing.T) {
}
func TestRunCaseWithTimeout(t *testing.T) {
r := NewRunner(t)
r := hrp.NewRunner(t)
// global timeout
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
testcase1 := &hrp.TestCase{
Config: hrp.NewConfig("TestCase1").
SetRequestTimeout(10). // set global timeout to 10s
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("step1").
TestSteps: []hrp.IStep{
hrp.NewStep("step1").
GET("/delay/1").
Validate().
AssertEqual("status_code", 200, "check status code"),
@ -181,12 +179,12 @@ func TestRunCaseWithTimeout(t *testing.T) {
t.FailNow()
}
testcase2 := &TestCase{
Config: NewConfig("TestCase2").
testcase2 := &hrp.TestCase{
Config: hrp.NewConfig("TestCase2").
SetRequestTimeout(5). // set global timeout to 10s
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("step1").
TestSteps: []hrp.IStep{
hrp.NewStep("step1").
GET("/delay/10").
Validate().
AssertEqual("status_code", 200, "check status code"),
@ -198,12 +196,12 @@ func TestRunCaseWithTimeout(t *testing.T) {
}
// step timeout
testcase3 := &TestCase{
Config: NewConfig("TestCase3").
testcase3 := &hrp.TestCase{
Config: hrp.NewConfig("TestCase3").
SetRequestTimeout(10).
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("step2").
TestSteps: []hrp.IStep{
hrp.NewStep("step2").
GET("/delay/11").
SetTimeout(15*time.Second). // set step timeout to 4s
Validate().
@ -215,70 +213,3 @@ func TestRunCaseWithTimeout(t *testing.T) {
t.FailNow()
}
}
func TestSearchJmespath(t *testing.T) {
testText := `{"a": {"b": "foo"}, "c": "bar", "d": {"e": [{"f": "foo"}, {"f": "bar"}]}}`
testData := []struct {
raw string
expected string
}{
{"body.a.b", "foo"},
{"body.c", "bar"},
{"body.d.e[0].f", "foo"},
{"body.d.e[1].f", "bar"},
}
resp := http.Response{}
resp.Body = io.NopCloser(strings.NewReader(testText))
respObj, err := newHttpResponseObject(t, newParser(), &resp)
if err != nil {
t.Fatal()
}
for _, data := range testData {
if !assert.Equal(t, data.expected, respObj.searchJmespath(data.raw)) {
t.Fatal()
}
}
}
func TestSearchRegexp(t *testing.T) {
testText := `
<ul class="nav navbar-nav navbar-right">
<li><a href="/order/addToCart" style="color: white"><i class="fa fa-shopping-cart fa-2x"></i><span class="badge">0</span></a></li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#" style="color: white">
Leo <i class="fa fa-cog fa-2x"></i><span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/user/changePassword">Change Password</a></li>
<li><a href="/user/addAddress">Shipping</a></li>
<li><a href="/user/addCard">Payment</a></li>
<li><a href="/order/orderHistory">Order History</a></li>
<li><a href="/user/signOut">Sign Out</a></li>
</ul>
</li>
<li>&nbsp;&nbsp;&nbsp;</li>
<li><a href="/user/signOut" style="color: white"><i class="fa fa-sign-out fa-2x"></i>
Sign Out</a></li>
</ul>
`
testData := []struct {
raw string
expected string
}{
{"/user/signOut\">(.*)</a></li>", "Sign Out"},
{"<li><a href=\"/user/(.*)\" style", "signOut"},
{" (.*) <i class=\"fa fa-cog fa-2x\"></i>", "Leo"},
}
// new response object
resp := http.Response{}
resp.Body = io.NopCloser(strings.NewReader(testText))
respObj, err := newHttpResponseObject(t, newParser(), &resp)
if err != nil {
t.Fatal()
}
for _, data := range testData {
if !assert.Equal(t, data.expected, respObj.searchRegexp(data.raw)) {
t.Fatal()
}
}
}

View File

@ -1,87 +1,88 @@
//go:build localtest
package hrp
package tests
import (
"testing"
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func TestIOSSettingsAction(t *testing.T) {
testCase := &TestCase{
Config: NewConfig("ios ui action on Settings").
testCase := &hrp.TestCase{
Config: hrp.NewConfig("ios ui action on Settings").
SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)),
TestSteps: []IStep{
NewStep("launch Settings").
TestSteps: []hrp.IStep{
hrp.NewStep("launch Settings").
IOS().Home().TapByOCR("设置").
Validate().
AssertNameExists("飞行模式").
AssertLabelExists("蓝牙").
AssertOCRExists("个人热点"),
NewStep("swipe up and down").
hrp.NewStep("swipe up and down").
IOS().SwipeUp().SwipeUp().SwipeDown(),
},
}
err := NewRunner(t).Run(testCase)
err := hrp.NewRunner(t).Run(testCase)
if err != nil {
t.Fatal(err)
}
}
func TestIOSSearchApp(t *testing.T) {
testCase := &TestCase{
Config: NewConfig("ios ui action on Search App 资源库"),
TestSteps: []IStep{
NewStep("进入 App 资源库 搜索框").
testCase := &hrp.TestCase{
Config: hrp.NewConfig("ios ui action on Search App 资源库"),
TestSteps: []hrp.IStep{
hrp.NewStep("进入 App 资源库 搜索框").
IOS().Home().SwipeLeft().SwipeLeft().TapByCV("dewey-search-field").
Validate().
AssertLabelExists("取消"),
NewStep("搜索抖音").
hrp.NewStep("搜索抖音").
IOS().Input("抖音\n"),
},
}
err := NewRunner(t).Run(testCase)
err := hrp.NewRunner(t).Run(testCase)
if err != nil {
t.Fatal(err)
}
}
func TestIOSAppLaunch(t *testing.T) {
testCase := &TestCase{
Config: NewConfig("启动 & 关闭 App").
testCase := &hrp.TestCase{
Config: hrp.NewConfig("启动 & 关闭 App").
SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)),
TestSteps: []IStep{
NewStep("终止今日头条").
TestSteps: []hrp.IStep{
hrp.NewStep("终止今日头条").
IOS().AppTerminate("com.ss.iphone.article.News"),
NewStep("启动今日头条").
hrp.NewStep("启动今日头条").
IOS().AppLaunch("com.ss.iphone.article.News"),
NewStep("终止今日头条").
hrp.NewStep("终止今日头条").
IOS().AppTerminate("com.ss.iphone.article.News"),
NewStep("启动今日头条").
hrp.NewStep("启动今日头条").
IOS().AppLaunch("com.ss.iphone.article.News"),
},
}
err := NewRunner(t).Run(testCase)
err := hrp.NewRunner(t).Run(testCase)
if err != nil {
t.Fatal(err)
}
}
func TestAndroidAction(t *testing.T) {
testCase := &TestCase{
Config: NewConfig("android ui action"),
TestSteps: []IStep{
NewStep("launch douyin").
testCase := &hrp.TestCase{
Config: hrp.NewConfig("android ui action"),
TestSteps: []hrp.IStep{
hrp.NewStep("launch douyin").
Android().Serial("xxx").TapByOCR("抖音").
Validate().
AssertNameExists("首页", "首页 tab 不存在").
AssertNameExists("消息", "消息 tab 不存在"),
NewStep("swipe up and down").
hrp.NewStep("swipe up and down").
Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(),
},
}
err := NewRunner(t).Run(testCase)
err := hrp.NewRunner(t).Run(testCase)
if err != nil {
t.Fatal(err)
}

Some files were not shown because too many files have changed in this diff Show More