Merge pull request #1654 from httprunner/feat-ga4

release v4.3.5

- refactor: send events to Google Analytics 4, replace GA v1
- fix: failure unittests caused by httpbin.org, replace with docker service
- fix: handle unstable unittests, restore github actions pipeline

**go version**

- feat: report GA4 events for hrp cmd
- change: create python venv with httprunner minimum version v4.3.5
- fix #1603: ensure path suffix '/' exists

**python version**

- fix: upgrade pyyaml from 5.4.1 to 6.0.1, fix installing error
- refactor: update httprunner dependencies
This commit is contained in:
debugtalk
2023-07-24 00:09:31 +08:00
committed by GitHub
91 changed files with 2028 additions and 1694 deletions

View File

@@ -16,12 +16,17 @@ jobs:
name: smoketest - httprunner - ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
services:
service-httpbin:
image: kennethreitz/httpbin
ports:
- 80:80
strategy:
fail-fast: false
max-parallel: 6
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest] # FIXME: docker services are not supported on macos-latest, windows-latest
steps:
- uses: actions/checkout@v2

View File

@@ -14,12 +14,17 @@ env:
jobs:
py-httprunner:
runs-on: ${{ matrix.os }}
services:
service-httpbin:
image: kennethreitz/httpbin
ports:
- 80:80
strategy:
fail-fast: false
max-parallel: 12
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest] # FIXME: docker services are not supported on macos-latest, windows-latest
steps:
- uses: actions/checkout@v2
@@ -45,7 +50,7 @@ jobs:
poetry run coverage xml
poetry run coverage report -m
- name: Codecov
uses: codecov/codecov-action@v1.0.5
uses: codecov/codecov-action@v3
with:
# User defined upload name. Visible in Codecov UI
name: httprunner
@@ -78,7 +83,7 @@ jobs:
- name: Run coverage
run: go test -coverprofile="cover.out" -covermode=atomic -race ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
with:
name: hrp (HttpRunner+) # User defined upload name. Visible in Codecov UI
token: ${{ secrets.CODECOV_TOKEN }} # Repository upload token

View File

@@ -1,5 +1,22 @@
# Release History
## v4.3.5 (2023-07-23)
- refactor: send events to Google Analytics 4, replace GA v1
- fix: failure unittests caused by httpbin.org, replace with docker service
- fix: handle unstable unittests, restore github actions pipeline
**go version**
- feat: report GA4 events for hrp cmd
- change: create python venv with httprunner minimum version v4.3.5
- fix #1603: ensure path suffix '/' exists
**python version**
- fix: upgrade pyyaml from 5.4.1 to 6.0.1, fix installing error
- refactor: update httprunner dependencies
## v4.3.4 (2023-06-01)
**go version**

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: a-b.c/1.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: a-b.c/2 3.yml
from httprunner import HttpRunner, Config, Step, RunRequest
from httprunner import RunTestCase

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-empty-project",
"create_time": "2023-05-31T20:46:10.736189+08:00",
"hrp_version": "v4.3.4"
"create_time": "2023-07-23T13:54:23.516072+08:00",
"hrp_version": "v4.3.5"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-go-plugin",
"create_time": "2023-05-31T20:44:56.120736+08:00",
"hrp_version": "v4.3.4"
"create_time": "2023-07-23T14:30:10.985053+08:00",
"hrp_version": "v4.3.5"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-py-plugin",
"create_time": "2023-05-31T20:45:00.44921+08:00",
"hrp_version": "v4.3.4"
"create_time": "2023-07-23T14:30:18.556239+08:00",
"hrp_version": "v4.3.5"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-without-plugin",
"create_time": "2023-05-31T20:46:10.621009+08:00",
"hrp_version": "v4.3.4"
"create_time": "2023-07-23T13:54:23.368356+08:00",
"hrp_version": "v4.3.5"
}

View File

@@ -1,6 +1,6 @@
config:
name: basic test with httpbin
base_url: https://httpbin.org/
base_url: ${get_httpbin_server()}
teststeps:
-
@@ -10,7 +10,7 @@ teststeps:
method: GET
validate:
- eq: ["status_code", 200]
- eq: [body.headers.Host, "httpbin.org"]
- eq: [body.headers.Host, "127.0.0.1"]
-
name: user-agent

View File

@@ -1,11 +1,11 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseBasic(HttpRunner):
config = Config("basic test with httpbin").base_url("https://httpbin.org/")
config = Config("basic test with httpbin").base_url("${get_httpbin_server()}")
teststeps = [
Step(
@@ -13,7 +13,7 @@ class TestCaseBasic(HttpRunner):
.get("/headers")
.validate()
.assert_equal("status_code", 200)
.assert_equal("body.headers.Host", "httpbin.org")
.assert_equal("body.headers.Host", "127.0.0.1")
),
Step(
RunRequest("user-agent")

View File

@@ -6,9 +6,11 @@ import uuid
from loguru import logger
from httprunner.utils import HTTP_BIN_URL
def get_httpbin_server():
return "https://httpbin.org"
return HTTP_BIN_URL
def setup_testcase(variables):
@@ -17,7 +19,7 @@ def setup_testcase(variables):
def teardown_testcase():
logger.info(f"teardown_testcase.")
logger.info("teardown_testcase.")
def setup_teststep(request, variables):

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,6 +1,6 @@
config:
name: basic test with httpbin
base_url: https://httpbin.org/
base_url: ${get_httpbin_server()}
teststeps:
-

View File

@@ -1,11 +1,11 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseValidate(HttpRunner):
config = Config("basic test with httpbin").base_url("https://httpbin.org/")
config = Config("basic test with httpbin").base_url("${get_httpbin_server()}")
teststeps = [
Step(

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: cookie_manipulation/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.1.4
# NOTE: Generated By HttpRunner v4.3.5
# FROM: cookie_manipulation/set_delete_cookies.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/request_with_parameters.yml
import pytest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/request_with_testcase_reference.yml
from httprunner import HttpRunner, Config, Step, RunRequest
from httprunner import RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.3.0
# NOTE: Generated By HttpRunner v4.3.5
# FROM: request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest

3
go.mod
View File

@@ -18,7 +18,6 @@ require (
github.com/maja42/goval v1.2.1
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/otiai10/gosseract/v2 v2.4.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.29.1
@@ -53,6 +52,7 @@ require (
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
@@ -67,6 +67,7 @@ require (
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect

15
go.sum
View File

@@ -203,7 +203,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -249,15 +250,9 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/gosseract/v2 v2.4.0 h1:gYd3mx6FuMtIlxL4sYb9JLCFEDzg09VgNSZRNbqpiGM=
github.com/otiai10/gosseract/v2 v2.4.0/go.mod h1:fhbIDRh29bj13vni6RT3gtWKjKCAeqDYI4C1dxeJuek=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -292,7 +287,9 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1 +1 @@
v4.3.4
v4.3.5

View File

@@ -8,4 +8,4 @@ import (
var VERSION string
// httprunner python version
const HttpRunnerMinimumVersion = "v4.3.0"
const HttpRunnerMinimumVersion = "v4.3.5"

View File

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

View File

@@ -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"]]
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -263,7 +263,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...)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
//go:build localtest
package tests
import (

View File

@@ -1,4 +1,4 @@
__version__ = "v4.3.0"
__version__ = "v4.3.5"
__description__ = "One-stop solution for HTTP(S) testing."

View File

@@ -9,7 +9,7 @@ from loguru import logger
from httprunner import __description__, __version__
from httprunner.compat import ensure_cli_args
from httprunner.make import init_make_parser, main_make
from httprunner.utils import ga_client, init_logger, init_sentry_sdk
from httprunner.utils import ga4_client, init_logger, init_sentry_sdk
init_sentry_sdk()
@@ -22,7 +22,7 @@ def init_parser_run(subparsers):
def main_run(extra_args) -> enum.IntEnum:
ga_client.track_event("RunAPITests", "hrun")
ga4_client.send_event("hrun")
# keep compatibility with v2
extra_args = ensure_cli_args(extra_args)

View File

@@ -1,6 +1,7 @@
import unittest
from httprunner.client import HttpSession
from httprunner.utils import HTTP_BIN_URL
class TestHttpSession(unittest.TestCase):
@@ -8,15 +9,15 @@ class TestHttpSession(unittest.TestCase):
self.session = HttpSession()
def test_request_http(self):
self.session.request("get", "https://httpbin.org/get")
self.session.request("get", f"{HTTP_BIN_URL}/get")
address = self.session.data.address
self.assertGreater(len(address.server_ip), 0)
self.assertEqual(address.server_port, 443)
self.assertEqual(address.server_port, 80)
self.assertGreater(len(address.client_ip), 0)
self.assertGreater(address.client_port, 10000)
def test_request_https(self):
self.session.request("get", "https://httpbin.org/get")
self.session.request("get", "https://postman-echo.com/get")
address = self.session.data.address
self.assertGreater(len(address.server_ip), 0)
self.assertEqual(address.server_port, 443)
@@ -26,7 +27,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_allow_redirects(self):
self.session.request(
"get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
f"{HTTP_BIN_URL}/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=True,
)
address = self.session.data.address
@@ -38,7 +39,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_https_allow_redirects(self):
self.session.request(
"get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
"https://postman-echo.com/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=True,
)
address = self.session.data.address
@@ -50,7 +51,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_not_allow_redirects(self):
self.session.request(
"get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
f"{HTTP_BIN_URL}/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=False,
)
address = self.session.data.address
@@ -62,7 +63,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_https_not_allow_redirects(self):
self.session.request(
"get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
"https://postman-echo.com/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=False,
)
address = self.session.data.address

View File

@@ -2,6 +2,7 @@ import os
import unittest
from httprunner import compat, exceptions, loader
from httprunner.utils import HTTP_BIN_URL
class TestCompat(unittest.TestCase):
@@ -155,7 +156,7 @@ class TestCompat(unittest.TestCase):
def test_ensure_testcase_v4(self):
testcase_content = {
"config": {"name": "xxx", "base_url": "https://httpbin.org"},
"config": {"name": "xxx", "base_url": HTTP_BIN_URL},
"teststeps": [
{
"name": "get with params",
@@ -179,7 +180,7 @@ class TestCompat(unittest.TestCase):
self.assertEqual(
compat.ensure_testcase_v4(testcase_content),
{
"config": {"name": "xxx", "base_url": "https://httpbin.org"},
"config": {"name": "xxx", "base_url": HTTP_BIN_URL},
"teststeps": [
{
"name": "get with params",

View File

@@ -47,7 +47,7 @@ import sys
from typing import Text
from httprunner.models import VariablesMapping, FunctionsMapping, TStep
from httprunner.parser import parse_data, parse_variables_mapping
from httprunner.parser import parse_data
from loguru import logger
try:
@@ -138,7 +138,6 @@ def multipart_encoder(**kwargs):
ensure_upload_ready()
fields_dict = {}
for key, value in kwargs.items():
if os.path.isabs(value):
# value is absolute file path
_file_path = value

View File

@@ -22,7 +22,7 @@ from httprunner.loader import (
load_testcase,
)
from httprunner.response import uniform_validator
from httprunner.utils import ga_client, is_support_multiprocessing
from httprunner.utils import ga4_client, is_support_multiprocessing
""" cache converted pytest files, avoid duplicate making
"""
@@ -541,7 +541,7 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
if not tests_paths:
return []
ga_client.track_event("ConvertTests", "hmake")
ga4_client.send_event("hmake")
for tests_path in tests_paths:
tests_path = ensure_path_sep(tests_path)

View File

@@ -4,12 +4,13 @@ import requests
from httprunner.parser import Parser
from httprunner.response import ResponseObject, uniform_validator
from httprunner.utils import HTTP_BIN_URL
class TestResponse(unittest.TestCase):
def setUp(self) -> None:
resp = requests.post(
"https://httpbin.org/anything",
f"{HTTP_BIN_URL}/anything",
json={
"locations": [
{"name": "Seattle", "state": "WA"},

View File

@@ -27,7 +27,7 @@ from httprunner.models import (
VariablesMapping,
)
from httprunner.parser import Parser
from httprunner.utils import LOGGER_FORMAT, merge_variables
from httprunner.utils import LOGGER_FORMAT, merge_variables, ga4_client
class SessionRunner(object):
@@ -210,6 +210,7 @@ class SessionRunner(object):
def test_start(self, param: Dict = None) -> "SessionRunner":
"""main entrance, discovered by pytest"""
ga4_client.send_event("test_start")
print("\n")
self.__init()
self.__parse_config(param)

View File

@@ -5,10 +5,12 @@ import json
import os
import os.path
import platform
import random
import sys
import time
import uuid
from multiprocessing import Queue
from typing import Any, Dict, List, Text
from typing import Any, Dict, List
import requests
import sentry_sdk
@@ -18,6 +20,25 @@ from httprunner import __version__, exceptions
from httprunner.models import VariablesMapping
""" run httpbin as test service
https://github.com/postmanlabs/httpbin
$ docker pull kennethreitz/httpbin
$ docker run -p 80:80 kennethreitz/httpbin
"""
HTTP_BIN_URL = "http://127.0.0.1:80"
def get_platform():
return {
"httprunner_version": __version__,
"python_version": "{} {}".format(
platform.python_implementation(), platform.python_version()
),
"platform": platform.platform(),
}
def init_sentry_sdk():
if os.getenv("DISABLE_SENTRY") == "true":
return
@@ -30,62 +51,79 @@ def init_sentry_sdk():
scope.set_user({"id": uuid.getnode()})
class GAClient(object):
class GA4Client(object):
"""send events to Google Analytics 4 via Measurement Protocol.
get details in hrp/internal/sdk/ga4.go
"""
version = "1" # GA API Version
report_url = "https://www.google-analytics.com/collect"
report_debug_url = (
"https://www.google-analytics.com/debug/collect" # used for debug
)
def __init__(self, tracking_id: Text):
def __init__(
self, measurement_id: str, api_secret: str, debug: bool = False
) -> None:
self.http_client = requests.Session()
self.label = f"v{__version__}"
self.common_params = {
"v": self.version,
"tid": tracking_id, # Tracking ID / Property ID, XX-XXXXXXX-X
"cid": uuid.getnode(), # Anonymous Client ID
"ua": f"HttpRunner/{__version__}",
}
self.debug = debug
if debug:
uri = "https://www.google-analytics.com/debug/mp/collect"
else:
uri = "https://www.google-analytics.com/mp/collect"
self.uri = f"{uri}?measurement_id={measurement_id}&api_secret={api_secret}"
self.user_id = str(uuid.getnode())
self.common_event_params = get_platform()
# do not send GA events in CI environment
self.__is_ci = os.getenv("DISABLE_GA") == "true"
def track_event(self, category: Text, action: Text, value: int = 0):
def send_event(self, name: str, event_params: dict = None) -> None:
if self.__is_ci:
return
data = {
"t": "event", # Event hit type = event
"ec": category, # Required. Event Category.
"ea": action, # Required. Event Action.
"el": self.label, # Optional. Event label, used as version.
"ev": value, # Optional. Event value, must be non-negative integer
event_params = event_params or {}
event_params.update(self.common_event_params)
event = {
"name": name,
"params": event_params,
}
data.update(self.common_params)
try:
self.http_client.post(self.report_url, data=data, timeout=5)
except Exception: # ProxyError, SSLError, ConnectionError
pass
def track_user_timing(self, category: Text, variable: Text, duration: int):
if self.__is_ci:
payload = {
"client_id": f"{int(random.random() * 10**8)}.{int(time.time())}",
"user_id": self.user_id,
"timestamp_micros": int(time.time() * 10**6),
"events": [event],
}
if self.debug:
logger.debug(f"send GA4 event, uri: {self.uri}, payload: {payload}")
try:
resp = self.http_client.post(self.uri, json=payload, timeout=5)
except Exception as err: # ProxyError, SSLError, ConnectionError
logger.error(f"request GA4 failed, error: {err}")
return
if resp.status_code >= 300:
logger.error(
f"validation response got unexpected status: {resp.status_code}"
)
return
if not self.debug:
return
data = {
"t": "timing", # Event hit type = timing
"utc": category, # Required. user timing category. e.g. jsonLoader
"utv": variable, # Required. timing variable. e.g. load
"utt": duration, # Required. time took duration.
"utl": self.label, # Optional. user timing label, used as version.
}
data.update(self.common_params)
try:
self.http_client.post(self.report_url, data=data, timeout=5)
except Exception: # ProxyError, SSLError, ConnectionError
resp_body = resp.json()
logger.debug(
"get GA4 validation response, "
f"status code: {resp.status_code}, body: {resp_body}"
)
except Exception:
pass
ga_client = GAClient("UA-114587036-1")
GA4_MEASUREMENT_ID = "G-9KHR3VC2LN"
GA4_API_SECRET = "w7lKNQIrQsKNS4ikgMPp0Q"
ga4_client = GA4Client(GA4_MEASUREMENT_ID, GA4_API_SECRET, False)
def set_os_environ(variables_mapping):
@@ -219,16 +257,6 @@ def omit_long_data(body, omit_len=512):
return omitted_body + appendix_str
def get_platform():
return {
"httprunner_version": __version__,
"python_version": "{} {}".format(
platform.python_implementation(), platform.python_version()
),
"platform": platform.platform(),
}
def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
def get_index_from_list(lst: List, item: Any):
try:

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import toml
from httprunner import __version__, loader, utils
from httprunner.utils import ExtendJSONEncoder, merge_variables
from httprunner.utils import ExtendJSONEncoder, merge_variables, ga4_client
class TestUtils(unittest.TestCase):
@@ -121,10 +121,10 @@ class TestUtils(unittest.TestCase):
def test_override_config_variables(self):
step_variables = {"base_url": "$base_url", "foo1": "bar1"}
config_variables = {"base_url": "https://httpbin.org", "foo1": "bar111"}
config_variables = {"base_url": "https://postman-echo.com", "foo1": "bar111"}
self.assertEqual(
merge_variables(step_variables, config_variables),
{"base_url": "https://httpbin.org", "foo1": "bar1"},
{"base_url": "https://postman-echo.com", "foo1": "bar1"},
)
def test_cartesian_product_one(self):
@@ -160,3 +160,12 @@ class TestUtils(unittest.TestCase):
pyproject = toml.loads(open(str(path)).read())
pyproject_version = pyproject["tool"]["poetry"]["version"]
self.assertEqual(pyproject_version, __version__)
def test_ga4_send_event(self):
ga4_client.send_event(
"httprunner_debug_event",
{
"a": 123,
"b": 456,
},
)

2307
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "httprunner"
version = "v4.3.0"
version = "v4.3.5"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"
@@ -30,8 +30,6 @@ include = ["docs/CHANGELOG.md"]
[tool.poetry.dependencies]
python = "^3.7"
requests = "^2.22.0"
pyyaml = "^5.4.1"
pydantic = "~1.8" # >=1.8.0 <1.9.0
loguru = "^0.4.1"
jmespath = "^0.9.5"
@@ -40,7 +38,7 @@ pytest = "^7.1.1"
pytest-html = "^3.1.1"
sentry-sdk = "^0.14.4"
allure-pytest = {version = "^2.8.16", optional = true}
requests-toolbelt = {version = "^0.9.1", optional = true}
requests-toolbelt = {version = "^0.10.1", optional = true}
filetype = {version = "^1.0.7", optional = true}
Brotli = "^1.0.9"
jinja2 = "^3.0.3"
@@ -50,6 +48,9 @@ pymysql = {version = "^1.0.2",optional = true}
cython = {version = "^0.29.28", optional = true}
thriftpy2 = {version = "^0.4.14", optional = true}
thrift = {version = "^0.16.0", optional = true}
pyyaml = "^6.0.1"
requests = "^2.31.0"
urllib3 = "^1.26"
[tool.poetry.extras]
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure