Merge branch 'master' into auto-reset-session

This commit is contained in:
bbx-winner
2023-07-24 13:34:37 +08:00
committed by GitHub
91 changed files with 2026 additions and 1697 deletions

View File

@@ -16,12 +16,17 @@ jobs:
name: smoketest - httprunner - ${{ matrix.python-version }} on ${{ matrix.os }} name: smoketest - httprunner - ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
services:
service-httpbin:
image: kennethreitz/httpbin
ports:
- 80:80
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 6 max-parallel: 6
matrix: matrix:
python-version: ['3.7', '3.8', '3.9', '3.10'] 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

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

View File

@@ -1,11 +1,23 @@
# Release History # Release History
## v4.3.5 (2023-07-20) ## 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
- feat: support to reset driver automatically when uia2 crashed
- feat: support to reset session when wda request failed
**go version** **go version**
- feat: support to reset driver automatically when uia2 crashed - feat: report GA4 events for hrp cmd
- feat: support to reset session when wda request failed - 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) ## v4.3.4 (2023-06-01)

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: a-b.c/1.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: a-b.c/2 3.yml
from httprunner import HttpRunner, Config, Step, RunRequest from httprunner import HttpRunner, Config, Step, RunRequest
from httprunner import RunTestCase from httprunner import RunTestCase

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
config: config:
name: basic test with httpbin name: basic test with httpbin
base_url: https://httpbin.org/ base_url: ${get_httpbin_server()}
teststeps: teststeps:
- -
@@ -10,7 +10,7 @@ teststeps:
method: GET method: GET
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: [body.headers.Host, "httpbin.org"] - eq: [body.headers.Host, "127.0.0.1"]
- -
name: user-agent 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: basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseBasic(HttpRunner): 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 = [ teststeps = [
Step( Step(
@@ -13,7 +13,7 @@ class TestCaseBasic(HttpRunner):
.get("/headers") .get("/headers")
.validate() .validate()
.assert_equal("status_code", 200) .assert_equal("status_code", 200)
.assert_equal("body.headers.Host", "httpbin.org") .assert_equal("body.headers.Host", "127.0.0.1")
), ),
Step( Step(
RunRequest("user-agent") RunRequest("user-agent")

View File

@@ -6,9 +6,11 @@ import uuid
from loguru import logger from loguru import logger
from httprunner.utils import HTTP_BIN_URL
def get_httpbin_server(): def get_httpbin_server():
return "https://httpbin.org" return HTTP_BIN_URL
def setup_testcase(variables): def setup_testcase(variables):
@@ -17,7 +19,7 @@ def setup_testcase(variables):
def teardown_testcase(): def teardown_testcase():
logger.info(f"teardown_testcase.") logger.info("teardown_testcase.")
def setup_teststep(request, variables): 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: hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest from httprunner import HttpRunner, Config, Step, RunRequest

View File

@@ -1,6 +1,6 @@
config: config:
name: basic test with httpbin name: basic test with httpbin
base_url: https://httpbin.org/ base_url: ${get_httpbin_server()}
teststeps: 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: validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseValidate(HttpRunner): 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 = [ teststeps = [
Step( 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: cookie_manipulation/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: cookie_manipulation/set_delete_cookies.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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 # FROM: request_methods/request_with_parameters.yml
import pytest 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: request_methods/request_with_testcase_reference.yml
from httprunner import HttpRunner, Config, Step, RunRequest from httprunner import HttpRunner, Config, Step, RunRequest
from httprunner import RunTestCase 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: request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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: request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest 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/maja42/goval v1.2.1
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/otiai10/gosseract/v2 v2.4.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.29.1
@@ -53,6 +52,7 @@ require (
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // 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/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // 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/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rivo/uniseg v0.2.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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.10 // 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/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/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.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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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= 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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.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/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 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 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. // Run starts to run load test for one or multiple testcases.
func (b *HRPBoomer) Run(testcases ...ITestCase) { func (b *HRPBoomer) Run(testcases ...ITestCase) {
event := sdk.EventTracking{ startTime := time.Now()
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
defer func() { 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 { pluginMap.Range(func(key, value interface{}) bool {
if plugin, ok := value.(funplugin.IPlugin); ok { if plugin, ok := value.(funplugin.IPlugin); ok {
plugin.Quit() plugin.Quit()

View File

@@ -10,7 +10,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
defer removeHashicorpGoPlugin() defer removeHashicorpGoPlugin()
testcase1 := &TestCase{ testcase1 := &TestCase{
Config: NewConfig("TestCase1").SetBaseURL("https://httpbin.org"), Config: NewConfig("TestCase1").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("headers"). NewStep("headers").
GET("/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/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version" "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 // buildGo builds debugtalk.go to debugtalk.bin
func buildGo(path string, output string) error { func buildGo(path string, output string) error {
log.Info().Str("path", path).Str("output", output).Msg("start to build go plugin") 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) content, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to read file") 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 // buildPy completes funppy information in debugtalk.py
func buildPy(path string, output string) error { func buildPy(path string, output string) error {
log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin") 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 // check the syntax of debugtalk.py
err := myexec.ExecPython3Command("py_compile", path) err := myexec.ExecPython3Command("py_compile", path)
if err != nil { if err != nil {

View File

@@ -4,9 +4,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
@@ -18,7 +21,16 @@ func format(data map[string]string) string {
var listAndroidDevicesCmd = &cobra.Command{ var listAndroidDevicesCmd = &cobra.Command{
Use: "devices", Use: "devices",
Short: "List all Android 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) deviceList, err := uixt.GetAndroidDevices(serial)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)

View File

@@ -3,16 +3,28 @@ package adb
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
var screencapAndroidDevicesCmd = &cobra.Command{ var screencapAndroidDevicesCmd = &cobra.Command{
Use: "screencap", Use: "screencap",
Short: "Start android screen capture", 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) device, err := getDevice(serial)
if err != nil { if err != nil {
return err return err

View File

@@ -10,6 +10,7 @@ import (
"github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer" "github.com/httprunner/httprunner/v4/hrp/pkg/boomer"
) )
@@ -29,7 +30,16 @@ var boomCmd = &cobra.Command{
} }
setLogLevel(logLevel) 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 var paths []hrp.ITestCase
for _, arg := range args { for _, arg := range args {
path := hrp.TestCasePath(arg) path := hrp.TestCasePath(arg)

View File

@@ -1,9 +1,13 @@
package cmd package cmd
import ( import (
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
var buildCmd = &cobra.Command{ var buildCmd = &cobra.Command{
@@ -16,7 +20,15 @@ var buildCmd = &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel) 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) return hrp.BuildPlugin(args[0], output)
}, },
} }

View File

@@ -2,10 +2,13 @@ package ios
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/spf13/cobra" "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/gidevice"
) )
@@ -19,7 +22,16 @@ var listAppsCmd = &cobra.Command{
Use: "apps", Use: "apps",
Short: "List all iOS installed apps", Short: "List all iOS installed apps",
PersistentPreRun: func(cmd *cobra.Command, args []string) {}, 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) device, err := getDevice(udid)
if err != nil { if err != nil {
return err return err

View File

@@ -4,10 +4,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "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/gidevice"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
@@ -69,7 +72,16 @@ var listDevicesCmd = &cobra.Command{
Use: "devices", Use: "devices",
Short: "List all iOS devices", Short: "List all iOS devices",
PersistentPreRun: func(cmd *cobra.Command, args []string) {}, 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) devices, err := uixt.GetIOSDevices(udid)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)

View File

@@ -5,18 +5,29 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
// mountCmd represents the mount command // mountCmd represents the mount command
var mountCmd = &cobra.Command{ var mountCmd = &cobra.Command{
Use: "mount", Use: "mount",
Short: "A brief description of your command", 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) device, err := getDevice(udid)
if err != nil { if err != nil {
return err return err

View File

@@ -3,6 +3,7 @@ package ios
import ( import (
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@@ -11,13 +12,23 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
var pcapCmd = &cobra.Command{ var pcapCmd = &cobra.Command{
Use: "pcap", Use: "pcap",
Short: "capture ios network packets", 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{} pcapOptions := []uixt.IOSPcapOption{}
if pid > 0 { if pid > 0 {
pcapOptions = append(pcapOptions, uixt.WithIOSPcapPID(pid)) pcapOptions = append(pcapOptions, uixt.WithIOSPcapPID(pid))

View File

@@ -3,6 +3,7 @@ package ios
import ( import (
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@@ -11,13 +12,23 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
var perfCmd = &cobra.Command{ var perfCmd = &cobra.Command{
Use: "perf", Use: "perf",
Short: "capture ios performance data (cpu,mem,disk,net,fps,etc.)", 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{} perfOptions := []uixt.IOSPerfOption{}
for _, p := range indicators { for _, p := range indicators {
switch p { switch p {

View File

@@ -2,17 +2,29 @@ package ios
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
var psCmd = &cobra.Command{ var psCmd = &cobra.Command{
Use: "ps", Use: "ps",
Short: "show running processes", Short: "show running processes",
PersistentPreRun: func(cmd *cobra.Command, args []string) {}, 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) device, err := getDevice(udid)
if err != nil { if err != nil {
return err return err

View File

@@ -2,15 +2,28 @@ package ios
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
var rebootCmd = &cobra.Command{ var rebootCmd = &cobra.Command{
Use: "reboot", Use: "reboot",
Short: "reboot or shutdown ios device", Short: "reboot or shutdown ios device",
PersistentPreRun: func(cmd *cobra.Command, args []string) {}, 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) device, err := getDevice(udid)
if err != nil { if err != nil {
return err return err

View File

@@ -4,17 +4,30 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
var xctestCmd = &cobra.Command{ var xctestCmd = &cobra.Command{
Use: "xctest", Use: "xctest",
Short: "run 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 == "" { if bundleID == "" {
return fmt.Errorf("bundleID is required") return fmt.Errorf("bundleID is required")
} }

View File

@@ -2,12 +2,15 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/pytest" "github.com/httprunner/httprunner/v4/hrp/internal/pytest"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version" "github.com/httprunner/httprunner/v4/hrp/internal/version"
) )
@@ -19,11 +22,20 @@ var pytestCmd = &cobra.Command{
setLogLevel(logLevel) setLogLevel(logLevel)
}, },
DisableFlagParsing: true, // allow to pass any args to pytest 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{ packages := []string{
fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion),
} }
_, err := myexec.EnsurePython3Venv(venv, packages...) _, err = myexec.EnsurePython3Venv(venv, packages...)
if err != nil { if err != nil {
log.Error().Err(err).Msg("python3 venv is not ready") log.Error().Err(err).Msg("python3 venv is not ready")
return err return err

View File

@@ -1,8 +1,12 @@
package cmd package cmd
import ( import (
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/wiki" "github.com/httprunner/httprunner/v4/hrp/internal/wiki"
) )
@@ -13,7 +17,15 @@ var wikiCmd = &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel) 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() 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 { func EqualLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected) length, err := convertInt(expected)
if err != nil { 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)
return assert.Len(t, actual, length, msgAndArgs...) 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 { func GreaterThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected) length, err := convertInt(expected)
if err != nil { 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) ok, l := getLen(actual)
if !ok { 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 { 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 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 { func GreaterOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected) length, err := convertInt(expected)
if err != nil { 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) ok, l := getLen(actual)
if !ok { 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 { 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 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 { func LessThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected) length, err := convertInt(expected)
if err != nil { 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) ok, l := getLen(actual)
if !ok { 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 { 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 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 { func LessOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected) length, err := convertInt(expected)
if err != nil { 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) ok, l := getLen(actual)
if !ok { 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 { 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 return true
} }

View File

@@ -2,15 +2,9 @@ package pytest
import ( import (
"github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
func RunPytest(args []string) error { func RunPytest(args []string) error {
sdk.SendEvent(sdk.EventTracking{
Category: "RunAPITests",
Action: "hrp pytest",
})
args = append([]string{"run"}, args...) args = append([]string{"run"}, args...)
return myexec.ExecPython3Command("httprunner", args...) return myexec.ExecPython3Command("httprunner", args...)
} }

View File

@@ -12,6 +12,7 @@ func TestGenDemoExamples(t *testing.T) {
t.Fatal() t.Fatal()
} }
// FIXME
dir = "../../../examples/demo-with-py-plugin" dir = "../../../examples/demo-with-py-plugin"
venv := filepath.Join(dir, ".venv") venv := filepath.Join(dir, ".venv")
err = CreateScaffold(dir, Py, venv, true) 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 { func CreateScaffold(projectName string, pluginType PluginType, venv string, force bool) error {
// report event // report GA event
sdk.SendEvent(sdk.EventTracking{ startTime := time.Now()
Category: "Scaffold", defer func() {
Action: "hrp startproject", sdk.SendGA4Event("hrp_startproject", map[string]interface{}{
}) "pluginType": string(pluginType),
"force": force,
"engagement_time_msec": time.Since(startTime).Milliseconds(),
})
}()
log.Info(). log.Info().
Str("projectName", projectName). Str("projectName", projectName).

View File

@@ -38,7 +38,7 @@
{ {
"check": "body.url", "check": "body.url",
"assert": "equals", "assert": "equals",
"expect": "https://postman-echo.com/post", "expect": "https://postman-echo.com/post/",
"msg": "assert response body url" "msg": "assert response body url"
} }
] ]

View File

@@ -26,5 +26,5 @@ validate:
msg: assert response body json msg: assert response body json
- check: body.url - check: body.url
assert: equals assert: equals
expect: https://postman-echo.com/post expect: https://postman-echo.com/post/
msg: assert response body url msg: assert response body url

View File

@@ -38,7 +38,7 @@
{ {
"check": "body.url", "check": "body.url",
"assert": "equals", "assert": "equals",
"expect": "https://postman-echo.com/put", "expect": "https://postman-echo.com/put/",
"msg": "assert response body url" "msg": "assert response body url"
} }
] ]

View File

@@ -26,5 +26,5 @@ validate:
msg: assert response body json msg: assert response body json
- check: body.url - check: body.url
assert: equals assert: equals
expect: https://postman-echo.com/put expect: https://postman-echo.com/put/
msg: assert response body url 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 package main
import ( 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 ( import (
"fmt" "fmt"
"github.com/denisbrodbeck/machineid"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/rs/zerolog/log" "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/env"
"github.com/httprunner/httprunner/v4/hrp/internal/version" "github.com/httprunner/httprunner/v4/hrp/internal/version"
) )
const ( 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() { func init() {
// init GA client
clientID, err := machineid.ProtectedID("hrp")
if err != nil {
clientID = uuid.NewV1().String()
}
gaClient = NewGAClient(trackingID, clientID)
// init sentry sdk // init sentry sdk
if env.DISABLE_SENTRY == "true" { if env.DISABLE_SENTRY == "true" {
return return
} }
err = sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
Dsn: sentryDSN, Dsn: sentryDSN,
Release: fmt.Sprintf("httprunner@%s", version.VERSION), Release: fmt.Sprintf("httprunner@%s", version.VERSION),
AttachStacktrace: true, AttachStacktrace: true,
@@ -43,15 +31,7 @@ func init() {
sentry.ConfigureScope(func(scope *sentry.Scope) { sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetLevel(sentry.LevelError) scope.SetLevel(sentry.LevelError)
scope.SetUser(sentry.User{ 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 var VERSION string
// httprunner python version // 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/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
func OpenWiki() error { func OpenWiki() error {
sdk.SendEvent(sdk.EventTracking{
Category: "OpenWiki",
Action: "hrp wiki",
})
log.Info().Msgf("%s https://httprunner.com", openCmd) log.Info().Msgf("%s https://httprunner.com", openCmd)
return myexec.RunCommand(openCmd, "https://httprunner.com") return myexec.RunCommand(openCmd, "https://httprunner.com")
} }

View File

@@ -27,8 +27,10 @@ const (
/* /*
[ [
{"username": "test1", "password": "111111"}, {"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"}, {"username": "test2", "password": "222222"},
] ]
*/ */
type Parameters []map[string]interface{} type Parameters []map[string]interface{}
@@ -205,36 +207,38 @@ func genCartesianProduct(multiParameters []Parameters) Parameters {
return cartesianProduct return cartesianProduct
} }
/* loadParameters loads parameters from multiple sources. /*
loadParameters loads parameters from multiple sources.
parameter value may be in three types: parameter value may be in three types:
(1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"] (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
(2) call built-in parameterize function, "${parameterize(account.csv)}" (2) call built-in parameterize function, "${parameterize(account.csv)}"
(3) call custom function in debugtalk.py, "${gen_app_version()}" (3) call custom function in debugtalk.py, "${gen_app_version()}"
configParameters = { configParameters = {
"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1 "user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1
"username-password": "${parameterize(account.csv)}", // case 2 "username-password": "${parameterize(account.csv)}", // case 2
"app_version": "${gen_app_version()}", // case 3 "app_version": "${gen_app_version()}", // case 3
} }
=> =>
{ {
"user_agent": [ "user_agent": [
{"user_agent": "iOS/10.1"}, {"user_agent": "iOS/10.1"},
{"user_agent": "iOS/10.2"}, {"user_agent": "iOS/10.2"},
{"user_agent": "iOS/10.3"}, {"user_agent": "iOS/10.3"},
], ],
"username-password": [ "username-password": [
{"username": "test1", "password": "111111"}, {"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"}, {"username": "test2", "password": "222222"},
], ],
"app_version": [ "app_version": [
{"app_version": "1.0.0"}, {"app_version": "1.0.0"},
{"app_version": "1.0.1"}, {"app_version": "1.0.1"},
] ]
} }
*/ */
func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) ( func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
map[string]Parameters, error) { map[string]Parameters, error) {
@@ -296,19 +300,23 @@ func (p *Parser) loadParameters(configParameters map[string]interface{}, variabl
return parsedParameters, nil return parsedParameters, nil
} }
/* convert parameters to standard format /*
convert parameters to standard format
key and parametersRawList may be in three types: key and parametersRawList may be in three types:
case 1: case 1:
key = "user_agent" key = "user_agent"
parametersRawList = ["iOS/10.1", "iOS/10.2"] parametersRawList = ["iOS/10.1", "iOS/10.2"]
case 2: case 2:
key = "username-password" key = "username-password"
parametersRawList = [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}] parametersRawList = [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
case 3: case 3:
key = "username-password" key = "username-password"
parametersRawList = [["test1", "111111"], ["test2", "222222"]] parametersRawList = [["test1", "111111"], ["test2", "222222"]]
*/ */

View File

@@ -28,32 +28,48 @@ type Parser struct {
plugin funplugin.IPlugin // plugin is used to call functions 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) uStep, err := url.Parse(stepURL)
if err != nil { if err != nil {
log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse url failed") 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 // step url is absolute url
if uStep.Host != "" { if uStep.Host != "" {
return stepURL return uStep
} }
// step url is relative, based on base url // step url is relative, based on base url
uConfig, err := url.Parse(baseURL) uConfig, err := url.Parse(baseURL)
if err != nil { if err != nil {
log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed") log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed")
return "" return
} }
// merge url // merge url
uStep.Scheme = uConfig.Scheme uStep.Scheme = uConfig.Scheme
uStep.Host = uConfig.Host uStep.Host = uConfig.Host
uStep.Path = path.Join(uConfig.Path, uStep.Path) uStep.Path = path.Join(uConfig.Path, uStep.Path)
return uStep
// base url missed
return uStep.String()
} }
func (p *Parser) ParseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) { func (p *Parser) ParseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) {

View File

@@ -1,6 +1,7 @@
package hrp package hrp
import ( import (
"net/url"
"sort" "sort"
"testing" "testing"
"time" "time"
@@ -9,60 +10,72 @@ import (
) )
func TestBuildURL(t *testing.T) { func TestBuildURL(t *testing.T) {
var url string var preparedURL *url.URL
url = buildURL("https://postman-echo.com", "/get") preparedURL = buildURL("https://postman-echo.com", "/get", nil)
if !assert.Equal(t, url, "https://postman-echo.com/get") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
t.Fatal() t.Fatal()
} }
url = buildURL("https://postman-echo.com", "get") preparedURL = buildURL("https://postman-echo.com", "get", nil)
if !assert.Equal(t, url, "https://postman-echo.com/get") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
t.Fatal() t.Fatal()
} }
url = buildURL("https://postman-echo.com/", "/get") preparedURL = buildURL("https://postman-echo.com/", "/get", nil)
if !assert.Equal(t, url, "https://postman-echo.com/get") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
t.Fatal() t.Fatal()
} }
url = buildURL("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, url, "https://postman-echo.com/abc/get?a=1&b=2") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fatal() t.Fatal()
} }
url = buildURL("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, url, "https://postman-echo.com/abc/get?a=1&b=2") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fatal() t.Fatal()
} }
// omit query string in base url // omit query string in base url
url = buildURL("https://postman-echo.com/abc?x=6&y=9", "/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, url, "https://postman-echo.com/abc/get?a=1&b=2") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fatal() t.Fatal()
} }
url = buildURL("", "https://postman-echo.com/get") preparedURL = buildURL("", "https://postman-echo.com/get", nil)
if !assert.Equal(t, url, "https://postman-echo.com/get") { if !assert.Equal(t, preparedURL.String(), "https://postman-echo.com/get/") {
t.Fatal() t.Fatal()
} }
// notice: step request url > config base url // notice: step request url > config base url
url = buildURL("https://postman-echo.com", "https://httpbin.org/get") preparedURL = buildURL("https://postman-echo.com", "https://httpbin.org/get", nil)
if !assert.Equal(t, url, "https://httpbin.org/get") { if !assert.Equal(t, preparedURL.String(), "https://httpbin.org/get/") {
t.Fatal() t.Fatal()
} }
// websocket url // websocket url
url = buildURL("wss://ws.postman-echo.com/raw", "") preparedURL = buildURL("wss://ws.postman-echo.com/raw", "", nil)
if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") { if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
t.Fatal() t.Fatal()
} }
url = buildURL("wss://ws.postman-echo.com", "/raw") preparedURL = buildURL("wss://ws.postman-echo.com", "/raw", nil)
if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") { if !assert.Equal(t, preparedURL.String(), "wss://ws.postman-echo.com/raw/") {
t.Fatal() t.Fatal()
} }
url = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events") preparedURL = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events", nil)
if !assert.Equal(t, url, "ws://echo.websocket.events") { 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() t.Fatal()
} }
} }

View File

@@ -198,7 +198,7 @@ func TestSpawnWorkersWithManyTasks(t *testing.T) {
const numToSpawn int64 = 20 const numToSpawn int64 = 20
go runner.spawnWorkers(numToSpawn, float64(numToSpawn), runner.stopChan, runner.spawnComplete) go runner.spawnWorkers(numToSpawn, float64(numToSpawn), runner.stopChan, runner.spawnComplete)
time.Sleep(3 * time.Second) time.Sleep(5 * time.Second)
currentClients := runner.controller.getCurrentClientsNum() currentClients := runner.controller.getCurrentClientsNum()
@@ -210,28 +210,29 @@ func TestSpawnWorkersWithManyTasks(t *testing.T) {
lock.Unlock() lock.Unlock()
total := hundreds + tens + ones 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, total > 111)
assert.True(t, ones > 1) assert.True(t, ones > 1)
actPercentage := float64(ones) / float64(total) actPercentage := float64(ones) / float64(total)
expectedPercentage := 1.0 / 111.0 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) t.Errorf("Unexpected percentage of ones task: exp %v, act %v", expectedPercentage, actPercentage)
} }
assert.True(t, tens > 10) assert.True(t, tens > 10)
actPercentage = float64(tens) / float64(total) actPercentage = float64(tens) / float64(total)
expectedPercentage = 10.0 / 111.0 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) t.Errorf("Unexpected percentage of tens task: exp %v, act %v", expectedPercentage, actPercentage)
} }
assert.True(t, hundreds > 100) assert.True(t, hundreds > 100)
actPercentage = float64(hundreds) / float64(total) actPercentage = float64(hundreds) / float64(total)
expectedPercentage = 100.0 / 111.0 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) 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() go runner.start()
// wait for spawning goroutines // wait for spawning goroutines
time.Sleep(2 * time.Second) time.Sleep(5 * time.Second)
if runner.controller.getCurrentClientsNum() != 10 { if runner.controller.getCurrentClientsNum() != 10 {
t.Error("Number of goroutines mismatches, expected: 10, current count", runner.controller.getCurrentClientsNum()) 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) t.Error("Runner should send spawning_complete message when spawning completed, got", msg.Type)
} }
go runner.stop() go runner.stop()
close(runner.doneChan)
runner.onQuiting() runner.onQuiting()
msg = <-runner.client.sendChannel() msg = <-runner.client.sendChannel()
@@ -384,7 +384,7 @@ func TestOnMessage(t *testing.T) {
} }
// spawn complete and running // spawn complete and running
time.Sleep(2 * time.Second) time.Sleep(5 * time.Second)
if runner.controller.getCurrentClientsNum() != 10 { if runner.controller.getCurrentClientsNum() != 10 {
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum()) 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 // spawn complete and running
time.Sleep(3 * time.Second) time.Sleep(5 * time.Second)
if runner.controller.getCurrentClientsNum() != 10 { if runner.controller.getCurrentClientsNum() != 10 {
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum()) t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum())
} }

View File

@@ -2,8 +2,8 @@ package convert
import ( import (
_ "embed" _ "embed"
"fmt"
"path/filepath" "path/filepath"
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -139,19 +139,25 @@ func (c *TCaseConverter) loadCase(casePath string, fromType FromType) error {
return err return err
} }
func (c *TCaseConverter) Convert(casePath string, fromType FromType, outputType OutputType) error { func (c *TCaseConverter) Convert(casePath string, fromType FromType, outputType OutputType) (err error) {
// report event // report GA event
sdk.SendEvent(sdk.EventTracking{ startTime := time.Now()
Category: "ConvertTests", defer func() {
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()), 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). log.Info().Str("path", casePath).
Str("fromType", fromType.String()). Str("fromType", fromType.String()).
Str("outputType", outputType.String()). Str("outputType", outputType.String()).
Msg("convert testcase") Msg("convert testcase")
// load source file // load source file
err := c.loadCase(casePath, fromType) err = c.loadCase(casePath, fromType)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -555,8 +555,9 @@ func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
// 2, the `className(String)` matches the image // 2, the `className(String)` matches the image
// widget class, and `enabled(boolean)` is true. // widget class, and `enabled(boolean)` is true.
// The code would look like this: // 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 { func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper {
s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance)) s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance))
return s return s

View File

@@ -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 // 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 // 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. // 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 { func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) 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 // WithMaxTypingFrequency
// Defaults to `60`. //
// Defaults to `60`.
func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities { func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities {
if n <= 0 { if n <= 0 {
n = 60 n = 60
@@ -41,21 +42,24 @@ func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities {
} }
// WithWaitForIdleTimeout // WithWaitForIdleTimeout
// Defaults to `10` //
// Defaults to `10`
func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities { func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities {
caps["waitForIdleTimeout"] = second caps["waitForIdleTimeout"] = second
return caps return caps
} }
// WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility // WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility
// Defaults to `false` //
// Defaults to `false`
func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities { func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities {
caps["shouldUseTestManagerForVisibilityDetection"] = b caps["shouldUseTestManagerForVisibilityDetection"] = b
return caps return caps
} }
// WithShouldUseCompactResponses If set to YES will use compact (standards-compliant) & faster responses // 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 { func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities {
caps["shouldUseCompactResponses"] = b caps["shouldUseCompactResponses"] = b
return caps return caps
@@ -63,28 +67,32 @@ func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities {
// WithElementResponseAttributes If shouldUseCompactResponses == NO, // WithElementResponseAttributes If shouldUseCompactResponses == NO,
// is the comma-separated list of fields to return with each element. // 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 { func (caps Capabilities) WithElementResponseAttributes(s string) Capabilities {
caps["elementResponseAttributes"] = s caps["elementResponseAttributes"] = s
return caps return caps
} }
// WithShouldUseSingletonTestManager // WithShouldUseSingletonTestManager
// Defaults to `true` //
// Defaults to `true`
func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities { func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities {
caps["shouldUseSingletonTestManager"] = b caps["shouldUseSingletonTestManager"] = b
return caps return caps
} }
// WithDisableAutomaticScreenshots // WithDisableAutomaticScreenshots
// Defaults to `true` //
// Defaults to `true`
func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities { func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities {
caps["disableAutomaticScreenshots"] = b caps["disableAutomaticScreenshots"] = b
return caps return caps
} }
// WithShouldTerminateApp // WithShouldTerminateApp
// Defaults to `true` //
// Defaults to `true`
func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities { func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities {
caps["shouldTerminateApp"] = b caps["shouldTerminateApp"] = b
return caps return caps
@@ -376,7 +384,8 @@ func (opt SourceOption) WithFormatAsDescription() SourceOption {
} }
// WithScope Allows to provide XML scope. // WithScope Allows to provide XML scope.
// only `xml` is supported. //
// only `xml` is supported.
func (opt SourceOption) WithScope(scope string) SourceOption { func (opt SourceOption) WithScope(scope string) SourceOption {
if vFormat, ok := opt["format"]; ok && vFormat != "xml" { if vFormat, ok := opt["format"]; ok && vFormat != "xml" {
return opt return opt

View File

@@ -176,11 +176,12 @@ func newVEDEMImageService(actions ...string) (*veDEMImageService, error) {
// veDEMImageService implements IImageService interface // veDEMImageService implements IImageService interface
// actions: // actions:
// ocr - get ocr texts //
// upload - get image uploaded url // ocr - get ocr texts
// liveType - get live type // upload - get image uploaded url
// popup - get popup windows // liveType - get live type
// close - get close popup // popup - get popup windows
// close - get close popup
type veDEMImageService struct { type veDEMImageService struct {
actions []string actions []string
} }
@@ -230,10 +231,6 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer) (
req.Header.Add("Agw-Auth-Content", signToken) req.Header.Add("Agw-Auth-Content", signToken)
req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) 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 var resp *http.Response
// retry 3 times // retry 3 times
for i := 1; i <= 3; i++ { 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) pluginMap.Store(pluginPath, plugin)
// report event for initializing plugin // report event for initializing plugin
event := sdk.EventTracking{ params := map[string]interface{}{
Category: "InitPlugin", "type": plugin.Type(),
Action: fmt.Sprintf("Init %s plugin", plugin.Type()), "result": "success",
Value: 0, // success
} }
if err != nil { if err != nil {
event.Value = 1 // failed params["result"] = "failed"
} }
go sdk.SendEvent(event) go sdk.SendGA4Event("init_plugin", params)
return return
} }

View File

@@ -194,14 +194,16 @@ func (r *HRPRunner) GenHTMLReport() *HRPRunner {
// Run starts to execute one or multiple testcases. // Run starts to execute one or multiple testcases.
func (r *HRPRunner) Run(testcases ...ITestCase) (err error) { func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
log.Info().Str("hrp_version", version.VERSION).Msg("start running") log.Info().Str("hrp_version", version.VERSION).Msg("start running")
event := sdk.EventTracking{
Category: "RunAPITests", startTime := time.Now()
Action: "hrp run", defer func() {
} // report run event
// report start event sdk.SendGA4Event("hrp_run", map[string]interface{}{
go sdk.SendEvent(event) "success": err == nil,
// report execution timing event "engagement_time_msec": time.Since(startTime).Milliseconds(),
defer sdk.SendEvent(event.StartTiming("execution")) })
}()
// record execution data to summary // record execution data to summary
s := newOutSummary() s := newOutSummary()
@@ -511,6 +513,9 @@ func (r *SessionRunner) inheritConnection(src *SessionRunner) {
// Start runs the test steps in sequential order. // Start runs the test steps in sequential order.
// givenVars is used for data driven // givenVars is used for data driven
func (r *SessionRunner) Start(givenVars map[string]interface{}) error { func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
// report GA event
sdk.SendGA4Event("hrp_session_runner_start", nil)
config := r.caseRunner.testCase.Config config := r.caseRunner.testCase.Config
log.Info().Str("testcase", config.Name).Msg("run testcase start") log.Info().Str("testcase", config.Name).Msg("run testcase start")

View File

@@ -7,9 +7,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
) )
func buildHashicorpGoPlugin() { func buildHashicorpGoPlugin() {
@@ -63,31 +64,26 @@ func assertRunTestCases(t *testing.T) {
refCase := TestCasePath(demoTestCaseWithPluginJSONPath) refCase := TestCasePath(demoTestCaseWithPluginJSONPath)
testcase1 := &TestCase{ testcase1 := &TestCase{
Config: NewConfig("TestCase1"). Config: NewConfig("TestCase1").
SetBaseURL("https://httpbin.org"), SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("testcase1-step1"). NewStep("testcase1-step1").
GET("/headers"). GET("/headers").
Validate(). Validate().
AssertEqual("status_code", 200, "check status code"). 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-step2"). NewStep("testcase1-step2").CallRefCase(
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(
&TestCase{ &TestCase{
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://httpbin.org"), Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("ip"). NewStep("ip").
GET("/ip"). GET("/ip").
Validate(). Validate().
AssertEqual("status_code", 200, "check status code"). 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{ testcase2 := &TestCase{

View File

@@ -7,6 +7,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
@@ -564,6 +565,11 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
mobileStep = step.Android mobileStep = step.Android
} }
// report GA event
sdk.SendGA4Event("hrp_run_ui", map[string]interface{}{
"osType": osType,
})
stepResult = &StepResult{ stepResult = &StepResult{
Name: step.Name, Name: step.Name,
StepType: StepType(osType), StepType: StepType(osType),

View File

@@ -149,9 +149,8 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{})
} }
var baseURL string var baseURL string
if stepVariables["base_url"] != nil { if stepVariables["base_url"] != nil {
baseURL = stepVariables["base_url"].(string) baseURL, _ = stepVariables["base_url"].(string)
} }
rawUrl := buildURL(baseURL, convertString(requestUrl))
// prepare request params // prepare request params
var queryParams url.Values var queryParams url.Values
@@ -161,35 +160,24 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{})
return errors.Wrap(err, "parse request params failed") return errors.Wrap(err, "parse request params failed")
} }
parsedParams := params.(map[string]interface{}) parsedParams := params.(map[string]interface{})
r.requestMap["params"] = parsedParams
if len(parsedParams) > 0 { if len(parsedParams) > 0 {
queryParams = make(url.Values) queryParams = make(url.Values)
for k, v := range parsedParams { for k, v := range parsedParams {
queryParams.Add(k, convertString(v)) queryParams.Add(k, convertString(v))
} }
} }
}
if queryParams != nil { // request params has been appended to url, thus delete it here
// append params to url delete(r.requestMap, "params")
paramStr := queryParams.Encode()
if strings.IndexByte(rawUrl, '?') == -1 {
rawUrl = rawUrl + "?" + paramStr
} else {
rawUrl = rawUrl + "&" + paramStr
}
} }
// prepare url // prepare url
u, err := url.Parse(rawUrl) preparedURL := buildURL(baseURL, convertString(requestUrl), queryParams)
if err != nil { r.req.URL = preparedURL
return errors.Wrap(err, "parse url failed") r.req.Host = preparedURL.Host
}
r.req.URL = u
r.req.Host = u.Host
// update url // update url
r.requestMap["url"] = u.String() r.requestMap["url"] = preparedURL.String()
return nil 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 // add request object to step variables, could be used in setup hooks
stepVariables["hrp_step_name"] = step.Name stepVariables["hrp_step_name"] = step.Name
stepVariables["hrp_step_request"] = rb.requestMap stepVariables["hrp_step_request"] = rb.requestMap
stepVariables["request"] = rb.requestMap stepVariables["request"] = rb.requestMap // setup hooks compatible with v3
// deal with setup hooks // deal with setup hooks
for _, setupHook := range step.SetupHooks { for _, setupHook := range step.SetupHooks {
req, err := parser.Parse(setupHook, stepVariables) _, err := parser.Parse(setupHook, stepVariables)
if err != nil { if err != nil {
return stepResult, errors.Wrap(err, "run setup hooks failed") 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 // log & print request
@@ -451,15 +410,10 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
// deal with teardown hooks // deal with teardown hooks
for _, teardownHook := range step.TeardownHooks { for _, teardownHook := range step.TeardownHooks {
res, err := parser.Parse(teardownHook, stepVariables) _, err := parser.Parse(teardownHook, stepVariables)
if err != nil { if err != nil {
return stepResult, errors.Wrap(err, "run teardown hooks failed") 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 sessionData.ReqResps.Request = rb.requestMap

View File

@@ -99,7 +99,7 @@ func TestRunRequestStatOn(t *testing.T) {
if !assert.Greater(t, stat["TLSHandshake"], int64(0)) { if !assert.Greater(t, stat["TLSHandshake"], int64(0)) {
t.Fatal() t.Fatal()
} }
if !assert.Greater(t, stat["ServerProcessing"], int64(1)) { if !assert.Greater(t, stat["ServerProcessing"], int64(0)) {
t.Fatal() t.Fatal()
} }
if !assert.GreaterOrEqual(t, stat["ContentTransfer"], int64(0)) { if !assert.GreaterOrEqual(t, stat["ContentTransfer"], int64(0)) {
@@ -165,7 +165,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase1 := &TestCase{ testcase1 := &TestCase{
Config: NewConfig("TestCase1"). Config: NewConfig("TestCase1").
SetRequestTimeout(10). // set global timeout to 10s SetRequestTimeout(10). // set global timeout to 10s
SetBaseURL("https://httpbin.org"), SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("step1"). NewStep("step1").
GET("/delay/1"). GET("/delay/1").
@@ -180,11 +180,11 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase2 := &TestCase{ testcase2 := &TestCase{
Config: NewConfig("TestCase2"). Config: NewConfig("TestCase2").
SetRequestTimeout(10). // set global timeout to 10s SetRequestTimeout(5). // set global timeout to 10s
SetBaseURL("https://httpbin.org"), SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("step1"). NewStep("step1").
GET("/delay/11"). GET("/delay/10").
Validate(). Validate().
AssertEqual("status_code", 200, "check status code"), AssertEqual("status_code", 200, "check status code"),
}, },
@@ -198,7 +198,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase3 := &TestCase{ testcase3 := &TestCase{
Config: NewConfig("TestCase3"). Config: NewConfig("TestCase3").
SetRequestTimeout(10). SetRequestTimeout(10).
SetBaseURL("https://httpbin.org"), SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{ TestSteps: []IStep{
NewStep("step2"). NewStep("step2").
GET("/delay/11"). GET("/delay/11").

View File

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

View File

@@ -1,4 +1,4 @@
__version__ = "v4.3.0" __version__ = "v4.3.5"
__description__ = "One-stop solution for HTTP(S) testing." __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 import __description__, __version__
from httprunner.compat import ensure_cli_args from httprunner.compat import ensure_cli_args
from httprunner.make import init_make_parser, main_make 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() init_sentry_sdk()
@@ -22,7 +22,7 @@ def init_parser_run(subparsers):
def main_run(extra_args) -> enum.IntEnum: def main_run(extra_args) -> enum.IntEnum:
ga_client.track_event("RunAPITests", "hrun") ga4_client.send_event("hrun")
# keep compatibility with v2 # keep compatibility with v2
extra_args = ensure_cli_args(extra_args) extra_args = ensure_cli_args(extra_args)

View File

@@ -1,6 +1,7 @@
import unittest import unittest
from httprunner.client import HttpSession from httprunner.client import HttpSession
from httprunner.utils import HTTP_BIN_URL
class TestHttpSession(unittest.TestCase): class TestHttpSession(unittest.TestCase):
@@ -8,15 +9,15 @@ class TestHttpSession(unittest.TestCase):
self.session = HttpSession() self.session = HttpSession()
def test_request_http(self): 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 address = self.session.data.address
self.assertGreater(len(address.server_ip), 0) 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(len(address.client_ip), 0)
self.assertGreater(address.client_port, 10000) self.assertGreater(address.client_port, 10000)
def test_request_https(self): 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 address = self.session.data.address
self.assertGreater(len(address.server_ip), 0) self.assertGreater(len(address.server_ip), 0)
self.assertEqual(address.server_port, 443) self.assertEqual(address.server_port, 443)
@@ -26,7 +27,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_allow_redirects(self): def test_request_http_allow_redirects(self):
self.session.request( self.session.request(
"get", "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, allow_redirects=True,
) )
address = self.session.data.address address = self.session.data.address
@@ -38,7 +39,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_https_allow_redirects(self): def test_request_https_allow_redirects(self):
self.session.request( self.session.request(
"get", "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, allow_redirects=True,
) )
address = self.session.data.address address = self.session.data.address
@@ -50,7 +51,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_not_allow_redirects(self): def test_request_http_not_allow_redirects(self):
self.session.request( self.session.request(
"get", "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, allow_redirects=False,
) )
address = self.session.data.address address = self.session.data.address
@@ -62,7 +63,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_https_not_allow_redirects(self): def test_request_https_not_allow_redirects(self):
self.session.request( self.session.request(
"get", "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, allow_redirects=False,
) )
address = self.session.data.address address = self.session.data.address

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,12 @@ import json
import os import os
import os.path import os.path
import platform import platform
import random
import sys import sys
import time
import uuid import uuid
from multiprocessing import Queue from multiprocessing import Queue
from typing import Any, Dict, List, Text from typing import Any, Dict, List
import requests import requests
import sentry_sdk import sentry_sdk
@@ -18,6 +20,25 @@ from httprunner import __version__, exceptions
from httprunner.models import VariablesMapping 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(): def init_sentry_sdk():
if os.getenv("DISABLE_SENTRY") == "true": if os.getenv("DISABLE_SENTRY") == "true":
return return
@@ -30,62 +51,79 @@ def init_sentry_sdk():
scope.set_user({"id": uuid.getnode()}) 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 def __init__(
report_url = "https://www.google-analytics.com/collect" self, measurement_id: str, api_secret: str, debug: bool = False
report_debug_url = ( ) -> None:
"https://www.google-analytics.com/debug/collect" # used for debug
)
def __init__(self, tracking_id: Text):
self.http_client = requests.Session() self.http_client = requests.Session()
self.label = f"v{__version__}"
self.common_params = { self.debug = debug
"v": self.version, if debug:
"tid": tracking_id, # Tracking ID / Property ID, XX-XXXXXXX-X uri = "https://www.google-analytics.com/debug/mp/collect"
"cid": uuid.getnode(), # Anonymous Client ID else:
"ua": f"HttpRunner/{__version__}", 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 # do not send GA events in CI environment
self.__is_ci = os.getenv("DISABLE_GA") == "true" 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: if self.__is_ci:
return return
data = { event_params = event_params or {}
"t": "event", # Event hit type = event event_params.update(self.common_event_params)
"ec": category, # Required. Event Category. event = {
"ea": action, # Required. Event Action. "name": name,
"el": self.label, # Optional. Event label, used as version. "params": event_params,
"ev": value, # Optional. Event value, must be non-negative integer
} }
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): payload = {
if self.__is_ci: "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 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: try:
self.http_client.post(self.report_url, data=data, timeout=5) resp_body = resp.json()
except Exception: # ProxyError, SSLError, ConnectionError logger.debug(
"get GA4 validation response, "
f"status code: {resp.status_code}, body: {resp_body}"
)
except Exception:
pass 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): def set_os_environ(variables_mapping):
@@ -219,16 +257,6 @@ def omit_long_data(body, omit_len=512):
return omitted_body + appendix_str 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 sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
def get_index_from_list(lst: List, item: Any): def get_index_from_list(lst: List, item: Any):
try: try:

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import toml import toml
from httprunner import __version__, loader, utils 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): class TestUtils(unittest.TestCase):
@@ -121,10 +121,10 @@ class TestUtils(unittest.TestCase):
def test_override_config_variables(self): def test_override_config_variables(self):
step_variables = {"base_url": "$base_url", "foo1": "bar1"} 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( self.assertEqual(
merge_variables(step_variables, config_variables), 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): def test_cartesian_product_one(self):
@@ -160,3 +160,12 @@ class TestUtils(unittest.TestCase):
pyproject = toml.loads(open(str(path)).read()) pyproject = toml.loads(open(str(path)).read())
pyproject_version = pyproject["tool"]["poetry"]["version"] pyproject_version = pyproject["tool"]["poetry"]["version"]
self.assertEqual(pyproject_version, __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] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "v4.3.0" version = "v4.3.5"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -30,8 +30,6 @@ include = ["docs/CHANGELOG.md"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
requests = "^2.22.0"
pyyaml = "^5.4.1"
pydantic = "~1.8" # >=1.8.0 <1.9.0 pydantic = "~1.8" # >=1.8.0 <1.9.0
loguru = "^0.4.1" loguru = "^0.4.1"
jmespath = "^0.9.5" jmespath = "^0.9.5"
@@ -40,7 +38,7 @@ pytest = "^7.1.1"
pytest-html = "^3.1.1" pytest-html = "^3.1.1"
sentry-sdk = "^0.14.4" sentry-sdk = "^0.14.4"
allure-pytest = {version = "^2.8.16", optional = true} 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} filetype = {version = "^1.0.7", optional = true}
Brotli = "^1.0.9" Brotli = "^1.0.9"
jinja2 = "^3.0.3" jinja2 = "^3.0.3"
@@ -50,6 +48,9 @@ pymysql = {version = "^1.0.2",optional = true}
cython = {version = "^0.29.28", optional = true} cython = {version = "^0.29.28", optional = true}
thriftpy2 = {version = "^0.4.14", optional = true} thriftpy2 = {version = "^0.4.14", optional = true}
thrift = {version = "^0.16.0", optional = true} thrift = {version = "^0.16.0", optional = true}
pyyaml = "^6.0.1"
requests = "^2.31.0"
urllib3 = "^1.26"
[tool.poetry.extras] [tool.poetry.extras]
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure