mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-24 01:40:02 +08:00
Merge branch 'master' into auto-reset-session
This commit is contained in:
@@ -93,17 +93,14 @@ func (b *HRPBoomer) SetPython3Venv(venv string) *HRPBoomer {
|
||||
|
||||
// Run starts to run load test for one or multiple testcases.
|
||||
func (b *HRPBoomer) Run(testcases ...ITestCase) {
|
||||
event := sdk.EventTracking{
|
||||
Category: "RunLoadTests",
|
||||
Action: "hrp boom",
|
||||
}
|
||||
// report start event
|
||||
go sdk.SendEvent(event)
|
||||
// report execution timing event
|
||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
||||
|
||||
// quit all plugins
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
// report boom event
|
||||
sdk.SendGA4Event("hrp_boomer_run", map[string]interface{}{
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
|
||||
// quit all plugins
|
||||
pluginMap.Range(func(key, value interface{}) bool {
|
||||
if plugin, ok := value.(funplugin.IPlugin); ok {
|
||||
plugin.Quit()
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").SetBaseURL("https://httpbin.org"),
|
||||
Config: NewConfig("TestCase1").SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("headers").
|
||||
GET("/headers").
|
||||
|
||||
13
hrp/build.go
13
hrp/build.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
@@ -172,6 +173,12 @@ func (pt *pluginTemplate) generateGo(output string) error {
|
||||
// buildGo builds debugtalk.go to debugtalk.bin
|
||||
func buildGo(path string, output string) error {
|
||||
log.Info().Str("path", path).Str("output", output).Msg("start to build go plugin")
|
||||
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{
|
||||
"pluginType": "go",
|
||||
})
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to read file")
|
||||
@@ -197,6 +204,12 @@ func buildGo(path string, output string) error {
|
||||
// buildPy completes funppy information in debugtalk.py
|
||||
func buildPy(path string, output string) error {
|
||||
log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin")
|
||||
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{
|
||||
"pluginType": "python",
|
||||
})
|
||||
|
||||
// check the syntax of debugtalk.py
|
||||
err := myexec.ExecPython3Command("py_compile", path)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
@@ -18,7 +21,16 @@ func format(data map[string]string) string {
|
||||
var listAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "List all Android devices",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
deviceList, err := uixt.GetAndroidDevices(serial)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
@@ -3,16 +3,28 @@ package adb
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var screencapAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "screencap",
|
||||
Short: "Start android screen capture",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_adb_screencap", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(serial)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer"
|
||||
)
|
||||
|
||||
@@ -29,7 +30,16 @@ var boomCmd = &cobra.Command{
|
||||
}
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_boom", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
var paths []hrp.ITestCase
|
||||
for _, arg := range args {
|
||||
path := hrp.TestCasePath(arg)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var buildCmd = &cobra.Command{
|
||||
@@ -16,7 +20,15 @@ var buildCmd = &cobra.Command{
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_build", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
return hrp.BuildPlugin(args[0], output)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
|
||||
)
|
||||
|
||||
@@ -19,7 +22,16 @@ var listAppsCmd = &cobra.Command{
|
||||
Use: "apps",
|
||||
Short: "List all iOS installed apps",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_apps", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
@@ -69,7 +72,16 @@ var listDevicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "List all iOS devices",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_devices", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
devices, err := uixt.GetIOSDevices(udid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
@@ -5,18 +5,29 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
// mountCmd represents the mount command
|
||||
var mountCmd = &cobra.Command{
|
||||
Use: "mount",
|
||||
Short: "A brief description of your command",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_mount", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package ios
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -11,13 +12,23 @@ import (
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var pcapCmd = &cobra.Command{
|
||||
Use: "pcap",
|
||||
Short: "capture ios network packets",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_pcap", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
pcapOptions := []uixt.IOSPcapOption{}
|
||||
if pid > 0 {
|
||||
pcapOptions = append(pcapOptions, uixt.WithIOSPcapPID(pid))
|
||||
|
||||
@@ -3,6 +3,7 @@ package ios
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -11,13 +12,23 @@ import (
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var perfCmd = &cobra.Command{
|
||||
Use: "perf",
|
||||
Short: "capture ios performance data (cpu,mem,disk,net,fps,etc.)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_perf", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
perfOptions := []uixt.IOSPerfOption{}
|
||||
for _, p := range indicators {
|
||||
switch p {
|
||||
|
||||
@@ -2,17 +2,29 @@ package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var psCmd = &cobra.Command{
|
||||
Use: "ps",
|
||||
Short: "show running processes",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_ps", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,15 +2,28 @@ package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var rebootCmd = &cobra.Command{
|
||||
Use: "reboot",
|
||||
Short: "reboot or shutdown ios device",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_reboot", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
device, err := getDevice(udid)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,17 +4,30 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
var xctestCmd = &cobra.Command{
|
||||
Use: "xctest",
|
||||
Short: "run xctest",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_ios_xctest", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
if bundleID == "" {
|
||||
return fmt.Errorf("bundleID is required")
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/pytest"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
@@ -19,11 +22,20 @@ var pytestCmd = &cobra.Command{
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
DisableFlagParsing: true, // allow to pass any args to pytest
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_pytest", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
packages := []string{
|
||||
fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion),
|
||||
}
|
||||
_, err := myexec.EnsurePython3Venv(venv, packages...)
|
||||
_, err = myexec.EnsurePython3Venv(venv, packages...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("python3 venv is not ready")
|
||||
return err
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/wiki"
|
||||
)
|
||||
|
||||
@@ -13,7 +17,15 @@ var wikiCmd = &cobra.Command{
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_wiki", map[string]interface{}{
|
||||
"args": strings.Join(args, "-"),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
return wiki.OpenWiki()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -86,23 +86,29 @@ func EndsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...int
|
||||
func EqualLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
|
||||
return assert.Len(t, actual, length, msgAndArgs...)
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l != length {
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect == %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GreaterThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l <= length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect > %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -110,14 +116,14 @@ func GreaterThanLength(t assert.TestingT, actual, expected interface{}, msgAndAr
|
||||
func GreaterOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l < length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect >= %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -125,14 +131,14 @@ func GreaterOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgA
|
||||
func LessThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l >= length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect < %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -140,14 +146,14 @@ func LessThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs
|
||||
func LessOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("expect int type, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("actual value %v(%T) can't get length", actual, actual), msgAndArgs...)
|
||||
}
|
||||
if l > length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
return assert.Fail(t, fmt.Sprintf("%v length == %d, expect <= %d", actual, l, length), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,15 +2,9 @@ package pytest
|
||||
|
||||
import (
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
func RunPytest(args []string) error {
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "RunAPITests",
|
||||
Action: "hrp pytest",
|
||||
})
|
||||
|
||||
args = append([]string{"run"}, args...)
|
||||
return myexec.ExecPython3Command("httprunner", args...)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ func TestGenDemoExamples(t *testing.T) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// FIXME
|
||||
dir = "../../../examples/demo-with-py-plugin"
|
||||
venv := filepath.Join(dir, ".venv")
|
||||
err = CreateScaffold(dir, Py, venv, true)
|
||||
|
||||
@@ -54,11 +54,15 @@ func CopyFile(templateFile, targetFile string) error {
|
||||
}
|
||||
|
||||
func CreateScaffold(projectName string, pluginType PluginType, venv string, force bool) error {
|
||||
// report event
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "Scaffold",
|
||||
Action: "hrp startproject",
|
||||
})
|
||||
// report GA event
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_startproject", map[string]interface{}{
|
||||
"pluginType": string(pluginType),
|
||||
"force": force,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
log.Info().
|
||||
Str("projectName", projectName).
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/post",
|
||||
"expect": "https://postman-echo.com/post/",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -26,5 +26,5 @@ validate:
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/post
|
||||
expect: https://postman-echo.com/post/
|
||||
msg: assert response body url
|
||||
@@ -38,7 +38,7 @@
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/put",
|
||||
"expect": "https://postman-echo.com/put/",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -26,5 +26,5 @@ validate:
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/put
|
||||
expect: https://postman-echo.com/put/
|
||||
msg: assert response body url
|
||||
@@ -1,4 +1,4 @@
|
||||
// NOTE: Generated By hrp v4.3.4, DO NOT EDIT!
|
||||
// NOTE: Generated By hrp v4.3.5, DO NOT EDIT!
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
gaAPIDebugURL = "https://www.google-analytics.com/debug/collect" // used for debug
|
||||
gaAPIURL = "https://www.google-analytics.com/collect"
|
||||
)
|
||||
|
||||
type GAClient struct {
|
||||
TrackingID string `form:"tid"` // Tracking ID / Property ID, XX-XXXXXXX-X
|
||||
ClientID string `form:"cid"` // Anonymous Client ID
|
||||
Version string `form:"v"` // Version
|
||||
httpClient *http.Client // http client session
|
||||
}
|
||||
|
||||
// NewGAClient creates a new GAClient object with the trackingID and clientID.
|
||||
func NewGAClient(trackingID, clientID string) *GAClient {
|
||||
return &GAClient{
|
||||
TrackingID: trackingID,
|
||||
ClientID: clientID,
|
||||
Version: "1", // constant v1
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GAClient) SendEvent(e IEvent) error {
|
||||
var data url.Values
|
||||
if event, ok := e.(UserTimingTracking); ok {
|
||||
event.duration = time.Since(event.startTime)
|
||||
data = event.ToUrlValues()
|
||||
} else {
|
||||
data = e.ToUrlValues()
|
||||
}
|
||||
|
||||
// append common params
|
||||
data.Add("v", g.Version)
|
||||
data.Add("tid", g.TrackingID)
|
||||
data.Add("cid", g.ClientID)
|
||||
|
||||
resp, err := g.httpClient.PostForm(gaAPIURL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("response status: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToUrlValues(i interface{}) (values url.Values) {
|
||||
values = url.Values{}
|
||||
iVal := reflect.ValueOf(i)
|
||||
for i := 0; i < iVal.NumField(); i++ {
|
||||
formTagName := iVal.Type().Field(i).Tag.Get("form")
|
||||
if formTagName == "" {
|
||||
continue
|
||||
}
|
||||
if iVal.Field(i).IsZero() {
|
||||
continue
|
||||
}
|
||||
values.Set(formTagName, fmt.Sprint(iVal.Field(i)))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendEvents(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "SendEvents",
|
||||
Value: 123,
|
||||
}
|
||||
err := SendEvent(event)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructToUrlValues(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "convert",
|
||||
Label: "v0.3.0",
|
||||
Value: 123,
|
||||
}
|
||||
val := structToUrlValues(event)
|
||||
if val.Encode() != "ea=convert&ec=unittest&el=v0.3.0&ev=123" {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
type IEvent interface {
|
||||
ToUrlValues() url.Values
|
||||
}
|
||||
|
||||
type EventTracking struct {
|
||||
HitType string `form:"t"` // Event hit type = event
|
||||
Category string `form:"ec"` // Required. Event Category.
|
||||
Action string `form:"ea"` // Required. Event Action.
|
||||
Label string `form:"el"` // Optional. Event label, used as version.
|
||||
Value int `form:"ev"` // Optional. Event value, must be non-negative integer
|
||||
}
|
||||
|
||||
func (e EventTracking) StartTiming(variable string) UserTimingTracking {
|
||||
return UserTimingTracking{
|
||||
HitType: "timing",
|
||||
Category: e.Category,
|
||||
Variable: variable,
|
||||
Label: e.Label,
|
||||
startTime: time.Now(), // starts the timer
|
||||
}
|
||||
}
|
||||
|
||||
func (e EventTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "event"
|
||||
e.Label = version.VERSION
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type UserTimingTracking struct {
|
||||
HitType string `form:"t"` // Timing hit type
|
||||
Category string `form:"utc"` // Required. user timing category. e.g. jsonLoader
|
||||
Variable string `form:"utv"` // Required. timing variable. e.g. load
|
||||
Duration string `form:"utt"` // Required. time took duration.
|
||||
Label string `form:"utl"` // Optional. user timing label. e.g jQuery
|
||||
startTime time.Time
|
||||
duration time.Duration // time took duration
|
||||
}
|
||||
|
||||
func (e UserTimingTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "timing"
|
||||
e.Label = version.VERSION
|
||||
e.Duration = fmt.Sprintf("%d", int64(e.duration.Seconds()*1000))
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type Exception struct {
|
||||
HitType string `form:"t"` // Hit Type = exception
|
||||
Description string `form:"exd"` // exception description. i.e. IOException
|
||||
IsFatal string `form:"exf"` // if the exception was fatal
|
||||
isFatal bool
|
||||
}
|
||||
|
||||
func (e Exception) ToUrlValues() url.Values {
|
||||
e.HitType = "exception"
|
||||
if e.isFatal {
|
||||
e.IsFatal = "1"
|
||||
} else {
|
||||
e.IsFatal = "0"
|
||||
}
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
211
hrp/internal/sdk/ga4.go
Normal file
211
hrp/internal/sdk/ga4.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
// Measurement Protocol (Google Analytics 4) docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4
|
||||
// debugging tools: https://ga-dev-tools.google/ga4/event-builder/
|
||||
const (
|
||||
ga4APISecret = "w7lKNQIrQsKNS4ikgMPp0Q"
|
||||
ga4MeasurementID = "G-9KHR3VC2LN"
|
||||
)
|
||||
|
||||
var (
|
||||
ga4Client *GA4Client
|
||||
userID string
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
userID, err = machineid.ProtectedID("hrp")
|
||||
if err != nil {
|
||||
userID = uuid.NewV1().String()
|
||||
}
|
||||
|
||||
// init GA4 client
|
||||
ga4Client = NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
}
|
||||
|
||||
type GA4Client struct {
|
||||
apiSecret string // Measurement Protocol API secret value
|
||||
measurementID string // MEASUREMENT ID, G-XXXXXXXXXX
|
||||
userID string // A unique identifier for a user
|
||||
httpClient *http.Client // http client session
|
||||
debug bool // send events for validation, used for debug
|
||||
}
|
||||
|
||||
// NewGA4Client creates a new GA4Client object with the measurementID and apiSecret.
|
||||
func NewGA4Client(measurementID, apiSecret string, debug ...bool) *GA4Client {
|
||||
dbg := false
|
||||
if len(debug) > 0 {
|
||||
dbg = debug[0]
|
||||
}
|
||||
|
||||
return &GA4Client{
|
||||
measurementID: measurementID,
|
||||
apiSecret: apiSecret,
|
||||
userID: userID,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
debug: dbg,
|
||||
}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
// Required. The name for the event.
|
||||
Name string `json:"name"`
|
||||
// Optional. The parameters for the event.
|
||||
// engagement_time_msec/session_id
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// payload docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag
|
||||
type Payload struct {
|
||||
// Required. Uniquely identifies a user instance of a web client
|
||||
ClientID string `json:"client_id"`
|
||||
// Optional. A unique identifier for a user
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
// Optional. A Unix timestamp (in microseconds) for the time to associate with the event.
|
||||
// This should only be set to record events that happened in the past.
|
||||
// This value can be overridden via user_property or event timestamps.
|
||||
// Events can be backdated up to 3 calendar days based on the property's timezone.
|
||||
TimestampMicros int64 `json:"timestamp_micros,omitempty"`
|
||||
// Optional. The user properties for the measurement.
|
||||
UserProperties map[string]string `json:"user_properties,omitempty"`
|
||||
// Optional. Set to true to indicate these events should not be used for personalized ads.
|
||||
NonPersonalizedAds bool `json:"non_personalized_ads,omitempty"`
|
||||
// Required. An array of event items. Up to 25 events can be sent per request.
|
||||
Events []Event `json:"events"`
|
||||
}
|
||||
|
||||
// validation docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag
|
||||
type ValidationResponse struct {
|
||||
ValidationMessages []ValidationMessage `json:"validationMessages"` // An array of validation messages.
|
||||
}
|
||||
|
||||
type ValidationMessage struct {
|
||||
FieldPath string `json:"fieldPath"` // The path to the field that was invalid.
|
||||
Description string `json:"description"` // A description of the error.
|
||||
ValidationCode ValidationCode `json:"validationCode"` // A ValidationCode that corresponds to the error.
|
||||
}
|
||||
|
||||
type ValidationCode string
|
||||
|
||||
const (
|
||||
VALUE_INVALID ValidationCode = "VALUE_INVALID" // The value provided for a fieldPath was invalid.
|
||||
VALUE_REQUIRED ValidationCode = "VALUE_REQUIRED" // A required value for a fieldPath was not provided.
|
||||
NAME_INVALID ValidationCode = "NAME_INVALID" // The name provided was invalid.
|
||||
NAME_RESERVED ValidationCode = "NAME_RESERVED" // The name provided was one of the reserved names.
|
||||
VALUE_OUT_OF_BOUNDS ValidationCode = "VALUE_OUT_OF_BOUNDS" // The value provided was too large.
|
||||
EXCEEDED_MAX_ENTITIES ValidationCode = "EXCEEDED_MAX_ENTITIES" // There were too many parameters in the request.
|
||||
NAME_DUPLICATED ValidationCode = "NAME_DUPLICATED" // The same name was provided more than once in the request.
|
||||
)
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GA4Client) SendEvent(event Event) error {
|
||||
query := url.Values{}
|
||||
query.Add("api_secret", g.apiSecret)
|
||||
query.Add("measurement_id", g.measurementID)
|
||||
|
||||
var uri string
|
||||
if g.debug {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/debug/mp/collect?%s", query.Encode())
|
||||
} else {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/mp/collect?%s", query.Encode())
|
||||
}
|
||||
|
||||
// append event params
|
||||
if event.Params == nil {
|
||||
event.Params = map[string]interface{}{}
|
||||
}
|
||||
event.Params["os"] = runtime.GOOS
|
||||
event.Params["arch"] = runtime.GOARCH
|
||||
event.Params["go_version"] = runtime.Version()
|
||||
event.Params["hrp_version"] = version.VERSION
|
||||
|
||||
payload := Payload{
|
||||
ClientID: fmt.Sprintf("%d.%d", rand.Int31(), time.Now().Unix()),
|
||||
UserID: g.userID,
|
||||
TimestampMicros: time.Now().UnixMicro(),
|
||||
Events: []Event{event},
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(payload)
|
||||
if g.debug {
|
||||
log.Debug().
|
||||
Str("uri", uri).
|
||||
Interface("payload", payload).
|
||||
Msg("send GA4 event")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshal GA4 request payload failed")
|
||||
}
|
||||
|
||||
body := bytes.NewReader(bs)
|
||||
res, err := g.httpClient.Post(uri, "application/json", body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "request GA4 failed")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return fmt.Errorf("validation response got unexpected status %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if !g.debug {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read GA4 response body failed")
|
||||
}
|
||||
|
||||
validationResponse := ValidationResponse{}
|
||||
err = json.Unmarshal(bs, &validationResponse)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshal GA4 response body failed")
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("statusCode", res.StatusCode).
|
||||
Interface("validationResponse", validationResponse).
|
||||
Msg("get GA4 validation response")
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendGA4Event(name string, params map[string]interface{}) {
|
||||
if env.DISABLE_GA == "true" {
|
||||
// do not send GA4 events in CI environment
|
||||
return
|
||||
}
|
||||
|
||||
event := Event{
|
||||
Name: name,
|
||||
Params: params,
|
||||
}
|
||||
err := ga4Client.SendEvent(event)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("send GA4 event failed")
|
||||
}
|
||||
}
|
||||
15
hrp/internal/sdk/ga4_test.go
Normal file
15
hrp/internal/sdk/ga4_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGA4(t *testing.T) {
|
||||
ga4Client := NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
|
||||
event := Event{
|
||||
Name: "hrp_debug_event",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
ga4Client.SendEvent(event)
|
||||
}
|
||||
@@ -3,35 +3,23 @@ package sdk
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
trackingID = "UA-114587036-1" // Tracking ID for Google Analytics
|
||||
sentryDSN = "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292"
|
||||
sentryDSN = "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292"
|
||||
)
|
||||
|
||||
var gaClient *GAClient
|
||||
|
||||
func init() {
|
||||
// init GA client
|
||||
clientID, err := machineid.ProtectedID("hrp")
|
||||
if err != nil {
|
||||
clientID = uuid.NewV1().String()
|
||||
}
|
||||
gaClient = NewGAClient(trackingID, clientID)
|
||||
|
||||
// init sentry sdk
|
||||
if env.DISABLE_SENTRY == "true" {
|
||||
return
|
||||
}
|
||||
err = sentry.Init(sentry.ClientOptions{
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: fmt.Sprintf("httprunner@%s", version.VERSION),
|
||||
AttachStacktrace: true,
|
||||
@@ -43,15 +31,7 @@ func init() {
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetUser(sentry.User{
|
||||
ID: clientID,
|
||||
ID: userID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func SendEvent(e IEvent) error {
|
||||
if env.DISABLE_GA == "true" {
|
||||
// do not send GA events in CI environment
|
||||
return nil
|
||||
}
|
||||
return gaClient.SendEvent(e)
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v4.3.4
|
||||
v4.3.5
|
||||
@@ -8,4 +8,4 @@ import (
|
||||
var VERSION string
|
||||
|
||||
// httprunner python version
|
||||
const HttpRunnerMinimumVersion = "v4.3.0"
|
||||
const HttpRunnerMinimumVersion = "v4.3.5"
|
||||
|
||||
@@ -4,14 +4,9 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
func OpenWiki() error {
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "OpenWiki",
|
||||
Action: "hrp wiki",
|
||||
})
|
||||
log.Info().Msgf("%s https://httprunner.com", openCmd)
|
||||
return myexec.RunCommand(openCmd, "https://httprunner.com")
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ const (
|
||||
|
||||
/*
|
||||
[
|
||||
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
|
||||
]
|
||||
*/
|
||||
type Parameters []map[string]interface{}
|
||||
@@ -205,36 +207,38 @@ func genCartesianProduct(multiParameters []Parameters) Parameters {
|
||||
return cartesianProduct
|
||||
}
|
||||
|
||||
/* loadParameters loads parameters from multiple sources.
|
||||
/*
|
||||
loadParameters loads parameters from multiple sources.
|
||||
|
||||
parameter value may be in three types:
|
||||
|
||||
(1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
|
||||
(2) call built-in parameterize function, "${parameterize(account.csv)}"
|
||||
(3) call custom function in debugtalk.py, "${gen_app_version()}"
|
||||
|
||||
configParameters = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1
|
||||
"username-password": "${parameterize(account.csv)}", // case 2
|
||||
"app_version": "${gen_app_version()}", // case 3
|
||||
}
|
||||
configParameters = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1
|
||||
"username-password": "${parameterize(account.csv)}", // case 2
|
||||
"app_version": "${gen_app_version()}", // case 3
|
||||
}
|
||||
|
||||
=>
|
||||
|
||||
{
|
||||
"user_agent": [
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
{"user_agent": "iOS/10.3"},
|
||||
],
|
||||
"username-password": [
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
],
|
||||
"app_version": [
|
||||
{"app_version": "1.0.0"},
|
||||
{"app_version": "1.0.1"},
|
||||
]
|
||||
}
|
||||
{
|
||||
"user_agent": [
|
||||
{"user_agent": "iOS/10.1"},
|
||||
{"user_agent": "iOS/10.2"},
|
||||
{"user_agent": "iOS/10.3"},
|
||||
],
|
||||
"username-password": [
|
||||
{"username": "test1", "password": "111111"},
|
||||
{"username": "test2", "password": "222222"},
|
||||
],
|
||||
"app_version": [
|
||||
{"app_version": "1.0.0"},
|
||||
{"app_version": "1.0.1"},
|
||||
]
|
||||
}
|
||||
*/
|
||||
func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
|
||||
map[string]Parameters, error) {
|
||||
@@ -296,19 +300,23 @@ func (p *Parser) loadParameters(configParameters map[string]interface{}, variabl
|
||||
return parsedParameters, nil
|
||||
}
|
||||
|
||||
/* convert parameters to standard format
|
||||
/*
|
||||
convert parameters to standard format
|
||||
|
||||
key and parametersRawList may be in three types:
|
||||
|
||||
case 1:
|
||||
|
||||
key = "user_agent"
|
||||
parametersRawList = ["iOS/10.1", "iOS/10.2"]
|
||||
|
||||
case 2:
|
||||
|
||||
key = "username-password"
|
||||
parametersRawList = [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
|
||||
|
||||
case 3:
|
||||
|
||||
key = "username-password"
|
||||
parametersRawList = [["test1", "111111"], ["test2", "222222"]]
|
||||
*/
|
||||
|
||||
@@ -28,32 +28,48 @@ type Parser struct {
|
||||
plugin funplugin.IPlugin // plugin is used to call functions
|
||||
}
|
||||
|
||||
func buildURL(baseURL, stepURL string) string {
|
||||
func buildURL(baseURL, stepURL string, queryParams url.Values) (fullUrl *url.URL) {
|
||||
uStep, err := url.Parse(stepURL)
|
||||
if err != nil {
|
||||
log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse url failed")
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// append query params
|
||||
if paramStr := queryParams.Encode(); paramStr != "" {
|
||||
if uStep.RawQuery == "" {
|
||||
uStep.RawQuery = paramStr
|
||||
} else {
|
||||
uStep.RawQuery = uStep.RawQuery + "&" + paramStr
|
||||
}
|
||||
}
|
||||
|
||||
// ensure path suffix '/' exists
|
||||
if uStep.RawQuery == "" {
|
||||
uStep.Path = strings.TrimRight(uStep.Path, "/") + "/"
|
||||
}
|
||||
|
||||
fullUrl = uStep
|
||||
}()
|
||||
|
||||
// step url is absolute url
|
||||
if uStep.Host != "" {
|
||||
return stepURL
|
||||
return uStep
|
||||
}
|
||||
|
||||
// step url is relative, based on base url
|
||||
uConfig, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed")
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
// merge url
|
||||
uStep.Scheme = uConfig.Scheme
|
||||
uStep.Host = uConfig.Host
|
||||
uStep.Path = path.Join(uConfig.Path, uStep.Path)
|
||||
|
||||
// base url missed
|
||||
return uStep.String()
|
||||
return uStep
|
||||
}
|
||||
|
||||
func (p *Parser) ParseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -9,60 +10,72 @@ import (
|
||||
)
|
||||
|
||||
func TestBuildURL(t *testing.T) {
|
||||
var url string
|
||||
var preparedURL *url.URL
|
||||
|
||||
url = buildURL("https://postman-echo.com", "/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
preparedURL = buildURL("https://postman-echo.com", "/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com", "get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
preparedURL = buildURL("https://postman-echo.com", "get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com/", "/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
preparedURL = buildURL("https://postman-echo.com/", "/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
preparedURL = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com/abc", "get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
preparedURL = buildURL("https://postman-echo.com/abc", "get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// omit query string in base url
|
||||
url = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
preparedURL = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("", "https://postman-echo.com/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
preparedURL = buildURL("", "https://postman-echo.com/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// notice: step request url > config base url
|
||||
url = buildURL("https://postman-echo.com", "https://httpbin.org/get")
|
||||
if !assert.Equal(t, url, "https://httpbin.org/get") {
|
||||
preparedURL = buildURL("https://postman-echo.com", "https://httpbin.org/get", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://httpbin.org/get/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// websocket url
|
||||
url = buildURL("wss://ws.postman-echo.com/raw", "")
|
||||
if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") {
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com/raw", "", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("wss://ws.postman-echo.com", "/raw")
|
||||
if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") {
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com", "/raw", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events")
|
||||
if !assert.Equal(t, url, "ws://echo.websocket.events") {
|
||||
preparedURL = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events", nil)
|
||||
if !assert.Equal(t, preparedURL.String(), "ws://echo.websocket.events/") {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
queryParams := url.Values{}
|
||||
queryParams.Add("c", "3")
|
||||
queryParams.Add("d", "4")
|
||||
preparedURL = buildURL("https://postman-echo.com/", "/get/", queryParams)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get?c=3&d=4") {
|
||||
t.Fatal()
|
||||
}
|
||||
preparedURL = buildURL("https://postman-echo.com/abc", "get?a=1&b=2", queryParams)
|
||||
if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2&c=3&d=4") {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestSpawnWorkersWithManyTasks(t *testing.T) {
|
||||
const numToSpawn int64 = 20
|
||||
|
||||
go runner.spawnWorkers(numToSpawn, float64(numToSpawn), runner.stopChan, runner.spawnComplete)
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
currentClients := runner.controller.getCurrentClientsNum()
|
||||
|
||||
@@ -210,28 +210,29 @@ func TestSpawnWorkersWithManyTasks(t *testing.T) {
|
||||
lock.Unlock()
|
||||
|
||||
total := hundreds + tens + ones
|
||||
t.Logf("total tasks run: %d\n", total)
|
||||
t.Logf("total tasks: %d, hundreds: %d, tens: %d, ones: %d\n",
|
||||
total, hundreds, tens, ones)
|
||||
|
||||
assert.True(t, total > 111)
|
||||
|
||||
assert.True(t, ones > 1)
|
||||
actPercentage := float64(ones) / float64(total)
|
||||
expectedPercentage := 1.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
if actPercentage > 4*expectedPercentage || actPercentage < 0.25*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of ones task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
|
||||
assert.True(t, tens > 10)
|
||||
actPercentage = float64(tens) / float64(total)
|
||||
expectedPercentage = 10.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
if actPercentage > 4*expectedPercentage || actPercentage < 0.25*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of tens task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
|
||||
assert.True(t, hundreds > 100)
|
||||
actPercentage = float64(hundreds) / float64(total)
|
||||
expectedPercentage = 100.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
if actPercentage > 1 || actPercentage < 0.25*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of hundreds task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
}
|
||||
@@ -259,7 +260,7 @@ func TestSpawnAndStop(t *testing.T) {
|
||||
go runner.start()
|
||||
|
||||
// wait for spawning goroutines
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(5 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
@@ -269,7 +270,6 @@ func TestSpawnAndStop(t *testing.T) {
|
||||
t.Error("Runner should send spawning_complete message when spawning completed, got", msg.Type)
|
||||
}
|
||||
go runner.stop()
|
||||
close(runner.doneChan)
|
||||
|
||||
runner.onQuiting()
|
||||
msg = <-runner.client.sendChannel()
|
||||
@@ -384,7 +384,7 @@ func TestOnMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
// spawn complete and running
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(5 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
@@ -430,7 +430,7 @@ func TestOnMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
// spawn complete and running
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(5 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package convert
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
@@ -139,19 +139,25 @@ func (c *TCaseConverter) loadCase(casePath string, fromType FromType) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) Convert(casePath string, fromType FromType, outputType OutputType) error {
|
||||
// report event
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()),
|
||||
})
|
||||
func (c *TCaseConverter) Convert(casePath string, fromType FromType, outputType OutputType) (err error) {
|
||||
// report GA event
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
sdk.SendGA4Event("hrp_convert", map[string]interface{}{
|
||||
"from": fromType.String(),
|
||||
"to": outputType.String(),
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
log.Info().Str("path", casePath).
|
||||
Str("fromType", fromType.String()).
|
||||
Str("outputType", outputType.String()).
|
||||
Msg("convert testcase")
|
||||
|
||||
// load source file
|
||||
err := c.loadCase(casePath, fromType)
|
||||
err = c.loadCase(casePath, fromType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -555,8 +555,9 @@ func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
|
||||
// 2, the `className(String)` matches the image
|
||||
// widget class, and `enabled(boolean)` is true.
|
||||
// The code would look like this:
|
||||
// `new UiSelector().className("android.widget.ImageView")
|
||||
// .enabled(true).instance(2);`
|
||||
//
|
||||
// `new UiSelector().className("android.widget.ImageView")
|
||||
// .enabled(true).instance(2);`
|
||||
func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance))
|
||||
return s
|
||||
|
||||
@@ -261,7 +261,8 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...Action
|
||||
// Swipe performs a swipe from one coordinate to another using the number of steps
|
||||
// to determine smoothness and speed. Each step execution is throttled to 5ms
|
||||
// per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
|
||||
// `steps` is the number of move steps sent to the system
|
||||
//
|
||||
// `steps` is the number of move steps sent to the system
|
||||
func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
|
||||
return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ func (caps Capabilities) WithDefaultAlertAction(alertAction AlertAction) Capabil
|
||||
}
|
||||
|
||||
// WithMaxTypingFrequency
|
||||
// Defaults to `60`.
|
||||
//
|
||||
// Defaults to `60`.
|
||||
func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities {
|
||||
if n <= 0 {
|
||||
n = 60
|
||||
@@ -41,21 +42,24 @@ func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities {
|
||||
}
|
||||
|
||||
// WithWaitForIdleTimeout
|
||||
// Defaults to `10`
|
||||
//
|
||||
// Defaults to `10`
|
||||
func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities {
|
||||
caps["waitForIdleTimeout"] = second
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility
|
||||
// Defaults to `false`
|
||||
//
|
||||
// Defaults to `false`
|
||||
func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities {
|
||||
caps["shouldUseTestManagerForVisibilityDetection"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseCompactResponses If set to YES will use compact (standards-compliant) & faster responses
|
||||
// Defaults to `true`
|
||||
//
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities {
|
||||
caps["shouldUseCompactResponses"] = b
|
||||
return caps
|
||||
@@ -63,28 +67,32 @@ func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities {
|
||||
|
||||
// WithElementResponseAttributes If shouldUseCompactResponses == NO,
|
||||
// is the comma-separated list of fields to return with each element.
|
||||
// Defaults to `type,label`.
|
||||
//
|
||||
// Defaults to `type,label`.
|
||||
func (caps Capabilities) WithElementResponseAttributes(s string) Capabilities {
|
||||
caps["elementResponseAttributes"] = s
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseSingletonTestManager
|
||||
// Defaults to `true`
|
||||
//
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities {
|
||||
caps["shouldUseSingletonTestManager"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithDisableAutomaticScreenshots
|
||||
// Defaults to `true`
|
||||
//
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities {
|
||||
caps["disableAutomaticScreenshots"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldTerminateApp
|
||||
// Defaults to `true`
|
||||
//
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities {
|
||||
caps["shouldTerminateApp"] = b
|
||||
return caps
|
||||
@@ -376,7 +384,8 @@ func (opt SourceOption) WithFormatAsDescription() SourceOption {
|
||||
}
|
||||
|
||||
// WithScope Allows to provide XML scope.
|
||||
// only `xml` is supported.
|
||||
//
|
||||
// only `xml` is supported.
|
||||
func (opt SourceOption) WithScope(scope string) SourceOption {
|
||||
if vFormat, ok := opt["format"]; ok && vFormat != "xml" {
|
||||
return opt
|
||||
|
||||
@@ -176,11 +176,12 @@ func newVEDEMImageService(actions ...string) (*veDEMImageService, error) {
|
||||
|
||||
// veDEMImageService implements IImageService interface
|
||||
// actions:
|
||||
// ocr - get ocr texts
|
||||
// upload - get image uploaded url
|
||||
// liveType - get live type
|
||||
// popup - get popup windows
|
||||
// close - get close popup
|
||||
//
|
||||
// ocr - get ocr texts
|
||||
// upload - get image uploaded url
|
||||
// liveType - get live type
|
||||
// popup - get popup windows
|
||||
// close - get close popup
|
||||
type veDEMImageService struct {
|
||||
actions []string
|
||||
}
|
||||
@@ -230,10 +231,6 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer) (
|
||||
req.Header.Add("Agw-Auth-Content", signToken)
|
||||
req.Header.Add("Content-Type", bodyWriter.FormDataContentType())
|
||||
|
||||
// ppe
|
||||
// req.Header.Add("x-use-ppe", "1")
|
||||
// req.Header.Add("x-tt-env", "ppe_vedem_algorithm")
|
||||
|
||||
var resp *http.Response
|
||||
// retry 3 times
|
||||
for i := 1; i <= 3; i++ {
|
||||
|
||||
@@ -90,15 +90,14 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
|
||||
pluginMap.Store(pluginPath, plugin)
|
||||
|
||||
// report event for initializing plugin
|
||||
event := sdk.EventTracking{
|
||||
Category: "InitPlugin",
|
||||
Action: fmt.Sprintf("Init %s plugin", plugin.Type()),
|
||||
Value: 0, // success
|
||||
params := map[string]interface{}{
|
||||
"type": plugin.Type(),
|
||||
"result": "success",
|
||||
}
|
||||
if err != nil {
|
||||
event.Value = 1 // failed
|
||||
params["result"] = "failed"
|
||||
}
|
||||
go sdk.SendEvent(event)
|
||||
go sdk.SendGA4Event("init_plugin", params)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,14 +194,16 @@ func (r *HRPRunner) GenHTMLReport() *HRPRunner {
|
||||
// Run starts to execute one or multiple testcases.
|
||||
func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
|
||||
log.Info().Str("hrp_version", version.VERSION).Msg("start running")
|
||||
event := sdk.EventTracking{
|
||||
Category: "RunAPITests",
|
||||
Action: "hrp run",
|
||||
}
|
||||
// report start event
|
||||
go sdk.SendEvent(event)
|
||||
// report execution timing event
|
||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
// report run event
|
||||
sdk.SendGA4Event("hrp_run", map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"engagement_time_msec": time.Since(startTime).Milliseconds(),
|
||||
})
|
||||
}()
|
||||
|
||||
// record execution data to summary
|
||||
s := newOutSummary()
|
||||
|
||||
@@ -511,6 +513,9 @@ func (r *SessionRunner) inheritConnection(src *SessionRunner) {
|
||||
// Start runs the test steps in sequential order.
|
||||
// givenVars is used for data driven
|
||||
func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_session_runner_start", nil)
|
||||
|
||||
config := r.caseRunner.testCase.Config
|
||||
log.Info().Str("testcase", config.Name).Msg("run testcase start")
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
)
|
||||
|
||||
func buildHashicorpGoPlugin() {
|
||||
@@ -63,31 +64,26 @@ func assertRunTestCases(t *testing.T) {
|
||||
refCase := TestCasePath(demoTestCaseWithPluginJSONPath)
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("testcase1-step1").
|
||||
GET("/headers").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
NewStep("testcase1-step2").
|
||||
GET("/user-agent").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
NewStep("testcase1-step3").CallRefCase(
|
||||
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://httpbin.org"),
|
||||
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("ip").
|
||||
GET("/ip").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check http response Content-Type"),
|
||||
},
|
||||
},
|
||||
),
|
||||
NewStep("testcase1-step4").CallRefCase(&refCase),
|
||||
NewStep("testcase1-step3").CallRefCase(&refCase),
|
||||
},
|
||||
}
|
||||
testcase2 := &TestCase{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
@@ -564,6 +565,11 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
mobileStep = step.Android
|
||||
}
|
||||
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_run_ui", map[string]interface{}{
|
||||
"osType": osType,
|
||||
})
|
||||
|
||||
stepResult = &StepResult{
|
||||
Name: step.Name,
|
||||
StepType: StepType(osType),
|
||||
|
||||
@@ -149,9 +149,8 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{})
|
||||
}
|
||||
var baseURL string
|
||||
if stepVariables["base_url"] != nil {
|
||||
baseURL = stepVariables["base_url"].(string)
|
||||
baseURL, _ = stepVariables["base_url"].(string)
|
||||
}
|
||||
rawUrl := buildURL(baseURL, convertString(requestUrl))
|
||||
|
||||
// prepare request params
|
||||
var queryParams url.Values
|
||||
@@ -161,35 +160,24 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{})
|
||||
return errors.Wrap(err, "parse request params failed")
|
||||
}
|
||||
parsedParams := params.(map[string]interface{})
|
||||
r.requestMap["params"] = parsedParams
|
||||
if len(parsedParams) > 0 {
|
||||
queryParams = make(url.Values)
|
||||
for k, v := range parsedParams {
|
||||
queryParams.Add(k, convertString(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
if queryParams != nil {
|
||||
// append params to url
|
||||
paramStr := queryParams.Encode()
|
||||
if strings.IndexByte(rawUrl, '?') == -1 {
|
||||
rawUrl = rawUrl + "?" + paramStr
|
||||
} else {
|
||||
rawUrl = rawUrl + "&" + paramStr
|
||||
}
|
||||
|
||||
// request params has been appended to url, thus delete it here
|
||||
delete(r.requestMap, "params")
|
||||
}
|
||||
|
||||
// prepare url
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse url failed")
|
||||
}
|
||||
r.req.URL = u
|
||||
r.req.Host = u.Host
|
||||
preparedURL := buildURL(baseURL, convertString(requestUrl), queryParams)
|
||||
r.req.URL = preparedURL
|
||||
r.req.Host = preparedURL.Host
|
||||
|
||||
// update url
|
||||
r.requestMap["url"] = u.String()
|
||||
|
||||
r.requestMap["url"] = preparedURL.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -340,43 +328,14 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
// add request object to step variables, could be used in setup hooks
|
||||
stepVariables["hrp_step_name"] = step.Name
|
||||
stepVariables["hrp_step_request"] = rb.requestMap
|
||||
stepVariables["request"] = rb.requestMap
|
||||
stepVariables["request"] = rb.requestMap // setup hooks compatible with v3
|
||||
|
||||
// deal with setup hooks
|
||||
for _, setupHook := range step.SetupHooks {
|
||||
req, err := parser.Parse(setupHook, stepVariables)
|
||||
_, err := parser.Parse(setupHook, stepVariables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run setup hooks failed")
|
||||
}
|
||||
reqMap, ok := req.(map[string]interface{})
|
||||
if ok && reqMap != nil {
|
||||
rb.requestMap = reqMap
|
||||
stepVariables["request"] = reqMap
|
||||
}
|
||||
}
|
||||
if len(step.SetupHooks) > 0 {
|
||||
requestBody, ok := rb.requestMap["body"].(map[string]interface{})
|
||||
if ok {
|
||||
body, err := json.Marshal(requestBody)
|
||||
if err == nil {
|
||||
rb.req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
rb.req.ContentLength = int64(len(body))
|
||||
}
|
||||
}
|
||||
requestParams, ok := rb.requestMap["params"].(map[string]interface{})
|
||||
if ok {
|
||||
params, err := json.Marshal(requestParams)
|
||||
if err == nil {
|
||||
rb.req.URL.RawQuery = string(params)
|
||||
}
|
||||
}
|
||||
requestHeaders, ok := rb.requestMap["headers"].(map[string]interface{})
|
||||
if ok {
|
||||
rb.req.Header = http.Header{}
|
||||
for k, v := range requestHeaders {
|
||||
rb.req.Header.Set(k, v.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log & print request
|
||||
@@ -451,15 +410,10 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
|
||||
// deal with teardown hooks
|
||||
for _, teardownHook := range step.TeardownHooks {
|
||||
res, err := parser.Parse(teardownHook, stepVariables)
|
||||
_, err := parser.Parse(teardownHook, stepVariables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run teardown hooks failed")
|
||||
}
|
||||
resMpa, ok := res.(map[string]interface{})
|
||||
if ok {
|
||||
stepVariables["response"] = resMpa
|
||||
respObj.respObjMeta = resMpa
|
||||
}
|
||||
}
|
||||
|
||||
sessionData.ReqResps.Request = rb.requestMap
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestRunRequestStatOn(t *testing.T) {
|
||||
if !assert.Greater(t, stat["TLSHandshake"], int64(0)) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Greater(t, stat["ServerProcessing"], int64(1)) {
|
||||
if !assert.Greater(t, stat["ServerProcessing"], int64(0)) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.GreaterOrEqual(t, stat["ContentTransfer"], int64(0)) {
|
||||
@@ -165,7 +165,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").
|
||||
SetRequestTimeout(10). // set global timeout to 10s
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step1").
|
||||
GET("/delay/1").
|
||||
@@ -180,11 +180,11 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
|
||||
testcase2 := &TestCase{
|
||||
Config: NewConfig("TestCase2").
|
||||
SetRequestTimeout(10). // set global timeout to 10s
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
SetRequestTimeout(5). // set global timeout to 10s
|
||||
SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step1").
|
||||
GET("/delay/11").
|
||||
GET("/delay/10").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
},
|
||||
@@ -198,7 +198,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
testcase3 := &TestCase{
|
||||
Config: NewConfig("TestCase3").
|
||||
SetRequestTimeout(10).
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step2").
|
||||
GET("/delay/11").
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user