Merge pull request #1608 from httprunner/fix-android-ui

v4.3.4 refactor android UI

- feat: add video crawler for feed and live
- feat: cache screenshot ocr texts
- feat: set testcase and request timeout in seconds
- feat: catch interrupt signal
- feat: add new exit code MobileUILaunchAppError/InterruptError/TimeoutError/MobileUIActivityNotMatchError/MobileUIPopupError
- feat: find text with regex
- feat: add UI ocr tags to summary
- feat: check android device offline when running shell failed
- refactor: replace OCR APIs with image APIs
- refactor: FindText(s) returns OCRText(s)
- refactor: merge ActionOption with DataOption
- change: exit with AndroidShellExecError code for adb shell failure
- change: request vedem ocr with uploading image
This commit is contained in:
debugtalk
2023-05-31 20:56:56 +08:00
committed by GitHub
63 changed files with 2617 additions and 1849 deletions

View File

@@ -1,5 +1,23 @@
# Release History
## v4.3.4 (2023-05-31)
**go version**
- feat: add video crawler for feed and live
- feat: cache screenshot ocr texts
- feat: set testcase and request timeout in seconds
- feat: catch interrupt signal
- feat: add new exit code MobileUILaunchAppError/InterruptError/TimeoutError/MobileUIActivityNotMatchError/MobileUIPopupError
- feat: find text with regex
- feat: add UI ocr tags to summary
- feat: check android device offline when running shell failed
- refactor: replace OCR APIs with image APIs
- refactor: FindText(s) returns OCRText(s)
- refactor: merge ActionOption with DataOption
- change: exit with AndroidShellExecError code for adb shell failure
- change: request vedem ocr with uploading image
## v4.3.3 (2023-04-19)
**go version**
@@ -7,7 +25,7 @@
- feat: add `sleep_random` to sleep random seconds, with weight for multiple time ranges
- feat: input text with adb
- feat: add adb `screencap` sub command
- feat: add `IsAppInForeground` to check if the given package is in foreground
- feat: add `AssertAppInForeground` to check if the given package is in foreground
- feat: check if app is in foreground when step failed
- feat: add validator AssertAppInForeground and AssertAppNotInForeground
- feat: save screenshots of all steps including ocr and cv recognition process data
@@ -20,6 +38,8 @@
- fix: fast fail not closing the websocket connection
- fix #1467: failed to parse parameters with plugin functions
- fix #1549: avoid duplicate creating plugins
- fix #1547: generate html report failed for referenced testcases
- fix: setup hooks compatible with v3
## v4.3.2 (2022-12-26)

View File

@@ -31,14 +31,12 @@ Copyright 2017 debugtalk
* [hrp boom](hrp_boom.md) - run load test with boomer
* [hrp build](hrp_build.md) - build plugin for testing
* [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases
* [hrp curl](hrp_curl.md) - run integrated curl command
* [hrp convert](hrp_convert.md) - convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases
* [hrp dns](hrp_dns.md) - DNS resolution for different source and record types
* [hrp ping](hrp_ping.md) - run integrated ping command
* [hrp pytest](hrp_pytest.md) - run API test with pytest
* [hrp run](hrp_run.md) - run API test with go engine
* [hrp startproject](hrp_startproject.md) - create a scaffold project
* [hrp traceroute](hrp_traceroute.md) - run integrated traceroute command
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -53,6 +53,5 @@ hrp boom [flags]
### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution.
* [hrp boom curl](hrp_boom_curl.md) - run load test with curl command
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -28,4 +28,4 @@ hrp build $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -1,6 +1,6 @@
## hrp convert
convert to JSON/YAML/gotest/pytest testcases
convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases
```
hrp convert $path... [flags]
@@ -9,18 +9,21 @@ hrp convert $path... [flags]
### Options
```
--from-curl load from curl format
--from-har load from HAR format
--from-json load from json case format (default true)
--from-postman load from postman format
--from-yaml load from yaml case format
-h, --help help for convert
-d, --output-dir string specify output directory, default to the same dir with har file
-d, --output-dir string specify output directory
-p, --profile string specify profile path to override headers and cookies
--to-gotest convert to gotest scripts (TODO)
--to-json convert to JSON scripts (default)
--to-json convert to JSON case scripts (default true)
--to-pytest convert to pytest scripts
--to-yaml convert to YAML scripts
--to-yaml convert to YAML case scripts
```
### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution.
* [hrp convert curl](hrp_convert_curl.md) - convert curl command to httprunner testcase
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -26,4 +26,4 @@ hrp dns $url [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -20,4 +20,4 @@ hrp ping $url [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -21,19 +21,19 @@ hrp run $path... [flags]
### Options
```
-c, --continue-on-failure continue running next step when failure occurs
-g, --gen-html-report generate html report
-h, --help help for run
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
--log-plugin turn on plugin logging
--log-requests-off turn off request & response details logging
-p, --proxy-url string set proxy url
-s, --save-tests save tests summary
--case-timeout float32 set testcase timeout (seconds) (default 3600)
-c, --continue-on-failure continue running next step when failure occurs
-g, --gen-html-report generate html report
-h, --help help for run
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
--log-plugin turn on plugin logging
--log-requests-off turn off request & response details logging
-p, --proxy-url string set proxy url
-s, --save-tests save tests summary
```
### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution.
* [hrp run curl](hrp_run_curl.md) - run API test with curl command
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -21,4 +21,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -16,4 +16,4 @@ hrp wiki [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 21-Oct-2022
###### Auto generated by spf13/cobra on 31-May-2023

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-empty-project",
"create_time": "2022-10-21T21:54:56.252853+08:00",
"hrp_version": "v4.3.0"
"create_time": "2023-05-31T20:46:10.736189+08:00",
"hrp_version": "v4.3.4"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-go-plugin",
"create_time": "2022-10-21T21:52:38.979867+08:00",
"hrp_version": "v4.3.0"
"create_time": "2023-05-31T20:44:56.120736+08:00",
"hrp_version": "v4.3.4"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-py-plugin",
"create_time": "2022-10-21T21:52:39.555851+08:00",
"hrp_version": "v4.3.0"
"create_time": "2023-05-31T20:45:00.44921+08:00",
"hrp_version": "v4.3.4"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-without-plugin",
"create_time": "2022-10-21T21:54:56.136458+08:00",
"hrp_version": "v4.3.0"
"create_time": "2023-05-31T20:46:10.621009+08:00",
"hrp_version": "v4.3.4"
}

View File

@@ -45,7 +45,9 @@
{
"method": "tap_ocr",
"params": "我知道了",
"ignore_NotFoundError": true
"options": {
"ignore_NotFoundError": true
}
}
]
}
@@ -56,7 +58,8 @@
"actions": [
{
"method": "swipe",
"params": "up"
"params": "up",
"options": {}
},
{
"method": "sleep_random",
@@ -75,7 +78,8 @@
"actions": [
{
"method": "swipe",
"params": "up"
"params": "up",
"options": {}
},
{
"method": "sleep_random",
@@ -94,7 +98,8 @@
"actions": [
{
"method": "swipe",
"params": "up"
"params": "up",
"options": {}
},
{
"method": "sleep_random",
@@ -131,4 +136,4 @@
]
}
]
}
}

View File

@@ -45,7 +45,9 @@
{
"method": "tap_ocr",
"params": "我知道了",
"ignore_NotFoundError": true
"options": {
"ignore_NotFoundError": true
}
}
]
}
@@ -57,8 +59,10 @@
{
"method": "swipe_to_tap_text",
"params": "点击进入直播间",
"identifier": "进入直播间",
"max_retry_times": 10
"options": {
"identifier": "进入直播间",
"max_retry_times": 10
}
}
]
}
@@ -69,7 +73,10 @@
"actions": [
{
"method": "swipe",
"params": "up"
"params": "up",
"options": {
}
},
{
"method": "sleep_random",
@@ -93,11 +100,13 @@
{
"method": "swipe",
"params": "up",
"identifier": "第一次上划"
"options": {
"identifier": "第一次上划"
}
},
{
"method": "sleep",
"params": 10
"params": 5
},
{
"method": "screenshot"
@@ -105,11 +114,13 @@
{
"method": "swipe",
"params": "up",
"identifier": "第二次上划"
"options": {
"identifier": "第二次上划"
}
},
{
"method": "sleep",
"params": 10
"params": 5
},
{
"method": "screenshot"

View File

@@ -37,8 +37,8 @@ func TestAndroidLiveSwipeTest(t *testing.T) {
SleepRandom(0, 5, 0.6, 5, 10, 0.4),
hrp.NewStep("向上滑动,等待 10s").
Android().
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s截图保存
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s截图保存
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(5).ScreenShot(). // 上划 1 次,等待 5s截图保存
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(5).ScreenShot(), // 再上划 1 次,等待 5s截图保存
hrp.NewStep("exit").
Android().
AppTerminate("com.ss.android.ugc.aweme").

View File

@@ -0,0 +1,120 @@
{
"config": {
"name": "抓取抖音视频信息",
"variables": {
"device": "${ENV(SerialNumber)}"
},
"android": [
{
"serial": "$device"
}
]
},
"teststeps": [
{
"name": "滑动消费 feed 至少 10 个live 至少 3 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s",
"android": {
"actions": [
{
"method": "video_crawler",
"params": {
"app_package_name": "com.smile.gifmaker",
"feed": {
"sleep_random": [
0,
5,
0.7,
5,
10,
0.3
],
"target_count": 5,
"target_labels": [
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"target": 0,
"text": "^广告$"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"target": 0,
"text": "^图文$"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^特效\\|"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^模板\\|"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^购物\\|"
}
]
},
"live": {
"sleep_random": [
15,
20
],
"target_count": 0
},
"timeout": 600
}
}
]
}
},
{
"name": "exit",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme"
}
]
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "not_equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
}
]
}
]
}

View File

@@ -0,0 +1,104 @@
//go:build localtest
package uitest
import (
"testing"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
func TestAndroidVideoCrawlerTest(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("抓取抖音视频信息").
WithVariables(map[string]interface{}{
"device": "${ENV(SerialNumber)}",
}).
SetAndroid(uixt.WithSerialNumber("$device")),
TestSteps: []hrp.IStep{
hrp.NewStep("滑动消费 feed 至少 10 个live 至少 3 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s").
Android().
VideoCrawler(map[string]interface{}{
"app_package_name": "com.ss.android.ugc.aweme",
"timeout": 600,
"feed": map[string]interface{}{
"target_count": 5,
"target_labels": []map[string]interface{}{
{"text": "^广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true, "target": 1},
{"text": "^图文$", "scope": []float64{0, 0.5, 1, 1}, "regex": true, "target": 1},
{"text": `^特效\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
{"text": `^模板\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
{"text": `^购物\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
},
"sleep_random": []float64{0, 5, 0.7, 5, 10, 0.3},
},
"live": map[string]interface{}{
"target_count": 3,
"sleep_random": []float64{15, 20},
},
}),
hrp.NewStep("exit").
Android().
AppTerminate("com.ss.android.ugc.aweme").
Validate().
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
},
}
if err := testCase.Dump2JSON("demo_android_video_crawler.json"); err != nil {
t.Fatal(err)
}
runner := hrp.NewRunner(t).SetSaveTests(true)
err := runner.Run(testCase)
if err != nil {
t.Fatal(err)
}
}
func TestAndroidVideoCrawlerKSTest(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("抓取 KS 视频信息").
WithVariables(map[string]interface{}{
"device": "${ENV(SerialNumber)}",
}).
SetAndroid(uixt.WithSerialNumber("$device")),
TestSteps: []hrp.IStep{
hrp.NewStep("滑动消费 feed 至少 100 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s").
Android().
VideoCrawler(map[string]interface{}{
"app_package_name": "com.smile.gifmaker",
"timeout": 3600,
"feed": map[string]interface{}{
"target_count": 100,
"target_labels": []map[string]interface{}{
{"text": "^广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
{"text": "^推广$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
{"text": "^磁力广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
},
"sleep_random": []float64{0, 5, 0.7, 5, 10, 0.3},
},
"live": map[string]interface{}{
"target_count": 0,
"sleep_random": []float64{15, 20},
},
}),
hrp.NewStep("exit").
Android().
AppTerminate("com.smile.gifmaker").
Validate().
AssertAppNotInForeground("com.smile.gifmaker"),
},
}
if err := testCase.Dump2JSON("demo_android_video_ks_crawler.json"); err != nil {
t.Fatal(err)
}
runner := hrp.NewRunner(t).SetSaveTests(true)
err := runner.Run(testCase)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,98 @@
{
"config": {
"name": "抓取 KS 视频信息",
"variables": {
"device": "${ENV(SerialNumber)}"
},
"android": [
{
"serial": "$device"
}
]
},
"teststeps": [
{
"name": "滑动消费 feed 至少 100 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s",
"android": {
"actions": [
{
"method": "video_crawler",
"params": {
"app_package_name": "com.smile.gifmaker",
"feed": {
"sleep_random": [
0,
5,
0.7,
5,
10,
0.3
],
"target_count": 100,
"target_labels": [
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^广告$"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^推广$"
},
{
"regex": true,
"scope": [
0,
0.5,
1,
1
],
"text": "^磁力广告$"
}
]
},
"live": {
"sleep_random": [
15,
20
],
"target_count": 0
},
"timeout": 3600
}
}
]
}
},
{
"name": "exit",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.smile.gifmaker"
}
]
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "not_equal",
"expect": "com.smile.gifmaker",
"msg": "app [com.smile.gifmaker] should not be in foreground"
}
]
}
]
}

View File

@@ -47,9 +47,6 @@ func TestIOSDouyinFollowLive(t *testing.T) {
if err := testCase.Dump2JSON("demo_douyin_follow_live.json"); err != nil {
t.Fatal(err)
}
if err := testCase.Dump2YAML("demo_douyin_follow_live.yaml"); err != nil {
t.Fatal(err)
}
runner := hrp.NewRunner(t).SetSaveTests(true)
err := runner.Run(testCase)

View File

@@ -39,7 +39,7 @@ func TestAndroidKuaiShouFeedCardLive(t *testing.T) {
uixt.WithCustomDirection(0.9, 0.7, 0.9, 0.3),
uixt.WithScope(0.2, 0.5, 0.8, 0.8),
uixt.WithMaxRetryTimes(20),
uixt.WithWaitTime(60),
uixt.WithInterval(60),
uixt.WithIdentifier("click_live"),
),
hrp.NewStep("等待1分钟").

View File

@@ -150,7 +150,7 @@ func NewWorldCupLive(device uixt.Device, matchName, bundleID string, duration, i
func (wc *WorldCupLive) getCurrentLiveTime(utcTime time.Time) error {
utcTimeStr := utcTime.Format("15:04:05")
ocrTexts, err := wc.driver.GetTextsByOCR()
ocrTexts, err := wc.driver.GetScreenTexts()
if err != nil {
log.Error().Err(err).Msg("get ocr texts failed")
return err
@@ -212,8 +212,11 @@ func (wc *WorldCupLive) EnterLive(bundleID string) error {
time.Sleep(5 * time.Second)
// 青少年弹窗处理
if points, err := wc.driver.GetTextXYs([]string{"青少年模式", "我知道了"}); err == nil {
_ = wc.driver.TapAbsXY(points[1].X, points[1].Y)
if ocrTexts, err := wc.driver.GetScreenTexts(); err == nil {
if points, err := ocrTexts.FindTexts([]string{"青少年模式", "我知道了"}); err == nil {
point := points[1].Center()
_ = wc.driver.TapAbsXY(point.X, point.Y)
}
}
// 进入世界杯 tab

View File

@@ -89,7 +89,7 @@ func TestIOSDouyinWorldCupLive(t *testing.T) {
uixt.WithMaxRetryTimes(5),
uixt.WithCustomDirection(0.4, 0.07, 0.6, 0.07), // 滑动 tab从左到右解决「世界杯」被遮挡的问题
uixt.WithScope(0, 0, 1, 0.15), // 限定 tab 区域
uixt.WithWaitTime(1),
uixt.WithInterval(1),
),
hrp.NewStep("点击进入赛程晋级").
Loop(5). // 重复执行 5 次

38
go.mod
View File

@@ -5,13 +5,13 @@ go 1.18
require (
github.com/andybalholm/brotli v1.0.4
github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/color v1.13.0
github.com/fatih/color v1.15.0
github.com/getsentry/sentry-go v0.13.0
github.com/go-openapi/spec v0.20.7
github.com/go-ping/ping v1.1.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.5.0
github.com/httprunner/funplugin v0.5.0
github.com/httprunner/funplugin v0.5.1
github.com/jinzhu/copier v0.3.5
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12
@@ -20,26 +20,28 @@ require (
github.com/miekg/dns v1.1.50
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/otiai10/gosseract/v2 v2.4.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.29.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
gocv.io/x/gocv v0.31.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1
github.com/stretchr/testify v1.8.2
gocv.io/x/gocv v0.32.1
golang.org/x/net v0.9.0
golang.org/x/oauth2 v0.6.0
google.golang.org/grpc v1.54.0
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0
)
require (
cloud.google.com/go/compute v1.7.0 // indirect
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-errors/errors v1.4.2 // indirect
@@ -47,10 +49,10 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/go-hclog v1.3.0 // indirect
github.com/hashicorp/go-plugin v1.4.5 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-plugin v1.4.9 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -73,13 +75,13 @@ require (
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.5.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

349
go.sum
View File

@@ -13,37 +13,18 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -53,11 +34,9 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -65,30 +44,20 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -101,19 +70,12 @@ github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbj
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -156,8 +118,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -173,10 +133,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -186,19 +145,12 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -206,55 +158,33 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.3.0 h1:G0ACM8Z2WilWgPv3Vdzwm3V0BQu/kSmrkVtpe1fy9do=
github.com/hashicorp/go-hclog v1.3.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo=
github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/httprunner/funplugin v0.5.0 h1:Laoe8URu71qeyST9wvRtGSkDWc8Y3T1IrnvFSTHmO84=
github.com/httprunner/funplugin v0.5.0/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/httprunner/funplugin v0.5.1 h1:7CbdN0jfSn8GOWgdxiHqqModIZ6pintqVZwPRw9Koww=
github.com/httprunner/funplugin v0.5.1/go.mod h1:o6l442jWROJgQytrEa9E/PgmL2uKA8c2AWeaYH4wSkg=
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -277,8 +207,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -292,13 +222,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -311,7 +238,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -326,11 +252,17 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/gosseract/v2 v2.4.0 h1:gYd3mx6FuMtIlxL4sYb9JLCFEDzg09VgNSZRNbqpiGM=
github.com/otiai10/gosseract/v2 v2.4.0/go.mod h1:fhbIDRh29bj13vni6RT3gtWKjKCAeqDYI4C1dxeJuek=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -366,11 +298,9 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -382,7 +312,6 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -390,16 +319,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
@@ -408,9 +338,7 @@ github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKo
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -418,18 +346,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
gocv.io/x/gocv v0.31.0 h1:BHDtK8v+YPvoSPQTTiZB2fM/7BLg6511JqkruY2z6LQ=
gocv.io/x/gocv v0.31.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
gocv.io/x/gocv v0.32.1 h1:BC9hHs5+47nVgySUFVKntc6RsF3SULFzqk6OV9xz+C0=
gocv.io/x/gocv v0.32.1/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -452,8 +376,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -462,12 +384,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -496,48 +415,23 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -546,17 +440,14 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -567,7 +458,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -587,45 +477,20 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
@@ -637,12 +502,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -686,28 +549,13 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -724,29 +572,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -755,7 +580,6 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -779,65 +603,14 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 h1:ucpgjuzWqWrj0NEwjUpsGTf2IGxyLtmuSk0oGgifjec=
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -850,28 +623,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -884,10 +637,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -898,7 +649,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -907,7 +657,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -37,6 +37,7 @@ var (
proxyUrl string
saveTests bool
genHTMLReport bool
caseTimeout float32
)
func init() {
@@ -48,12 +49,14 @@ func init() {
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
runCmd.Flags().Float32Var(&caseTimeout, "case-timeout", 3600, "set testcase timeout (seconds)")
}
func makeHRPRunner() *hrp.HRPRunner {
runner := hrp.NewRunner(nil).
SetFailfast(!continueOnFailure).
SetSaveTests(saveTests)
SetSaveTests(saveTests).
SetCaseTimeout(caseTimeout)
if genHTMLReport {
runner.GenHTMLReport()
}

View File

@@ -2,7 +2,6 @@ package hrp
import (
"reflect"
"time"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
@@ -32,7 +31,8 @@ type TConfig struct {
WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"`
IOS []*uixt.IOSDevice `json:"ios,omitempty" yaml:"ios,omitempty"`
Android []*uixt.AndroidDevice `json:"android,omitempty" yaml:"android,omitempty"`
Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds
RequestTimeout float32 `json:"request_timeout,omitempty" yaml:"request_timeout,omitempty"` // request timeout in seconds
CaseTimeout float32 `json:"case_timeout,omitempty" yaml:"case_timeout,omitempty"` // testcase timeout in seconds
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
@@ -75,9 +75,15 @@ func (c *TConfig) SetThinkTime(strategy thinkTimeStrategy, cfg interface{}, limi
return c
}
// SetTimeout sets testcase timeout in seconds.
func (c *TConfig) SetTimeout(timeout time.Duration) *TConfig {
c.Timeout = timeout.Seconds()
// SetRequestTimeout sets request timeout in seconds.
func (c *TConfig) SetRequestTimeout(seconds float32) *TConfig {
c.RequestTimeout = seconds
return c
}
// SetCaseTimeout sets testcase timeout in seconds.
func (c *TConfig) SetCaseTimeout(seconds float32) *TConfig {
c.CaseTimeout = seconds
return c
}

View File

@@ -44,6 +44,8 @@ var (
InitPluginFailed = errors.New("init plugin failed") // 31
BuildGoPluginFailed = errors.New("build go plugin failed") // 32
BuildPyPluginFailed = errors.New("build py plugin failed") // 33
InterruptError = errors.New("interrupt error") // 38
TimeoutError = errors.New("timeout error") // 39
)
// summary: [40, 50)
@@ -61,6 +63,7 @@ var (
var (
AndroidDeviceConnectionError = errors.New("android device connection error") // 60
AndroidDeviceUSBDriverError = errors.New("android device USB driver error") // 61
AndroidShellExecError = errors.New("android adb shell exec error") // 62
AndroidScreenShotError = errors.New("android screenshot error") // 65
AndroidCaptureLogError = errors.New("android capture log error") // 66
)
@@ -68,8 +71,11 @@ var (
// UI automation related: [70, 80)
var (
MobileUIDriverError = errors.New("mobile UI driver error") // 70
MobileUILaunchAppError = errors.New("mobile UI launch app error") // 71
MobileUIValidationError = errors.New("mobile UI validation error") // 75
MobileUIAppNotInForegroundError = errors.New("mobile UI app not in foreground error") // 76
MobileUIActivityNotMatchError = errors.New("mobile UI activity not match error") // 77
MobileUIPopupError = errors.New("mobile UI popup error") // 78
)
// OCR related: [80, 90)
@@ -109,6 +115,8 @@ var errorsMap = map[error]int{
InitPluginFailed: 31,
BuildGoPluginFailed: 32,
BuildPyPluginFailed: 33,
InterruptError: 38,
TimeoutError: 39,
// ios related
IOSDeviceConnectionError: 50,
@@ -120,13 +128,17 @@ var errorsMap = map[error]int{
// android related
AndroidDeviceConnectionError: 60,
AndroidDeviceUSBDriverError: 61,
AndroidShellExecError: 62,
AndroidScreenShotError: 65,
AndroidCaptureLogError: 66,
// UI automation related
MobileUIDriverError: 70,
MobileUILaunchAppError: 71,
MobileUIValidationError: 75,
MobileUIAppNotInForegroundError: 76,
MobileUIActivityNotMatchError: 77,
MobileUIPopupError: 78,
// OCR related
OCREnvMissedError: 80,

View File

@@ -10,9 +10,9 @@ var (
WDA_USB_DRIVER = os.Getenv("WDA_USB_DRIVER")
WDA_LOCAL_PORT = os.Getenv("WDA_LOCAL_PORT")
WDA_LOCAL_MJPEG_PORT = os.Getenv("WDA_LOCAL_MJPEG_PORT")
VEDEM_OCR_URL = os.Getenv("VEDEM_OCR_URL")
VEDEM_OCR_AK = os.Getenv("VEDEM_OCR_AK")
VEDEM_OCR_SK = os.Getenv("VEDEM_OCR_SK")
VEDEM_IMAGE_URL = os.Getenv("VEDEM_IMAGE_URL")
VEDEM_IMAGE_AK = os.Getenv("VEDEM_IMAGE_AK")
VEDEM_IMAGE_SK = os.Getenv("VEDEM_IMAGE_SK")
DISABLE_GA = os.Getenv("DISABLE_GA")
DISABLE_SENTRY = os.Getenv("DISABLE_SENTRY")
PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL")

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By hrp v4.3.0, DO NOT EDIT!
# NOTE: Generated By hrp v4.3.4, DO NOT EDIT!
import sys
import os

View File

@@ -1,4 +1,4 @@
// NOTE: Generated By hrp v4.3.0, DO NOT EDIT!
// NOTE: Generated By hrp v4.3.4, DO NOT EDIT!
package main
import (

View File

@@ -1 +1 @@
v4.3.3
v4.3.4

View File

@@ -3,16 +3,17 @@ package gadb
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
)
type DeviceFileInfo struct {
@@ -244,7 +245,13 @@ func (d *Device) ReverseForwardKillAll() error {
func (d *Device) RunShellCommand(cmd string, args ...string) (string, error) {
raw, err := d.RunShellCommandWithBytes(cmd, args...)
return string(raw), err
if err != nil {
if errors.Is(err, code.AndroidDeviceConnectionError) {
return "", err
}
return "", errors.Wrap(code.AndroidShellExecError, err.Error())
}
return string(raw), nil
}
func (d *Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
@@ -548,7 +555,7 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
res, err := d.RunShellCommand("pm", "install", "-f", remote)
if err != nil {
return "", fmt.Errorf("error installing: %v", err)
return "", errors.Wrap(err, "install apk failed")
}
if haserr(res) {
return "", errors.New(res)

View File

@@ -1,15 +1,18 @@
package gadb
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"regexp"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
)
var ErrConnBroken = errors.New("socket connection broken")
@@ -45,6 +48,8 @@ func (t transport) Conn() net.Conn {
return t.sock
}
var regexDeviceOffline = regexp.MustCompile("device .* not found")
func (t transport) VerifyResponse() (err error) {
var status string
if status, err = t.ReadStringN(4); err != nil {
@@ -58,9 +63,15 @@ func (t transport) VerifyResponse() (err error) {
if sError, err = t.UnpackString(); err != nil {
return err
}
err = fmt.Errorf("command failed: %s", sError)
log.Error().Str("status", status).Str("err", sError).Msg("verify adb response failed")
return
if regexDeviceOffline.MatchString(sError) {
// device offline
return errors.Wrap(code.AndroidDeviceConnectionError, sError)
}
log.Warn().Str("status", status).Str("err", sError).
Msg("verify adb response failed")
return errors.New(sError)
}
func (t transport) ReadStringAll() (s string, err error) {

View File

@@ -28,9 +28,9 @@ You can get more installation introduction on [hybridgroup/gocv].
OCR API is a paid service, you need to pre-purchase and configure the environment variables.
- VEDEM_OCR_URL
- VEDEM_OCR_AK
- VEDEM_OCR_SK
- VEDEM_IMAGE_URL
- VEDEM_IMAGE_AK
- VEDEM_IMAGE_SK
## Thanks

591
hrp/pkg/uixt/action.go Normal file
View File

@@ -0,0 +1,591 @@
package uixt
import (
"encoding/json"
"fmt"
"math/rand"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
type ActionMethod string
const (
ACTION_AppInstall ActionMethod = "install"
ACTION_AppUninstall ActionMethod = "uninstall"
ACTION_AppStart ActionMethod = "app_start"
ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
ACTION_AppTerminate ActionMethod = "app_terminate"
ACTION_AppStop ActionMethod = "app_stop"
ACTION_ScreenShot ActionMethod = "screenshot"
ACTION_Sleep ActionMethod = "sleep"
ACTION_SleepRandom ActionMethod = "sleep_random"
ACTION_StartCamera ActionMethod = "camera_start" // alias for app_launch camera
ACTION_StopCamera ActionMethod = "camera_stop" // alias for app_terminate camera
// UI validation
// selectors
SelectorName string = "ui_name"
SelectorLabel string = "ui_label"
SelectorOCR string = "ui_ocr"
SelectorImage string = "ui_image"
SelectorForegroundApp string = "ui_foreground_app"
// assertions
AssertionEqual string = "equal"
AssertionNotEqual string = "not_equal"
AssertionExists string = "exists"
AssertionNotExists string = "not_exists"
// UI handling
ACTION_Home ActionMethod = "home"
ACTION_TapXY ActionMethod = "tap_xy"
ACTION_TapAbsXY ActionMethod = "tap_abs_xy"
ACTION_TapByOCR ActionMethod = "tap_ocr"
ACTION_TapByCV ActionMethod = "tap_cv"
ACTION_Tap ActionMethod = "tap"
ACTION_DoubleTapXY ActionMethod = "double_tap_xy"
ACTION_DoubleTap ActionMethod = "double_tap"
ACTION_Swipe ActionMethod = "swipe"
ACTION_Input ActionMethod = "input"
ACTION_Back ActionMethod = "back"
// custom actions
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
ACTION_VideoCrawler ActionMethod = "video_crawler"
)
type MobileAction struct {
Method ActionMethod `json:"method,omitempty" yaml:"method,omitempty"`
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
Options *ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
}
// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner
// [x1, y1, x2, y2] in percentage of the screen
type Scope []float64
// [x1, y1, x2, y2] in absolute pixels
type AbsScope []int
func (s AbsScope) Option() ActionOption {
return WithAbsScope(s[0], s[1], s[2], s[3])
}
type ActionOptions struct {
// log
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
// control related
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times
IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found
Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty"` // interval between retries in seconds
PressDuration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration of ios swipe action
Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of android swipe action
Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"`
// scope related
Scope Scope `json:"scope,omitempty" yaml:"scope,omitempty"`
AbsScope AbsScope `json:"abs_scope,omitempty" yaml:"abs_scope,omitempty"`
Regex bool `json:"regex,omitempty" yaml:"regex,omitempty"` // use regex to match text
Offset []int `json:"offset,omitempty" yaml:"offset,omitempty"` // used to tap offset of point
Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element
// set custiom options such as textview, id, description
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
}
func (o *ActionOptions) Options() []ActionOption {
options := make([]ActionOption, 0)
if o == nil {
return options
}
if o.Identifier != "" {
options = append(options, WithIdentifier(o.Identifier))
}
if o.MaxRetryTimes != 0 {
options = append(options, WithMaxRetryTimes(o.MaxRetryTimes))
}
if o.IgnoreNotFoundError {
options = append(options, WithIgnoreNotFoundError(true))
}
if o.Interval != 0 {
options = append(options, WithInterval(o.Interval))
}
if o.PressDuration != 0 {
options = append(options, WithPressDuration(o.PressDuration))
}
if o.Steps != 0 {
options = append(options, WithSteps(o.Steps))
}
switch v := o.Direction.(type) {
case string:
options = append(options, WithDirection(v))
case []float64:
options = append(options, WithCustomDirection(
v[0], v[1],
v[2], v[3],
))
case []interface{}:
// loaded from json case
// custom direction: [fromX, fromY, toX, toY]
sx, _ := builtin.Interface2Float64(v[0])
sy, _ := builtin.Interface2Float64(v[1])
ex, _ := builtin.Interface2Float64(v[2])
ey, _ := builtin.Interface2Float64(v[3])
options = append(options, WithCustomDirection(
sx, sy,
ex, ey,
))
}
if o.Timeout != 0 {
options = append(options, WithTimeout(o.Timeout))
}
if o.Frequency != 0 {
options = append(options, WithFrequency(o.Frequency))
}
if len(o.AbsScope) == 4 {
options = append(options, WithAbsScope(
o.AbsScope[0], o.AbsScope[1], o.AbsScope[2], o.AbsScope[3]))
}
if len(o.Offset) == 2 {
options = append(options, WithOffset(o.Offset[0], o.Offset[1]))
}
if o.Regex {
options = append(options, WithRegex(true))
}
// custom options
if o.Custom != nil {
for k, v := range o.Custom {
options = append(options, WithCustomOption(k, v))
}
}
return options
}
func NewActionOptions(options ...ActionOption) *ActionOptions {
actionOptions := &ActionOptions{}
for _, option := range options {
option(actionOptions)
}
return actionOptions
}
func mergeDataWithOptions(data map[string]interface{}, options ...ActionOption) map[string]interface{} {
actionOptions := NewActionOptions(options...)
if actionOptions.Identifier != "" {
data["log"] = map[string]interface{}{
"enable": true,
"data": actionOptions.Identifier,
}
}
// handle point offset
if len(actionOptions.Offset) == 2 {
if x, ok := data["x"]; ok {
xf, _ := builtin.Interface2Float64(x)
data["x"] = xf + float64(actionOptions.Offset[0])
}
if y, ok := data["y"]; ok {
yf, _ := builtin.Interface2Float64(y)
data["y"] = yf + float64(actionOptions.Offset[1])
}
}
if actionOptions.Steps > 0 {
data["steps"] = actionOptions.Steps
}
if _, ok := data["steps"]; !ok {
data["steps"] = 12 // default steps
}
if actionOptions.PressDuration > 0 {
data["duration"] = actionOptions.PressDuration
}
if _, ok := data["duration"]; !ok {
data["duration"] = 0 // default duration
}
if actionOptions.Frequency > 0 {
data["frequency"] = actionOptions.Frequency
}
if _, ok := data["frequency"]; !ok {
data["frequency"] = 60 // default frequency
}
if _, ok := data["isReplace"]; !ok {
data["isReplace"] = true // default true
}
// custom options
if actionOptions.Custom != nil {
for k, v := range actionOptions.Custom {
data[k] = v
}
}
return data
}
type ActionOption func(o *ActionOptions)
func WithCustomOption(key string, value interface{}) ActionOption {
return func(o *ActionOptions) {
if o.Custom == nil {
o.Custom = make(map[string]interface{})
}
o.Custom[key] = value
}
}
func WithIdentifier(identifier string) ActionOption {
return func(o *ActionOptions) {
o.Identifier = identifier
}
}
func WithIndex(index int) ActionOption {
return func(o *ActionOptions) {
o.Index = index
}
}
// set alias for compatibility
var WithWaitTime = WithInterval
func WithInterval(sec float64) ActionOption {
return func(o *ActionOptions) {
o.Interval = sec
}
}
func WithPressDuration(duration float64) ActionOption {
return func(o *ActionOptions) {
o.PressDuration = duration
}
}
func WithSteps(steps int) ActionOption {
return func(o *ActionOptions) {
o.Steps = steps
}
}
// WithDirection inputs direction (up, down, left, right)
func WithDirection(direction string) ActionOption {
return func(o *ActionOptions) {
o.Direction = direction
}
}
// WithCustomDirection inputs sx, sy, ex, ey
func WithCustomDirection(sx, sy, ex, ey float64) ActionOption {
return func(o *ActionOptions) {
o.Direction = []float64{sx, sy, ex, ey}
}
}
// WithScope inputs area of [(x1,y1), (x2,y2)]
// x1, y1, x2, y2 are all in [0, 1], which means the relative position of the screen
func WithScope(x1, y1, x2, y2 float64) ActionOption {
return func(o *ActionOptions) {
o.Scope = Scope{x1, y1, x2, y2}
}
}
// WithAbsScope inputs area of [(x1,y1), (x2,y2)]
// x1, y1, x2, y2 are all absolute position of the screen
func WithAbsScope(x1, y1, x2, y2 int) ActionOption {
return func(o *ActionOptions) {
o.AbsScope = AbsScope{x1, y1, x2, y2}
}
}
func WithOffset(offsetX, offsetY int) ActionOption {
return func(o *ActionOptions) {
o.Offset = []int{offsetX, offsetY}
}
}
func WithRegex(regex bool) ActionOption {
return func(o *ActionOptions) {
o.Regex = regex
}
}
func WithFrequency(frequency int) ActionOption {
return func(o *ActionOptions) {
o.Frequency = frequency
}
}
func WithMaxRetryTimes(maxRetryTimes int) ActionOption {
return func(o *ActionOptions) {
o.MaxRetryTimes = maxRetryTimes
}
}
func WithTimeout(timeout int) ActionOption {
return func(o *ActionOptions) {
o.Timeout = timeout
}
}
func WithIgnoreNotFoundError(ignoreError bool) ActionOption {
return func(o *ActionOptions) {
o.IgnoreNotFoundError = ignoreError
}
}
func (dExt *DriverExt) ParseActionOptions(options ...ActionOption) []ActionOption {
actionOptions := NewActionOptions(options...)
// convert relative scope to absolute scope
if len(actionOptions.AbsScope) != 4 && len(actionOptions.Scope) == 4 {
scope := actionOptions.Scope
actionOptions.AbsScope = dExt.GenAbsScope(
scope[0], scope[1], scope[2], scope[3])
}
return actionOptions.Options()
}
func (dExt *DriverExt) GenAbsScope(x1, y1, x2, y2 float64) AbsScope {
// convert relative scope to absolute scope
absX1 := int(x1 * float64(dExt.windowSize.Width))
absY1 := int(y1 * float64(dExt.windowSize.Height))
absX2 := int(x2 * float64(dExt.windowSize.Width))
absY2 := int(y2 * float64(dExt.windowSize.Height))
return AbsScope{absX1, absY1, absX2, absY2}
}
func (dExt *DriverExt) DoAction(action MobileAction) error {
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
switch action.Method {
case ACTION_AppInstall:
// TODO
return errActionNotImplemented
case ACTION_AppLaunch:
if bundleId, ok := action.Params.(string); ok {
return dExt.Driver.AppLaunch(bundleId)
}
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
ACTION_AppLaunch, action.Params)
case ACTION_SwipeToTapApp:
if appName, ok := action.Params.(string); ok {
return dExt.swipeToTapApp(appName, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
ACTION_SwipeToTapApp, action.Params)
case ACTION_SwipeToTapText:
if text, ok := action.Params.(string); ok {
return dExt.swipeToTapTexts([]string{text}, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
ACTION_SwipeToTapText, action.Params)
case ACTION_SwipeToTapTexts:
if texts, ok := action.Params.([]string); ok {
return dExt.swipeToTapTexts(texts, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params, should be app text([]string), got %v",
ACTION_SwipeToTapText, action.Params)
case ACTION_AppTerminate:
if bundleId, ok := action.Params.(string); ok {
success, err := dExt.Driver.AppTerminate(bundleId)
if err != nil {
return errors.Wrap(err, "failed to terminate app")
}
if !success {
log.Warn().Str("bundleId", bundleId).Msg("app was not running")
}
return nil
}
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
case ACTION_Home:
return dExt.Driver.Homescreen()
case ACTION_TapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.TapXY(x, y, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params)
case ACTION_TapAbsXY:
if location, ok := action.Params.([]interface{}); ok {
// absolute coordinates x,y of window size: [100, 300]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.TapAbsXY(x, y, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params)
case ACTION_Tap:
if param, ok := action.Params.(string); ok {
return dExt.Tap(param, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params)
case ACTION_TapByOCR:
if ocrText, ok := action.Params.(string); ok {
return dExt.TapByOCR(ocrText, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params)
case ACTION_TapByCV:
if imagePath, ok := action.Params.(string); ok {
return dExt.TapByCV(imagePath, action.Options.Options()...)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params)
case ACTION_DoubleTapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.DoubleTapXY(x, y)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params)
case ACTION_DoubleTap:
if param, ok := action.Params.(string); ok {
return dExt.DoubleTap(param)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params)
case ACTION_Swipe:
swipeAction := dExt.prepareSwipeAction(action.Options.Options()...)
return swipeAction(dExt)
case ACTION_Input:
// input text on current active element
// append \n to send text with enter
// send \b\b\b to delete 3 chars
param := fmt.Sprintf("%v", action.Params)
return dExt.Driver.Input(param, action.Options.Options()...)
case ACTION_Back:
return dExt.Driver.PressBack()
case ACTION_Sleep:
if param, ok := action.Params.(json.Number); ok {
seconds, _ := param.Float64()
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(float64); ok {
time.Sleep(time.Duration(param*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(int64); ok {
time.Sleep(time.Duration(param) * time.Second)
return nil
}
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case ACTION_SleepRandom:
if params, ok := action.Params.([]interface{}); ok {
return sleepRandom(params)
}
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
case ACTION_ScreenShot:
// take screenshot
log.Info().Msg("take screenshot for current screen")
_, _, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("%d_screenshot"))
return err
case ACTION_StartCamera:
return dExt.Driver.StartCamera()
case ACTION_StopCamera:
return dExt.Driver.StopCamera()
case ACTION_VideoCrawler:
params, ok := action.Params.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid video crawler params: %v(%T)", action.Params, action.Params)
}
data, _ := json.Marshal(params)
configs := &VideoCrawlerConfigs{}
if err := json.Unmarshal(data, configs); err != nil {
return errors.Wrapf(err, "invalid video crawler params: %v(%T)", action.Params, action.Params)
}
return dExt.VideoCrawler(configs)
}
return nil
}
var errActionNotImplemented = errors.New("UI action not implemented")
func convertToFloat64(val interface{}) (float64, error) {
switch v := val.(type) {
case float64:
return v, nil
case int:
return float64(v), nil
case int64:
return float64(v), nil
default:
return 0, fmt.Errorf("invalid type for conversion to float64: %T, value: %+v", val, val)
}
}
func sleepRandom(params []interface{}) error {
// append default weight 1
if len(params) == 2 {
params = append(params, 1.0)
}
var sections []struct {
min, max, weight float64
}
totalProb := 0.0
for i := 0; i+3 <= len(params); i += 3 {
min, err := convertToFloat64(params[i])
if err != nil {
return errors.Wrapf(err, "invalid minimum time: %v", params[i])
}
max, err := convertToFloat64(params[i+1])
if err != nil {
return errors.Wrapf(err, "invalid maximum time: %v", params[i+1])
}
weight, err := convertToFloat64(params[i+2])
if err != nil {
return errors.Wrapf(err, "invalid weight value: %v", params[i+2])
}
totalProb += weight
sections = append(sections,
struct{ min, max, weight float64 }{min, max, weight},
)
}
if totalProb == 0 {
log.Warn().Msg("total weight is 0, skip sleep")
return nil
}
r := rand.Float64()
accProb := 0.0
for _, s := range sections {
accProb += s.weight / totalProb
if r < accProb {
n := s.min + rand.Float64()*(s.max-s.min)
log.Info().Float64("duration", n).
Interface("strategy_params", params).Msg("sleep random seconds")
time.Sleep(time.Duration(n*1000) * time.Millisecond)
return nil
}
}
return nil
}

View File

@@ -59,7 +59,7 @@ func (ad *adbDriver) WindowSize() (size Size, err error) {
// adb shell wm size
resp, err := ad.adbClient.RunShellCommand("wm", "size")
if err != nil {
return
return size, errors.Wrap(err, "get window size failed")
}
// Physical size: 1080x2340
@@ -81,15 +81,18 @@ func (ad *adbDriver) Scale() (scale float64, err error) {
}
// PressBack simulates a short press on the BACK button.
func (ad *adbDriver) PressBack(options ...DataOption) (err error) {
func (ad *adbDriver) PressBack(options ...ActionOption) (err error) {
// adb shell input keyevent 4
_, err = ad.adbClient.RunShellCommand("input", "keyevent", fmt.Sprintf("%d", KCBack))
return
if err != nil {
return errors.Wrap(err, "press back failed")
}
return nil
}
func (ad *adbDriver) StartCamera() (err error) {
if _, err = ad.adbClient.RunShellCommand("rm", "-r", "/sdcard/DCIM/Camera"); err != nil {
return err
return errors.Wrap(err, "remove /sdcard/DCIM/Camera failed")
}
time.Sleep(5 * time.Second)
var version string
@@ -160,10 +163,12 @@ func (ad *adbDriver) AppLaunch(packageName string) (err error) {
"monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1",
)
if err != nil {
return err
return errors.Wrap(code.MobileUILaunchAppError,
fmt.Sprintf("monkey launch failed: %v", err))
}
if strings.Contains(sOutput, "monkey aborted") {
return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput))
return errors.Wrap(code.MobileUILaunchAppError,
fmt.Sprintf("monkey aborted: %s", strings.TrimSpace(sOutput)))
}
ad.lastLaunchedPackageName = packageName
return nil
@@ -174,7 +179,7 @@ func (ad *adbDriver) AppTerminate(packageName string) (successful bool, err erro
// adb shell am force-stop <packagename>
_, err = ad.adbClient.RunShellCommand("am", "force-stop", packageName)
if err != nil {
return false, err
return false, errors.Wrap(err, "force-stop app failed")
}
if ad.lastLaunchedPackageName == packageName {
@@ -183,22 +188,27 @@ func (ad *adbDriver) AppTerminate(packageName string) (successful bool, err erro
return true, nil
}
func (ad *adbDriver) Tap(x, y int, options ...DataOption) error {
func (ad *adbDriver) Tap(x, y int, options ...ActionOption) error {
return ad.TapFloat(float64(x), float64(y), options...)
}
func (ad *adbDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
dataOptions := NewDataOptions(options...)
func (ad *adbDriver) TapFloat(x, y float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
if len(dataOptions.Offset) == 2 {
x += float64(dataOptions.Offset[0])
y += float64(dataOptions.Offset[1])
if len(actionOptions.Offset) == 2 {
x += float64(actionOptions.Offset[0])
y += float64(actionOptions.Offset[1])
}
// adb shell input tap x y
xStr := fmt.Sprintf("%.1f", x)
yStr := fmt.Sprintf("%.1f", y)
_, err = ad.adbClient.RunShellCommand(
"input", "tap", fmt.Sprintf("%.1f", x), fmt.Sprintf("%.1f", y))
return
"input", "tap", xStr, yStr)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("tap <%s, %s> failed", xStr, yStr))
}
return nil
}
func (ad *adbDriver) DoubleTap(x, y int) error {
@@ -219,27 +229,30 @@ func (ad *adbDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err err
return
}
func (ad *adbDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
func (ad *adbDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ad.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ad *adbDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
func (ad *adbDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
err = errDriverNotImplemented
return
}
func (ad *adbDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
func (ad *adbDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ad.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ad *adbDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
func (ad *adbDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
// adb shell input swipe fromX fromY toX toY
_, err := ad.adbClient.RunShellCommand(
"input", "swipe",
fmt.Sprintf("%.1f", fromX), fmt.Sprintf("%.1f", fromY),
fmt.Sprintf("%.1f", toX), fmt.Sprintf("%.1f", toY),
)
return err
if err != nil {
return errors.Wrap(err, "swipe failed")
}
return nil
}
func (ad *adbDriver) ForceTouch(x, y int, pressure float64, second ...float64) error {
@@ -261,13 +274,16 @@ func (ad *adbDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
return
}
func (ad *adbDriver) SendKeys(text string, options ...DataOption) (err error) {
func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) {
// adb shell input text <text>
_, err = ad.adbClient.RunShellCommand("input", "text", text)
return
if err != nil {
return errors.Wrap(err, "send keys failed")
}
return nil
}
func (ad *adbDriver) Input(text string, options ...DataOption) (err error) {
func (ad *adbDriver) Input(text string, options ...ActionOption) (err error) {
return ad.SendKeys(text, options...)
}
@@ -360,30 +376,53 @@ func (ad *adbDriver) GetLastLaunchedApp() (packageName string) {
return ad.lastLaunchedPackageName
}
func (ad *adbDriver) IsAppInForeground(packageName string) (bool, error) {
func (ad *adbDriver) AssertAppForeground(packageName string) error {
if packageName == "" {
return false, errors.New("package name is not given")
return errors.New("package name is not given")
}
// adb shell dumpsys activity activities | grep mResumedActivity
app, err := ad.GetForegroundApp()
if err != nil {
return err
}
if app.PackageName != packageName {
return fmt.Errorf("%v is not in foreground, current is %v",
packageName, app.PackageName)
}
return nil
}
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
// adb shell dumpsys activity activities
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
if err != nil {
log.Error().Err(err).Msg("failed to dumpsys activities")
return false, err
return AppInfo{}, errors.Wrap(err, "dumpsys activities failed")
}
lines := strings.Split(string(output), "\n")
isInForeground := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "mResumedActivity:") {
if strings.Contains(trimmedLine, packageName) {
isInForeground = true
// grep mResumedActivity|ResumedActivity
if strings.HasPrefix(trimmedLine, "mResumedActivity:") || strings.HasPrefix(trimmedLine, "ResumedActivity:") {
// mResumedActivity: ActivityRecord{9656d74 u0 com.android.settings/.Settings t407}
// ResumedActivity: ActivityRecord{8265c25 u0 com.android.settings/.Settings t73}
strs := strings.Split(trimmedLine, " ")
for _, str := range strs {
if strings.Contains(str, "/") {
// com.android.settings/.Settings
s := strings.Split(str, "/")
app := AppInfo{
AppBaseInfo: AppBaseInfo{
PackageName: s[0],
Activity: s[1],
},
}
return app, nil
}
}
break
}
}
return isInForeground, nil
return AppInfo{}, errors.New("get foreground app failed")
}

View File

@@ -10,7 +10,17 @@ import (
"time"
)
var uiaServerURL = "http://localhost:6790/wd/hub"
var (
uiaServerURL = "http://localhost:6790/wd/hub"
driverExt *DriverExt
)
func setupAndroid(t *testing.T) {
device, err := NewAndroidDevice()
checkErr(t, err)
driverExt, err = device.NewDriver(nil)
checkErr(t, err)
}
func TestDriver_NewSession(t *testing.T) {
driver, err := NewUIADriver(nil, uiaServerURL)
@@ -153,20 +163,6 @@ func TestDriver_GetAppiumSettings(t *testing.T) {
// t.Log(appiumSettings)
}
func TestDriver_DeviceScaleRatio(t *testing.T) {
driver, err := NewUIADriver(nil, uiaServerURL)
if err != nil {
t.Fatal(err)
}
scaleRatio, err := driver.Scale()
if err != nil {
t.Fatal(err)
}
t.Log(scaleRatio)
}
func TestDriver_DeviceInfo(t *testing.T) {
driver, err := NewUIADriver(nil, uiaServerURL)
if err != nil {
@@ -354,29 +350,33 @@ func TestDriver_AppLaunch(t *testing.T) {
}
func TestDriver_IsAppInForeground(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
setupAndroid(t)
err := driverExt.Driver.AppLaunch("com.android.settings")
checkErr(t, err)
app, err := driverExt.Driver.GetForegroundApp()
checkErr(t, err)
if app.PackageName != "com.android.settings" {
t.FailNow()
}
if app.Activity != ".Settings" {
t.FailNow()
}
err = driverExt.Driver.AssertAppForeground(driverExt.Driver.GetLastLaunchedApp())
if err != nil {
t.Fatal(err)
}
err = driver.Driver.AppLaunch("com.android.settings")
time.Sleep(2 * time.Second)
_, err = driverExt.Driver.AppTerminate("com.android.settings")
if err != nil {
t.Fatal(err)
}
yes, err := driver.Driver.IsAppInForeground(driver.Driver.GetLastLaunchedApp())
if err != nil || !yes {
t.Fatal(err)
}
_, err = driver.Driver.AppTerminate("com.android.settings")
if err != nil {
t.Fatal(err)
}
yes, err = driver.Driver.IsAppInForeground("com.android.settings")
if err != nil || yes {
err = driverExt.Driver.AssertAppForeground("com.android.settings")
if err == nil {
t.Fatal(err)
}
}

View File

@@ -163,7 +163,7 @@ func (ud *uiaDriver) WindowSize() (size Size, err error) {
// register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "window/:windowHandle/size"); err != nil {
return Size{}, err
return Size{}, errors.Wrap(err, "get window size failed with uiautomator2")
}
reply := new(struct{ Value struct{ Size } })
if err = json.Unmarshal(rawResp, reply); err != nil {
@@ -174,7 +174,7 @@ func (ud *uiaDriver) WindowSize() (size Size, err error) {
}
// PressBack simulates a short press on the BACK button.
func (ud *uiaDriver) PressBack(options ...DataOption) (err error) {
func (ud *uiaDriver) PressBack(options ...ActionOption) (err error) {
// register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"))
_, err = ud.httpPOST(nil, "/session", ud.sessionId, "back")
return
@@ -199,18 +199,18 @@ func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...K
return
}
func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error {
func (ud *uiaDriver) Tap(x, y int, options ...ActionOption) error {
return ud.TapFloat(float64(x), float64(y), options...)
}
func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error) {
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
data := map[string]interface{}{
"x": x,
"y": y,
}
// new data options in post data for extra uiautomator configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = ud.httpPOST(newData, "/session", ud.sessionId, "appium/tap")
return
@@ -240,11 +240,11 @@ func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err err
// the smoothness and speed of the swipe by specifying the number of steps.
// Each step execution is throttled to 5 milliseconds per step, so for a 100
// steps, the swipe will take around 0.5 seconds to complete.
func (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
func (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ud.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
data := map[string]interface{}{
"startX": fromX,
"startY": fromY,
@@ -253,7 +253,7 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp
}
// new data options in post data for extra uiautomator configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
// register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"))
_, err = ud.httpPOST(newData, "/session", ud.sessionId, "touch/drag")
@@ -264,11 +264,11 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp
// to determine smoothness and speed. Each step execution is throttled to 5ms
// per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
// `steps` is the number of move steps sent to the system
func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) 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...)
}
func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
data := map[string]interface{}{
"startX": fromX,
@@ -278,7 +278,7 @@ func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataO
}
// new data options in post data for extra uiautomator configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err := ud.httpPOST(newData, "/session", ud.sessionId, "touch/perform")
return err
@@ -327,20 +327,20 @@ func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
return
}
func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) {
func (ud *uiaDriver) SendKeys(text string, options ...ActionOption) (err error) {
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
data := map[string]interface{}{
"text": text,
}
// new data options in post data for extra uiautomator configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = ud.httpPOST(newData, "/session", ud.sessionId, "keys")
return
}
func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) {
func (ud *uiaDriver) Input(text string, options ...ActionOption) (err error) {
return ud.SendKeys(text, options...)
}

View File

@@ -20,6 +20,7 @@ type Driver struct {
urlPrefix *url.URL
sessionId string
client *http.Client
scale float64
// cache the last launched package name
lastLaunchedPackageName string
}

View File

@@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
@@ -32,13 +34,21 @@ func TestIOSDemo(t *testing.T) {
// 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」
for {
points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"})
// take screenshot and get screen texts by OCR
ocrTexts, err := driverExt.GetScreenTexts()
if err != nil {
log.Error().Err(err).Msg("OCR GetTexts failed")
t.Fatal(err)
}
points, err := ocrTexts.FindTexts([]string{"青少年模式", "我知道了"})
if err != nil {
time.Sleep(1 * time.Second)
continue
}
err = driverExt.TapAbsXY(points[1].X, points[1].Y)
point := points[1].Center()
err = driverExt.TapAbsXY(point.X, point.Y)
if err != nil {
t.Fatal(err)
}

View File

@@ -5,7 +5,7 @@ func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...f
}
func (dExt *DriverExt) DragFloat(pathname string, toX, toY float64, pressForDuration ...float64) (err error) {
return dExt.DragOffsetFloat(pathname, toX, toY, 0.5, 0.5, pressForDuration...)
return dExt.DragOffsetFloat(pathname, toX, toY, 0, 0, pressForDuration...)
}
func (dExt *DriverExt) DragOffset(pathname string, toX, toY int, xOffset, yOffset float64, pressForDuration ...float64) (err error) {
@@ -17,14 +17,11 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs
pressForDuration = []float64{1.0}
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil {
point, err := dExt.FindUIRectInUIKit(pathname)
if err != nil {
return err
}
fromX := x + width*xOffset
fromY := y + height*yOffset
return dExt.Driver.DragFloat(fromX, fromY, toX, toY,
WithDataPressDuration(pressForDuration[0]))
return dExt.Driver.DragFloat(point.X+xOffset, point.Y+yOffset, toX, toY,
WithPressDuration(pressForDuration[0]))
}

View File

@@ -13,180 +13,19 @@ import (
"mime/multipart"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type MobileMethod string
const (
AppInstall MobileMethod = "install"
AppUninstall MobileMethod = "uninstall"
AppStart MobileMethod = "app_start"
AppLaunch MobileMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
AppTerminate MobileMethod = "app_terminate"
AppStop MobileMethod = "app_stop"
CtlScreenShot MobileMethod = "screenshot"
CtlSleep MobileMethod = "sleep"
CtlSleepRandom MobileMethod = "sleep_random"
CtlStartCamera MobileMethod = "camera_start" // alias for app_launch camera
CtlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera
RecordStart MobileMethod = "record_start"
RecordStop MobileMethod = "record_stop"
// UI validation
// selectors
SelectorName string = "ui_name"
SelectorLabel string = "ui_label"
SelectorOCR string = "ui_ocr"
SelectorImage string = "ui_image"
SelectorForegroundApp string = "ui_foreground_app"
// assertions
AssertionEqual string = "equal"
AssertionNotEqual string = "not_equal"
AssertionExists string = "exists"
AssertionNotExists string = "not_exists"
// UI handling
ACTION_Home MobileMethod = "home"
ACTION_TapXY MobileMethod = "tap_xy"
ACTION_TapAbsXY MobileMethod = "tap_abs_xy"
ACTION_TapByOCR MobileMethod = "tap_ocr"
ACTION_TapByCV MobileMethod = "tap_cv"
ACTION_Tap MobileMethod = "tap"
ACTION_DoubleTapXY MobileMethod = "double_tap_xy"
ACTION_DoubleTap MobileMethod = "double_tap"
ACTION_Swipe MobileMethod = "swipe"
ACTION_Input MobileMethod = "input"
ACTION_Back MobileMethod = "back"
// custom actions
ACTION_SwipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
ACTION_SwipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
ACTION_SwipeToTapTexts MobileMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
)
type MobileAction struct {
Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"`
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times
WaitTime float64 `json:"wait_time,omitempty" yaml:"wait_time,omitempty"` // wait time between swipe and ocr, unit: second
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration of ios swipe action
Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of android swipe action
Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app
Scope []float64 `json:"scope,omitempty" yaml:"scope,omitempty"` // used by ocr to get text position in the scope
Offset []int `json:"offset,omitempty" yaml:"offset,omitempty"` // used to tap offset of point
Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action
IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found
Text string `json:"text,omitempty" yaml:"text,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
type ActionOption func(o *MobileAction)
func WithIdentifier(identifier string) ActionOption {
return func(o *MobileAction) {
o.Identifier = identifier
}
}
func WithIndex(index int) ActionOption {
return func(o *MobileAction) {
o.Index = index
}
}
func WithWaitTime(sec float64) ActionOption {
return func(o *MobileAction) {
o.WaitTime = sec
}
}
func WithDuration(duration float64) ActionOption {
return func(o *MobileAction) {
o.Duration = duration
}
}
func WithSteps(steps int) ActionOption {
return func(o *MobileAction) {
o.Steps = steps
}
}
// WithDirection inputs direction (up, down, left, right)
func WithDirection(direction string) ActionOption {
return func(o *MobileAction) {
o.Direction = direction
}
}
// WithCustomDirection inputs sx, sy, ex, ey
func WithCustomDirection(sx, sy, ex, ey float64) ActionOption {
return func(o *MobileAction) {
o.Direction = []float64{sx, sy, ex, ey}
}
}
// WithScope inputs area of [(x1,y1), (x2,y2)]
func WithScope(x1, y1, x2, y2 float64) ActionOption {
return func(o *MobileAction) {
o.Scope = []float64{x1, y1, x2, y2}
}
}
func WithOffset(offsetX, offsetY int) ActionOption {
return func(o *MobileAction) {
o.Offset = []int{offsetX, offsetY}
}
}
func WithText(text string) ActionOption {
return func(o *MobileAction) {
o.Text = text
}
}
func WithID(id string) ActionOption {
return func(o *MobileAction) {
o.ID = id
}
}
func WithDescription(description string) ActionOption {
return func(o *MobileAction) {
o.Description = description
}
}
func WithMaxRetryTimes(maxRetryTimes int) ActionOption {
return func(o *MobileAction) {
o.MaxRetryTimes = maxRetryTimes
}
}
func WithTimeout(timeout int) ActionOption {
return func(o *MobileAction) {
o.Timeout = timeout
}
}
func WithIgnoreNotFoundError(ignoreError bool) ActionOption {
return func(o *MobileAction) {
o.IgnoreNotFoundError = ignoreError
}
}
// TemplateMatchMode is the type of the template matching operation.
type TemplateMatchMode int
@@ -209,37 +48,69 @@ func WithThreshold(threshold float64) CVOption {
}
}
type Popularity struct {
Stars string `json:"stars,omitempty"` // 点赞数
Comments string `json:"comments,omitempty"` // 评论数
Favorites string `json:"favorites,omitempty"` // 收藏数
Shares string `json:"shares,omitempty"` // 分享数
LiveUsers string `json:"live_users,omitempty"` // 直播间人数
}
type ScreenResult struct {
Texts OCRTexts `json:"texts"` // dumped OCRTexts
Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"]
Popularity Popularity `json:"popularity"` // video popularity data
}
type cacheStepData struct {
// cache step screenshot paths
screenShots []string
screenShotsUrls map[string]string // map screenshot file path to uploaded url
// cache step screenshot ocr results, key is image path, value is ScreenResult
screenResults map[string]*ScreenResult
// cache feed/live video stat
videoStat *VideoStat
}
func (d *cacheStepData) reset() {
d.screenShots = make([]string, 0)
d.screenShotsUrls = make(map[string]string)
d.screenResults = make(map[string]*ScreenResult)
d.videoStat = nil
}
type DriverExt struct {
CVArgs
Device Device
Driver WebDriver
windowSize Size
frame *bytes.Buffer
doneMjpegStream chan bool
scale float64
ocrService OCRService // used to get text from image
screenShots []string // cache screenshot paths
ImageService IImageService // used to extract image data
interruptSignal chan os.Signal
CVArgs
// cache step data
cacheStepData cacheStepData
}
func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error) {
dExt = &DriverExt{
Device: device,
Driver: driver,
Device: device,
Driver: driver,
cacheStepData: cacheStepData{},
interruptSignal: make(chan os.Signal, 1),
}
dExt.cacheStepData.reset()
signal.Notify(dExt.interruptSignal, syscall.SIGTERM, syscall.SIGINT)
dExt.doneMjpegStream = make(chan bool, 1)
// get device window size
dExt.windowSize, err = dExt.Driver.WindowSize()
if err != nil {
return nil, errors.Wrap(err, "failed to get windows size")
}
if dExt.scale, err = dExt.Driver.Scale(); err != nil {
return nil, err
}
if dExt.ocrService, err = newVEDEMOCRService(); err != nil {
if dExt.ImageService, err = newVEDEMImageService("ocr", "upload", "liveType"); err != nil {
return nil, err
}
@@ -255,32 +126,40 @@ func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error)
// TakeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
// if fileName is empty, it will not save image file and only return raw image data
func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, err error) {
// wait for action done
time.Sleep(500 * time.Millisecond)
func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, path string, err error) {
// iOS 优先使用 MJPEG 流进行截图,性能最优
// 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口
if dExt.frame != nil {
return dExt.frame, nil
return dExt.frame, "", nil
}
if raw, err = dExt.Driver.Screenshot(); err != nil {
log.Error().Err(err).Msg("capture screenshot data failed")
return nil, err
return nil, "", err
}
// compress image data
compressed, err := compressImageBuffer(raw)
if err != nil {
log.Error().Err(err).Msg("compress screenshot data failed")
return nil, "", err
}
// save screenshot to file
if len(fileName) > 0 && fileName[0] != "" {
path := filepath.Join(env.ScreenShotsPath, fileName[0])
path, err := dExt.saveScreenShot(raw, path)
path, err := dExt.saveScreenShot(compressed, path)
if err != nil {
log.Error().Err(err).Msg("save screenshot file failed")
return nil, err
return nil, "", err
}
dExt.screenShots = append(dExt.screenShots, path)
log.Info().Str("path", path).Msg("save screenshot file success")
return compressed, path, nil
}
return compressed, "", nil
}
func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) {
// TODO: compress image data
return raw, nil
}
@@ -320,14 +199,33 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin
return "", errors.Wrap(err, "encode screenshot image failed")
}
dExt.cacheStepData.screenShots = append(dExt.cacheStepData.screenShots, screenshotPath)
log.Info().Str("path", screenshotPath).Msg("save screenshot file success")
return screenshotPath, nil
}
func (dExt *DriverExt) GetScreenShots() []string {
defer func() {
dExt.screenShots = nil
}()
return dExt.screenShots
func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
cacheData := make(map[string]interface{})
cacheData["video_stat"] = dExt.cacheStepData.videoStat
cacheData["screenshots"] = dExt.cacheStepData.screenShots
cacheData["screenshots_urls"] = dExt.cacheStepData.screenShotsUrls
screenResults := make(map[string]interface{})
for imagePath, screenResult := range dExt.cacheStepData.screenResults {
o, _ := json.Marshal(screenResult.Texts)
data := map[string]interface{}{
"tags": screenResult.Tags,
"texts": string(o),
"popularity": screenResult.Popularity,
}
screenResults[imagePath] = data
}
cacheData["screen_results"] = screenResults
// clear cache
dExt.cacheStepData.reset()
return cacheData
}
// isPathExists returns true if path exists, whether path is file or dir
@@ -342,385 +240,25 @@ func init() {
rand.Seed(time.Now().UnixNano())
}
func (dExt *DriverExt) FindUIRectInUIKit(search string, options ...DataOption) (x, y, width, height float64, err error) {
func (dExt *DriverExt) FindUIRectInUIKit(search string, options ...ActionOption) (point PointF, err error) {
// click on text, using OCR
if !isPathExists(search) {
return dExt.FindTextByOCR(search, options...)
return dExt.FindScreenText(search, options...)
}
// click on image, using opencv
return dExt.FindImageRectInUIKit(search, options...)
}
func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) {
x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale
width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale
return
}
func (dExt *DriverExt) IsOCRExist(text string) bool {
_, _, _, _, err := dExt.FindTextByOCR(text)
_, err := dExt.FindScreenText(text)
return err == nil
}
func (dExt *DriverExt) IsImageExist(text string) bool {
_, _, _, _, err := dExt.FindImageRectInUIKit(text)
_, err := dExt.FindImageRectInUIKit(text)
return err == nil
}
func (dExt *DriverExt) IsAppInForeground(packageName string) bool {
// check if app is in foreground
yes, err := dExt.Driver.IsAppInForeground(packageName)
if !yes || err != nil {
log.Info().Str("packageName", packageName).Msg("app is not in foreground")
return false
}
return true
}
var errActionNotImplemented = errors.New("UI action not implemented")
func convertToFloat64(val interface{}) (float64, error) {
switch v := val.(type) {
case float64:
return v, nil
case int:
return float64(v), nil
case int64:
return float64(v), nil
default:
return 0, fmt.Errorf("invalid type for conversion to float64: %T, value: %+v", val, val)
}
}
func (dExt *DriverExt) DoAction(action MobileAction) error {
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
switch action.Method {
case AppInstall:
// TODO
return errActionNotImplemented
case AppLaunch:
if bundleId, ok := action.Params.(string); ok {
return dExt.Driver.AppLaunch(bundleId)
}
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
AppLaunch, action.Params)
case ACTION_SwipeToTapApp:
if appName, ok := action.Params.(string); ok {
return dExt.swipeToTapApp(appName, action)
}
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
ACTION_SwipeToTapApp, action.Params)
case ACTION_SwipeToTapText:
// TODO: merge to LoopUntil
if text, ok := action.Params.(string); ok {
if len(action.Scope) != 4 {
action.Scope = []float64{0, 0, 1, 1}
}
if len(action.Offset) != 2 {
action.Offset = []int{0, 0}
}
identifierOption := WithDataIdentifier(action.Identifier)
offsetOption := WithDataOffset(action.Offset[0], action.Offset[1])
indexOption := WithDataIndex(action.Index)
scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3]))
// default to retry 10 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 10
}
maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes)
waitTimeOption := WithDataWaitTime(action.WaitTime)
var point PointF
// findTextAction := func(d *DriverExt) error {
// return nil
// }
findTextCondition := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY(text, indexOption, scopeOption)
return err
}
foundTextAction := func(d *DriverExt) error {
// tap text
return d.TapAbsXY(point.X, point.Y, identifierOption, offsetOption)
}
if action.Direction != nil {
return dExt.SwipeUntil(action.Direction, findTextCondition, foundTextAction, maxRetryOption, waitTimeOption)
}
// swipe until found
return dExt.SwipeUntil("up", findTextCondition, foundTextAction, maxRetryOption, waitTimeOption)
}
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
ACTION_SwipeToTapText, action.Params)
case ACTION_SwipeToTapTexts:
// TODO: merge to LoopUntil
if texts, ok := action.Params.([]interface{}); ok {
var textList []string
for _, t := range texts {
textList = append(textList, t.(string))
}
action.Params = textList
}
if texts, ok := action.Params.([]string); ok {
if len(action.Scope) != 4 {
action.Scope = []float64{0, 0, 1, 1}
}
if len(action.Offset) != 2 {
action.Offset = []int{0, 0}
}
identifierOption := WithDataIdentifier(action.Identifier)
offsetOption := WithDataOffset(action.Offset[0], action.Offset[1])
scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3]))
// default to retry 10 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 10
}
maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes)
waitTimeOption := WithDataWaitTime(action.WaitTime)
var point PointF
findTexts := func(d *DriverExt) error {
var err error
points, err := d.GetTextXYs(texts, scopeOption)
if err != nil {
return err
}
for _, point = range points {
if point != (PointF{X: 0, Y: 0}) {
return nil
}
}
return errors.New("failed to find text position")
}
foundTextAction := func(d *DriverExt) error {
// tap text
return d.TapAbsXY(point.X, point.Y, identifierOption, offsetOption)
}
// default to retry 10 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 10
}
if action.Direction != nil {
return dExt.SwipeUntil(action.Direction, findTexts, foundTextAction, maxRetryOption, waitTimeOption)
}
// swipe until found
return dExt.SwipeUntil("up", findTexts, foundTextAction, maxRetryOption, waitTimeOption)
}
return fmt.Errorf("invalid %s params, should be app text([]string), got %v",
ACTION_SwipeToTapText, action.Params)
case AppTerminate:
if bundleId, ok := action.Params.(string); ok {
success, err := dExt.Driver.AppTerminate(bundleId)
if err != nil {
return errors.Wrap(err, "failed to terminate app")
}
if !success {
log.Warn().Str("bundleId", bundleId).Msg("app was not running")
}
return nil
}
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
case ACTION_Home:
return dExt.Driver.Homescreen()
case ACTION_TapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.TapXY(x, y, WithDataIdentifier(action.Identifier))
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params)
case ACTION_TapAbsXY:
if location, ok := action.Params.([]interface{}); ok {
// absolute coordinates x,y of window size: [100, 300]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
if len(action.Offset) != 2 {
action.Offset = []int{0, 0}
}
return dExt.TapAbsXY(x, y, WithDataIdentifier(action.Identifier), WithDataOffset(action.Offset[0], action.Offset[1]))
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params)
case ACTION_Tap:
if param, ok := action.Params.(string); ok {
return dExt.Tap(param, WithDataIdentifier(action.Identifier), WithDataIgnoreNotFoundError(true), WithDataIndex(action.Index))
}
return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params)
case ACTION_TapByOCR:
if ocrText, ok := action.Params.(string); ok {
if len(action.Scope) != 4 {
action.Scope = []float64{0, 0, 1, 1}
}
if len(action.Offset) != 2 {
action.Offset = []int{0, 0}
}
indexOption := WithDataIndex(action.Index)
offsetOption := WithDataOffset(action.Offset[0], action.Offset[1])
scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3]))
identifierOption := WithDataIdentifier(action.Identifier)
IgnoreNotFoundErrorOption := WithDataIgnoreNotFoundError(action.IgnoreNotFoundError)
return dExt.TapByOCR(ocrText, identifierOption, IgnoreNotFoundErrorOption, indexOption, scopeOption, offsetOption)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params)
case ACTION_TapByCV:
if imagePath, ok := action.Params.(string); ok {
return dExt.TapByCV(imagePath, WithDataIdentifier(action.Identifier), WithDataIgnoreNotFoundError(true), WithDataIndex(action.Index))
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params)
case ACTION_DoubleTapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.DoubleTapXY(x, y)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params)
case ACTION_DoubleTap:
if param, ok := action.Params.(string); ok {
return dExt.DoubleTap(param)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params)
case ACTION_Swipe:
identifierOption := WithDataIdentifier(action.Identifier)
durationOption := WithDataPressDuration(action.Duration)
if action.Steps == 0 {
action.Steps = 10
}
stepsOption := WithDataSteps(action.Steps)
if positions, ok := action.Params.([]interface{}); ok {
// relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1]
if len(positions) != 4 {
return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions)
}
fromX, _ := positions[0].(float64)
fromY, _ := positions[1].(float64)
toX, _ := positions[2].(float64)
toY, _ := positions[3].(float64)
return dExt.SwipeRelative(fromX, fromY, toX, toY, identifierOption, durationOption, stepsOption)
}
if direction, ok := action.Params.(string); ok {
return dExt.SwipeTo(direction, identifierOption, durationOption, stepsOption)
}
return fmt.Errorf("invalid %s params: %v", ACTION_Swipe, action.Params)
case ACTION_Input:
// input text on current active element
// append \n to send text with enter
// send \b\b\b to delete 3 chars
param := fmt.Sprintf("%v", action.Params)
options := []DataOption{}
if action.Text != "" {
options = append(options, WithCustomOption("textview", action.Text))
}
if action.ID != "" {
options = append(options, WithCustomOption("id", action.ID))
}
if action.Description != "" {
options = append(options, WithCustomOption("description", action.Description))
}
if action.Identifier != "" {
options = append(options, WithDataIdentifier(action.Identifier))
}
return dExt.Driver.Input(param, options...)
case ACTION_Back:
return dExt.Driver.PressBack()
case CtlSleep:
if param, ok := action.Params.(json.Number); ok {
seconds, _ := param.Float64()
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(float64); ok {
time.Sleep(time.Duration(param*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(int64); ok {
time.Sleep(time.Duration(param) * time.Second)
return nil
}
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case CtlSleepRandom:
params, ok := action.Params.([]interface{})
if !ok {
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
}
// append default weight 1
if len(params) == 2 {
params = append(params, 1.0)
}
var sections []struct {
min, max, weight float64
}
totalProb := 0.0
for i := 0; i+3 <= len(params); i += 3 {
min, err := convertToFloat64(params[i])
if err != nil {
return errors.Wrapf(err, "invalid minimum time: %v", params[i])
}
max, err := convertToFloat64(params[i+1])
if err != nil {
return errors.Wrapf(err, "invalid maximum time: %v", params[i+1])
}
weight, err := convertToFloat64(params[i+2])
if err != nil {
return errors.Wrapf(err, "invalid weight value: %v", params[i+2])
}
totalProb += weight
sections = append(sections,
struct{ min, max, weight float64 }{min, max, weight},
)
}
if totalProb == 0 {
log.Warn().Msg("total weight is 0, skip sleep")
return nil
}
r := rand.Float64()
accProb := 0.0
for _, s := range sections {
accProb += s.weight / totalProb
if r < accProb {
n := s.min + rand.Float64()*(s.max-s.min)
log.Info().Float64("duration", n).Msg("sleep random seconds")
time.Sleep(time.Duration(n*1000) * time.Millisecond)
return nil
}
}
case CtlScreenShot:
// take screenshot
log.Info().Msg("take screenshot for current screen")
_, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_screenshot"))
return err
case CtlStartCamera:
return dExt.Driver.StartCamera()
case CtlStopCamera:
return dExt.Driver.StopCamera()
}
return nil
}
func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) {
return int(x1 * float64(dExt.windowSize.Width) * dExt.scale),
int(y1 * float64(dExt.windowSize.Height) * dExt.scale),
int(x2 * float64(dExt.windowSize.Width) * dExt.scale),
int(y2 * float64(dExt.windowSize.Height) * dExt.scale)
}
func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool {
var exp bool
if assert == AssertionExists || assert == AssertionEqual {
@@ -735,7 +273,7 @@ func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...s
case SelectorImage:
result = (dExt.IsImageExist(expected) == exp)
case SelectorForegroundApp:
result = (dExt.IsAppInForeground(expected) == exp)
result = ((dExt.Driver.AssertAppForeground(expected) == nil) == exp)
}
if !result {

View File

@@ -2,11 +2,8 @@ package uixt
import (
"bytes"
"math"
"strings"
"time"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
var (
@@ -251,8 +248,11 @@ type AppInfo struct {
}
type AppBaseInfo struct {
Pid int `json:"pid"`
BundleId string `json:"bundleId"`
Pid int `json:"pid,omitempty"`
BundleId string `json:"bundleId,omitempty"` // ios package name
ViewController string `json:"viewController,omitempty"` // ios view controller
PackageName string `json:"packageName,omitempty"` // android package name
Activity string `json:"activity,omitempty"` // android activity
}
type AppState int
@@ -428,144 +428,6 @@ type Rect struct {
Size
}
type DataOptions struct {
Data map[string]interface{} // configurations used by ios/android driver
Scope []int // used by ocr to get text position in the scope
Offset []int // used to tap offset of point
Index int // index of the target element, should start from 1
IgnoreNotFoundError bool // ignore error if target element not found
MaxRetryTimes int // max retry times if target element not found
Interval float64 // interval between retries in seconds
}
type DataOption func(data *DataOptions)
func WithCustomOption(key string, value interface{}) DataOption {
return func(data *DataOptions) {
data.Data[key] = value
}
}
func WithDataPressDuration(duration float64) DataOption {
return func(data *DataOptions) {
data.Data["duration"] = duration
}
}
func WithDataSteps(steps int) DataOption {
return func(data *DataOptions) {
data.Data["steps"] = steps
}
}
func WithDataFrequency(frequency int) DataOption {
return func(data *DataOptions) {
data.Data["frequency"] = frequency
}
}
func WithDataIndex(index int) DataOption {
return func(data *DataOptions) {
data.Index = index
}
}
func WithDataScope(x1, x2, y1, y2 int) DataOption {
return func(data *DataOptions) {
data.Scope = []int{x1, x2, y1, y2}
}
}
func WithDataOffset(offsetX, offsetY int) DataOption {
return func(data *DataOptions) {
data.Offset = []int{offsetX, offsetY}
}
}
func WithDataIdentifier(identifier string) DataOption {
if identifier == "" {
return func(data *DataOptions) {}
}
return func(data *DataOptions) {
data.Data["log"] = map[string]interface{}{
"enable": true,
"data": identifier,
}
}
}
func WithDataIgnoreNotFoundError(ignoreError bool) DataOption {
return func(data *DataOptions) {
data.IgnoreNotFoundError = ignoreError
}
}
func WithDataMaxRetryTimes(maxRetryTimes int) DataOption {
return func(data *DataOptions) {
data.MaxRetryTimes = maxRetryTimes
}
}
func WithDataWaitTime(sec float64) DataOption {
return func(data *DataOptions) {
data.Interval = sec
}
}
func NewDataOptions(options ...DataOption) *DataOptions {
dataOptions := &DataOptions{
Data: make(map[string]interface{}),
}
for _, option := range options {
option(dataOptions)
}
if len(dataOptions.Scope) == 0 {
dataOptions.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope
}
return dataOptions
}
func NewData(data map[string]interface{}, options ...DataOption) map[string]interface{} {
dataOptions := NewDataOptions(options...)
// merge with data options
for k, v := range dataOptions.Data {
data[k] = v
}
// handle point offset
if len(dataOptions.Offset) == 2 {
if x, ok := data["x"]; ok {
xf, _ := builtin.Interface2Float64(x)
data["x"] = xf + float64(dataOptions.Offset[0])
}
if y, ok := data["y"]; ok {
yf, _ := builtin.Interface2Float64(y)
data["y"] = yf + float64(dataOptions.Offset[1])
}
}
// add default options
if _, ok := data["steps"]; !ok {
data["steps"] = 12 // default steps
}
if _, ok := data["duration"]; !ok {
data["duration"] = 0 // default duration
}
if _, ok := data["frequency"]; !ok {
data["frequency"] = 60 // default frequency
}
if _, ok := data["isReplace"]; !ok {
data["isReplace"] = true // default true
}
return data
}
// current implemeted device: IOSDevice, AndroidDevice
type Device interface {
UUID() string // ios udid or android serial
@@ -579,6 +441,11 @@ type Device interface {
StopPcap() string
}
type ForegroundApp struct {
PackageName string
Activity string
}
// WebDriver defines methods supported by WebDriver drivers.
type WebDriver interface {
// NewSession starts a new session and returns the SessionInfo.
@@ -621,8 +488,10 @@ type WebDriver interface {
AppTerminate(packageName string) (bool, error)
// GetLastLaunchedApp returns the package name of the last launched app
GetLastLaunchedApp() string
// IsAppInForeground returns true if the given package is in foreground
IsAppInForeground(packageName string) (bool, error)
// AssertAppForeground returns nil if the given package is in foreground
AssertAppForeground(packageName string) error
// GetForegroundApp returns current foreground app package name and activity name
GetForegroundApp() (app AppInfo, err error)
// StartCamera Starts a new camera for recording
StartCamera() error
@@ -630,8 +499,8 @@ type WebDriver interface {
StopCamera() error
// Tap Sends a tap event at the coordinate.
Tap(x, y int, options ...DataOption) error
TapFloat(x, y float64, options ...DataOption) error
Tap(x, y int, options ...ActionOption) error
TapFloat(x, y float64, options ...ActionOption) error
// DoubleTap Sends a double tap event at the coordinate.
DoubleTap(x, y int) error
@@ -644,12 +513,12 @@ type WebDriver interface {
// Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate.
// WithPressDurationOption option can be used to set pressForDuration (default to 1 second).
Drag(fromX, fromY, toX, toY int, options ...DataOption) error
DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) error
Drag(fromX, fromY, toX, toY int, options ...ActionOption) error
DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error
// Swipe works like Drag, but `pressForDuration` value is 0
Swipe(fromX, fromY, toX, toY int, options ...DataOption) error
SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error
Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error
SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error
// SetPasteboard Sets data to the general pasteboard
SetPasteboard(contentType PasteboardType, content string) error
@@ -660,16 +529,16 @@ type WebDriver interface {
// SendKeys Types a string into active element. There must be element with keyboard focus,
// otherwise an error is raised.
// WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60
SendKeys(text string, options ...DataOption) error
SendKeys(text string, options ...ActionOption) error
// Input works like SendKeys
Input(text string, options ...DataOption) error
Input(text string, options ...ActionOption) error
// PressButton Presses the corresponding hardware button on the device
PressButton(devBtn DeviceButton) error
// PressBack Presses the back button
PressBack(options ...DataOption) error
PressBack(options ...ActionOption) error
Screenshot() (*bytes.Buffer, error)

View File

@@ -631,6 +631,11 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn)
// init WDA scale
if wd.scale, err = wd.Scale(); err != nil {
return nil, err
}
return wd, nil
}
@@ -659,6 +664,11 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver,
return nil, errors.Wrap(code.IOSDeviceUSBDriverError, err.Error())
}
// init WDA scale
if wd.scale, err = wd.Scale(); err != nil {
return nil, err
}
return wd, nil
}

View File

@@ -137,7 +137,7 @@ func (wd *wdaDriver) WindowSize() (size Size, err error) {
// [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/window/size"); err != nil {
return Size{}, err
return Size{}, errors.Wrap(err, "get window size failed with wda")
}
reply := new(struct{ Value struct{ Size } })
if err = json.Unmarshal(rawResp, reply); err != nil {
@@ -164,11 +164,16 @@ func (wd *wdaDriver) Screen() (screen Screen, err error) {
func (wd *wdaDriver) Scale() (float64, error) {
screen, err := wd.Screen()
if err != nil {
return 0, err
return 0, errors.Wrap(code.MobileUIDriverError,
fmt.Sprintf("get screen info failed: %v", err))
}
return screen.Scale, nil
}
func (wd *wdaDriver) toScale(x float64) float64 {
return x / wd.scale
}
func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) {
// [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)]
// [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession
@@ -308,17 +313,23 @@ func (wd *wdaDriver) AppLaunch(bundleId string) (err error) {
data := make(map[string]interface{})
data["bundleId"] = bundleId
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch")
if err == nil {
wd.lastLaunchedPackageName = bundleId
if err != nil {
return errors.Wrap(code.MobileUILaunchAppError,
fmt.Sprintf("wda launch failed: %v", err))
}
return
wd.lastLaunchedPackageName = bundleId
return nil
}
func (wd *wdaDriver) AppLaunchUnattached(bundleId string) (err error) {
// [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)]
data := map[string]interface{}{"bundleId": bundleId}
_, err = wd.httpPOST(data, "/wda/apps/launchUnattached")
return
if err != nil {
return errors.Wrap(code.MobileUILaunchAppError,
fmt.Sprintf("wda launchUnattached failed: %v", err))
}
return nil
}
func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) {
@@ -358,22 +369,26 @@ func (wd *wdaDriver) GetLastLaunchedApp() (packageName string) {
return wd.lastLaunchedPackageName
}
func (wd *wdaDriver) IsAppInForeground(packageName string) (bool, error) {
return false, errors.New("not implemented")
func (wd *wdaDriver) AssertAppForeground(packageName string) error {
return nil
}
func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error {
func (wd *wdaDriver) GetForegroundApp() (app AppInfo, err error) {
return AppInfo{}, nil
}
func (wd *wdaDriver) Tap(x, y int, options ...ActionOption) error {
return wd.TapFloat(float64(x), float64(y), options...)
}
func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
func (wd *wdaDriver) TapFloat(x, y float64, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)]
data := map[string]interface{}{
"x": x,
"y": y,
"x": wd.toScale(x),
"y": wd.toScale(y),
}
// new data options in post data for extra WDA configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = wd.httpPOST(newData, "/session", wd.sessionId, "/wda/tap/0")
return
@@ -386,8 +401,8 @@ func (wd *wdaDriver) DoubleTap(x, y int) error {
func (wd *wdaDriver) DoubleTapFloat(x, y float64) (err error) {
// [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)]
data := map[string]interface{}{
"x": x,
"y": y,
"x": wd.toScale(x),
"y": wd.toScale(y),
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/doubleTap")
return
@@ -400,8 +415,8 @@ func (wd *wdaDriver) TouchAndHold(x, y int, second ...float64) error {
func (wd *wdaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
// [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)]
data := map[string]interface{}{
"x": x,
"y": y,
"x": wd.toScale(x),
"y": wd.toScale(y),
}
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
@@ -411,31 +426,31 @@ func (wd *wdaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err err
return
}
func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)]
data := map[string]interface{}{
"fromX": fromX,
"fromY": fromY,
"toX": toX,
"toY": toY,
"fromX": wd.toScale(fromX),
"fromY": wd.toScale(fromY),
"toX": wd.toScale(toX),
"toY": wd.toScale(toY),
}
// new data options in post data for extra WDA configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = wd.httpPOST(newData, "/session", wd.sessionId, "/wda/dragfromtoforduration")
return
}
func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
return wd.DragFloat(fromX, fromY, toX, toY, options...)
}
@@ -462,37 +477,37 @@ func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
return
}
func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) {
func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
data := map[string]interface{}{"value": strings.Split(text, "")}
// new data options in post data for extra WDA configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = wd.httpPOST(newData, "/session", wd.sessionId, "/wda/keys")
return
}
func (wd *wdaDriver) Input(text string, options ...DataOption) (err error) {
func (wd *wdaDriver) Input(text string, options ...ActionOption) (err error) {
return wd.SendKeys(text, options...)
}
// PressBack simulates a short press on the BACK button.
func (wd *wdaDriver) PressBack(options ...DataOption) (err error) {
func (wd *wdaDriver) PressBack(options ...ActionOption) (err error) {
windowSize, err := wd.WindowSize()
if err != nil {
return
}
data := map[string]interface{}{
"fromX": float64(windowSize.Width) * 0,
"fromY": float64(windowSize.Height) * 0.5,
"toX": float64(windowSize.Width) * 0.6,
"toY": float64(windowSize.Height) * 0.5,
"fromX": wd.toScale(float64(windowSize.Width) * 0),
"fromY": wd.toScale(float64(windowSize.Height) * 0.5),
"toX": wd.toScale(float64(windowSize.Width) * 0.6),
"toY": wd.toScale(float64(windowSize.Height) * 0.5),
}
// new data options in post data for extra WDA configurations
newData := NewData(data, options...)
newData := mergeDataWithOptions(data, options...)
_, err = wd.httpPOST(newData, "/session", wd.sessionId, "/wda/dragfromtoforduration")
return

View File

@@ -68,6 +68,17 @@ func TestNewUSBDriver(t *testing.T) {
// t.Log(driver.IsWdaHealthy())
}
func TestDriver_DeviceScaleRatio(t *testing.T) {
setup(t)
scaleRatio, err := driver.Scale()
if err != nil {
t.Fatal(err)
}
t.Log(scaleRatio)
}
func Test_remoteWD_DeleteSession(t *testing.T) {
setup(t)

View File

@@ -10,9 +10,9 @@ func TestDriverExtOCR(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
checkErr(t, err)
x, y, width, height, err := driverExt.FindTextByOCR("抖音")
point, err := driverExt.FindScreenText("抖音")
checkErr(t, err)
t.Logf("x: %v, y: %v, width: %v, height: %v", x, y, width, height)
driverExt.Driver.TapFloat(x+width*0.5, y+height*0.5-20)
t.Logf("point.X: %v, point.Y: %v", point.X, point.Y)
driverExt.Driver.TapFloat(point.X, point.Y-20)
}

View File

@@ -7,7 +7,7 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
"strings"
"regexp"
"time"
"github.com/pkg/errors"
@@ -28,71 +28,216 @@ type OCRResult struct {
Points []PointF `json:"points"`
}
type ResponseOCR struct {
Code int `json:"code"`
Message string `json:"message"`
OCRResult []OCRResult `json:"ocrResult"`
type OCRResults []OCRResult
func (o OCRResults) ToOCRTexts() (ocrTexts OCRTexts) {
for _, ocrResult := range o {
rect := image.Rectangle{
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
Min: image.Point{
X: int(ocrResult.Points[0].X),
Y: int(ocrResult.Points[0].Y),
},
Max: image.Point{
X: int(ocrResult.Points[2].X),
Y: int(ocrResult.Points[2].Y),
},
}
ocrText := OCRText{
Text: ocrResult.Text,
Rect: rect,
}
ocrTexts = append(ocrTexts, ocrText)
}
return
}
type veDEMOCRService struct{}
type ImageResult struct {
imagePath string
URL string `json:"url"` // image uploaded url
OCRResult OCRResults `json:"ocrResult"` // OCR texts
LiveType string `json:"liveType"` // 直播间类型
}
func newVEDEMOCRService() (*veDEMOCRService, error) {
type ImageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Result ImageResult `json:"result"`
}
type OCRText struct {
Text string
Rect image.Rectangle
}
func (t OCRText) Center() PointF {
return getRectangleCenterPoint(t.Rect)
}
type OCRTexts []OCRText
func (t OCRTexts) texts() (texts []string) {
for _, text := range t {
texts = append(texts, text.Text)
}
return texts
}
func (t OCRTexts) FilterScope(scope AbsScope) (results OCRTexts) {
for _, ocrText := range t {
rect := ocrText.Rect
// check if text in scope
if len(scope) == 4 {
if rect.Min.X < scope[0] ||
rect.Min.Y < scope[1] ||
rect.Max.X > scope[2] ||
rect.Max.Y > scope[3] {
// not in scope
continue
}
}
results = append(results, ocrText)
}
return
}
func (t OCRTexts) FindText(text string, options ...ActionOption) (
result OCRText, err error) {
actionOptions := NewActionOptions(options...)
var results []OCRText
for _, ocrText := range t.FilterScope(actionOptions.AbsScope) {
if actionOptions.Regex {
// regex on, check if match regex
if !regexp.MustCompile(text).MatchString(ocrText.Text) {
continue
}
} else {
// regex off, check if match exactly
if ocrText.Text != text {
continue
}
}
results = append(results, ocrText)
}
if len(results) == 0 {
return OCRText{}, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("text %s not found in %v", text, t.texts()))
}
// get index
idx := actionOptions.Index
if idx < 0 {
idx = len(results) + idx
}
// index out of range
if idx >= len(results) || idx < 0 {
return OCRText{}, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("text %s found %d, index %d out of range", text, len(results), idx))
}
return results[idx], nil
}
func (t OCRTexts) FindTexts(texts []string, options ...ActionOption) (
results OCRTexts, err error) {
for _, text := range texts {
ocrText, err := t.FindText(text, options...)
if err != nil {
continue
}
results = append(results, ocrText)
}
if len(results) != len(texts) {
return nil, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("texts %s not found in %v", texts, t.texts()))
}
return results, nil
}
func newVEDEMImageService(actions ...string) (*veDEMImageService, error) {
if err := checkEnv(); err != nil {
return nil, err
}
return &veDEMOCRService{}, nil
if len(actions) == 0 {
actions = []string{"ocr"}
}
return &veDEMImageService{
actions: actions,
}, nil
}
func checkEnv() error {
if env.VEDEM_OCR_URL == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_OCR_URL missed")
}
if env.VEDEM_OCR_AK == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_OCR_AK missed")
}
if env.VEDEM_OCR_SK == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_OCR_SK missed")
}
return nil
// veDEMImageService implements IImageService interface
// actions:
// ocr - get ocr texts
// upload - get image uploaded url
// liveType - get live type
// popup - get popup windows
// close - get close popup
type veDEMImageService struct {
actions []string
}
func (s *veDEMOCRService) getOCRResult(imageBuf *bytes.Buffer) ([]OCRResult, error) {
func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer) (
imageResult ImageResult, err error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
bodyWriter.WriteField("withDet", "true")
// bodyWriter.WriteField("timestampOnly", "true")
for _, action := range s.actions {
bodyWriter.WriteField("actions", action)
}
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
if err != nil {
return nil, errors.Wrap(code.OCRRequestError,
err = errors.Wrap(code.OCRRequestError,
fmt.Sprintf("create form file error: %v", err))
return
}
size, err := formWriter.Write(imageBuf.Bytes())
if err != nil {
return nil, errors.Wrap(code.OCRRequestError,
err = errors.Wrap(code.OCRRequestError,
fmt.Sprintf("write form error: %v", err))
return
}
err = bodyWriter.Close()
if err != nil {
return nil, errors.Wrap(code.OCRRequestError,
err = errors.Wrap(code.OCRRequestError,
fmt.Sprintf("close body writer error: %v", err))
return
}
req, err := http.NewRequest("POST", env.VEDEM_OCR_URL, bodyBuf)
req, err := http.NewRequest("POST", env.VEDEM_IMAGE_URL, bodyBuf)
if err != nil {
return nil, errors.Wrap(code.OCRRequestError,
err = errors.Wrap(code.OCRRequestError,
fmt.Sprintf("construct request error: %v", err))
return
}
token := builtin.Sign("auth-v2", env.VEDEM_OCR_AK, env.VEDEM_OCR_SK, bodyBuf.Bytes())
signToken := "UNSIGNED-PAYLOAD"
token := builtin.Sign("auth-v2", env.VEDEM_IMAGE_AK, env.VEDEM_IMAGE_SK, []byte(signToken))
req.Header.Add("Agw-Auth", token)
req.Header.Add("Agw-Auth-Content", signToken)
req.Header.Add("Content-Type", bodyWriter.FormDataContentType())
// ppe
// req.Header.Add("x-use-ppe", "1")
// req.Header.Add("x-tt-env", "ppe_vedem_algorithm")
var resp *http.Response
// retry 3 times
for i := 1; i <= 3; i++ {
start := time.Now()
resp, err = client.Do(req)
elapsed := time.Since(start)
var logID string
if resp != nil {
logID = getLogID(resp.Header)
@@ -100,42 +245,73 @@ func (s *veDEMOCRService) getOCRResult(imageBuf *bytes.Buffer) ([]OCRResult, err
if err == nil && resp.StatusCode == http.StatusOK {
log.Debug().
Str("X-TT-LOGID", logID).
Int("imageBufSize", size).
Int("image_bytes", size).
Float64("elapsed_seconds", elapsed.Seconds()).
Msg("request OCR service success")
break
}
log.Error().Err(err).
Str("X-TT-LOGID", logID).
Int("imageBufSize", size).
Msgf("request OCR service failed, retry %d", i)
Msgf("request veDEM OCR service failed, retry %d", i)
time.Sleep(1 * time.Second)
}
if resp == nil {
return nil, code.OCRServiceConnectionError
err = code.OCRServiceConnectionError
return
}
defer resp.Body.Close()
results, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(code.OCRResponseError,
err = errors.Wrap(code.OCRResponseError,
fmt.Sprintf("read response body error: %v", err))
return
}
if resp.StatusCode != http.StatusOK {
return nil, errors.Wrap(code.OCRResponseError,
err = errors.Wrap(code.OCRResponseError,
fmt.Sprintf("unexpected response status code: %d, results: %v",
resp.StatusCode, string(results)))
return
}
var ocrResult ResponseOCR
err = json.Unmarshal(results, &ocrResult)
var imageResponse ImageResponse
err = json.Unmarshal(results, &imageResponse)
if err != nil {
return nil, errors.Wrap(code.OCRResponseError,
fmt.Sprintf("json unmarshal response body error: %v", err))
log.Error().Err(err).
Str("response", string(results)).
Msg("json unmarshal veDEM image response body failed")
err = errors.Wrap(code.OCRResponseError,
"json unmarshal veDEM image response body error")
return
}
return ocrResult.OCRResult, nil
if imageResponse.Code != 0 {
log.Error().
Int("code", imageResponse.Code).
Str("message", imageResponse.Message).
Msg("request veDEM OCR service failed")
}
imageResult = imageResponse.Result
log.Debug().Interface("imageResult", imageResult).Msg("get image data by veDEM")
return imageResult, nil
}
func checkEnv() error {
if env.VEDEM_IMAGE_URL == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_IMAGE_URL missed")
}
log.Info().Str("VEDEM_IMAGE_URL", env.VEDEM_IMAGE_URL).Msg("get env")
if env.VEDEM_IMAGE_AK == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_IMAGE_AK missed")
}
if env.VEDEM_IMAGE_SK == "" {
return errors.Wrap(code.OCREnvMissedError, "VEDEM_IMAGE_SK missed")
}
return nil
}
func getLogID(header http.Header) string {
@@ -150,210 +326,72 @@ func getLogID(header http.Header) string {
return logID[0]
}
type OCRText struct {
Text string
Rect image.Rectangle
type IImageService interface {
// GetImage returns image result including ocr texts, uploaded image url, etc
GetImage(imageBuf *bytes.Buffer) (imageResult ImageResult, err error)
}
type OCRTexts []OCRText
func (t OCRTexts) Texts() (texts []string) {
for _, text := range t {
texts = append(texts, text.Text)
}
return texts
}
func (s *veDEMOCRService) GetTexts(imageBuf *bytes.Buffer, options ...DataOption) (
ocrTexts OCRTexts, err error) {
ocrResults, err := s.getOCRResult(imageBuf)
if err != nil {
log.Error().Err(err).Msg("getOCRResult failed")
return
}
dataOptions := NewDataOptions(options...)
for _, ocrResult := range ocrResults {
rect := image.Rectangle{
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
Min: image.Point{
X: int(ocrResult.Points[0].X),
Y: int(ocrResult.Points[0].Y),
},
Max: image.Point{
X: int(ocrResult.Points[2].X),
Y: int(ocrResult.Points[2].Y),
},
}
// check if text in scope
if rect.Min.X < dataOptions.Scope[0] || rect.Max.X > dataOptions.Scope[2] ||
rect.Min.Y < dataOptions.Scope[1] || rect.Max.Y > dataOptions.Scope[3] {
// not in scope
continue
}
ocrTexts = append(ocrTexts, OCRText{
Text: ocrResult.Text,
Rect: rect,
})
}
return
}
func (s *veDEMOCRService) FindText(text string, imageBuf *bytes.Buffer, options ...DataOption) (
rect image.Rectangle, err error) {
ocrTexts, err := s.GetTexts(imageBuf, options...)
if err != nil {
log.Error().Err(err).Msg("GetTexts failed")
return
}
dataOptions := NewDataOptions(options...)
var rects []image.Rectangle
for _, ocrText := range ocrTexts {
rect = ocrText.Rect
// not contains text
if !strings.Contains(ocrText.Text, text) {
continue
}
rects = append(rects, rect)
// contains text while not match exactly
if ocrText.Text != text {
continue
}
// match exactly, and not specify index, return the first one
if dataOptions.Index == 0 {
return rect, nil
}
}
if len(rects) == 0 {
return image.Rectangle{}, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("text %s not found in %v", text, ocrTexts.Texts()))
}
// get index
idx := dataOptions.Index
if idx > 0 {
// NOTICE: index start from 1
idx = idx - 1
} else if idx < 0 {
idx = len(rects) + idx
}
// index out of range
if idx >= len(rects) {
return image.Rectangle{}, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("text %s found %d, index %d out of range", text, len(rects), idx))
}
return rects[idx], nil
}
func (s *veDEMOCRService) FindTexts(texts []string, imageBuf *bytes.Buffer, options ...DataOption) (
rects []image.Rectangle, err error) {
ocrTexts, err := s.GetTexts(imageBuf, options...)
if err != nil {
log.Error().Err(err).Msg("GetTexts failed")
return
}
var success bool
for _, text := range texts {
var found bool
for _, ocrText := range ocrTexts {
rect := ocrText.Rect
// not contains text
if !strings.Contains(ocrText.Text, text) {
continue
}
found = true
rects = append(rects, rect)
break
}
if !found {
rects = append(rects, image.Rectangle{})
}
success = found || success
}
if !success {
return rects, errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("texts %s not found in %v", texts, ocrTexts.Texts()))
}
return rects, nil
}
type OCRService interface {
GetTexts(imageBuf *bytes.Buffer, options ...DataOption) (ocrTexts OCRTexts, err error)
FindText(text string, imageBuf *bytes.Buffer, options ...DataOption) (rect image.Rectangle, err error)
FindTexts(texts []string, imageBuf *bytes.Buffer, options ...DataOption) (rects []image.Rectangle, err error)
}
func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err error) {
// GetScreenResult takes a screenshot, returns the image recognization result
func (dExt *DriverExt) GetScreenResult() (imageResult ImageResult, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
var imagePath string
if bufSource, imagePath, err = dExt.TakeScreenShot(
builtin.GenNameWithTimestamp("%d_ocr")); err != nil {
return
}
ocrTexts, err := dExt.ocrService.GetTexts(bufSource, options...)
imageResult, err = dExt.ImageService.GetImage(bufSource)
if err != nil {
log.Error().Err(err).Msg("GetTexts failed")
log.Error().Err(err).Msg("GetScreenResult failed")
return
}
return ocrTexts, nil
imageUrl := imageResult.URL
if imageUrl != "" {
dExt.cacheStepData.screenShotsUrls[imagePath] = imageUrl
log.Debug().Str("imagePath", imagePath).Str("imageUrl", imageUrl).Msg("log screenshot")
}
dExt.cacheStepData.screenResults[imagePath] = &ScreenResult{
Texts: imageResult.OCRResult.ToOCRTexts(),
}
imageResult.imagePath = imagePath
return imageResult, nil
}
func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, y, width, height float64, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
func (dExt *DriverExt) GetScreenTexts() (ocrTexts OCRTexts, err error) {
imageResult, err := dExt.GetScreenResult()
if err != nil {
return
}
return imageResult.OCRResult.ToOCRTexts(), nil
}
func (dExt *DriverExt) FindScreenText(text string, options ...ActionOption) (point PointF, err error) {
ocrTexts, err := dExt.GetScreenTexts()
if err != nil {
return
}
rect, err := dExt.ocrService.FindText(ocrText, bufSource, options...)
result, err := ocrTexts.FindText(text, dExt.ParseActionOptions(options...)...)
if err != nil {
log.Warn().Msgf("FindText failed: %s", err.Error())
return
}
point = result.Center()
log.Info().Str("ocrText", ocrText).
Interface("rect", rect).Msgf("FindTextByOCR success")
x, y, width, height = dExt.MappingToRectInUIKit(rect)
log.Info().Str("text", text).
Interface("point", point).Msgf("FindScreenText success")
return
}
func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption) (points [][]float64, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
return
func getRectangleCenterPoint(rect image.Rectangle) (point PointF) {
x, y := float64(rect.Min.X), float64(rect.Min.Y)
width, height := float64(rect.Dx()), float64(rect.Dy())
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
rects, err := dExt.ocrService.FindTexts(ocrTexts, bufSource, options...)
if err != nil {
log.Warn().Msgf("FindTexts failed: %s", err.Error())
return
}
log.Info().Interface("ocrTexts", ocrTexts).
Interface("rects", rects).Msgf("FindTextsByOCR success")
for _, rect := range rects {
x, y, width, height := dExt.MappingToRectInUIKit(rect)
points = append(points, []float64{x, y, width, height})
}
return
return point
}

View File

@@ -10,26 +10,22 @@ import (
)
func checkOCR(buff *bytes.Buffer) error {
service, err := newVEDEMOCRService()
service, err := newVEDEMImageService()
if err != nil {
return err
}
ocrResults, err := service.getOCRResult(buff)
imageResult, err := service.GetImage(buff)
if err != nil {
return err
}
fmt.Println(ocrResults)
fmt.Println(fmt.Sprintf("imageResult: %v", imageResult))
return nil
}
func TestOCRWithScreenshot(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
if err != nil {
t.Fatal(err)
}
setupAndroid(t)
raw, err := driver.Driver.Screenshot()
raw, err := driverExt.Driver.Screenshot()
if err != nil {
t.Fatal(err)
}

View File

@@ -71,7 +71,6 @@ func (dExt *DriverExt) Debug(dm DebugMode) {
func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) {
newExt = new(DriverExt)
newExt.Driver = dExt.Driver
newExt.scale = dExt.scale
newExt.matchMode = dExt.matchMode
newExt.threshold = threshold
return
@@ -80,7 +79,6 @@ func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt)
func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) {
newExt = new(DriverExt)
newExt.Driver = dExt.Driver
newExt.scale = dExt.scale
newExt.matchMode = matchMode
newExt.threshold = dExt.threshold
return
@@ -103,7 +101,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
if bufSearch, err = getBufFromDisk(search); err != nil {
return nil, err
}
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
if bufSource, _, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("%d_cv")); err != nil {
return nil, err
}
@@ -113,25 +111,19 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
return
}
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, err error) {
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...ActionOption) (point PointF, err error) {
var bufSource, bufSearch *bytes.Buffer
if bufSearch, err = getBufFromDisk(imagePath); err != nil {
return 0, 0, 0, 0, err
}
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
return 0, 0, 0, 0, err
return PointF{}, err
}
var rect image.Rectangle
if rect, err = FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.threshold), TemplateMatchMode(dExt.matchMode)); err != nil {
return 0, 0, 0, 0, err
return PointF{}, err
}
// if rect, err = dExt.findImgRect(search); err != nil {
// return 0, 0, 0, 0, err
// }
x, y, width, height = dExt.MappingToRectInUIKit(rect)
return
point = getRectangleCenterPoint(rect)
return point, nil
}
func getBufFromDisk(name string) (*bytes.Buffer, error) {

View File

@@ -17,7 +17,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
return
}
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, err error) {
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...ActionOption) (point PointF, err error) {
log.Fatal().Msg("opencv is not supported")
return
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
)
@@ -16,7 +15,7 @@ func assertRelative(p float64) bool {
}
// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY]
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...DataOption) error {
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...ActionOption) error {
width := dExt.windowSize.Width
height := dExt.windowSize.Height
@@ -34,7 +33,7 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...)
}
func (dExt *DriverExt) SwipeTo(direction string, options ...DataOption) (err error) {
func (dExt *DriverExt) SwipeTo(direction string, options ...ActionOption) (err error) {
switch direction {
case "up":
return dExt.SwipeUp(options...)
@@ -48,64 +47,28 @@ func (dExt *DriverExt) SwipeTo(direction string, options ...DataOption) (err err
return fmt.Errorf("unexpected direction: %s", direction)
}
func (dExt *DriverExt) SwipeUp(options ...DataOption) (err error) {
func (dExt *DriverExt) SwipeUp(options ...ActionOption) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, options...)
}
func (dExt *DriverExt) SwipeDown(options ...DataOption) (err error) {
func (dExt *DriverExt) SwipeDown(options ...ActionOption) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, options...)
}
func (dExt *DriverExt) SwipeLeft(options ...DataOption) (err error) {
func (dExt *DriverExt) SwipeLeft(options ...ActionOption) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, options...)
}
func (dExt *DriverExt) SwipeRight(options ...DataOption) (err error) {
func (dExt *DriverExt) SwipeRight(options ...ActionOption) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, options...)
}
type Action func(driver *DriverExt) error
// findCondition indicates the condition to find a UI element
// foundAction indicates the action to do after a UI element is found
func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, foundAction Action, options ...DataOption) error {
dataOptions := NewDataOptions(options...)
maxRetryTimes := dataOptions.MaxRetryTimes
interval := dataOptions.Interval
for i := 0; i < maxRetryTimes; i++ {
if err := findCondition(dExt); err == nil {
// do action after found
return foundAction(dExt)
}
if d, ok := direction.(string); ok {
if err := dExt.SwipeTo(d); err != nil {
log.Error().Err(err).Msgf("swipe %s failed", d)
}
} else if d, ok := direction.([]float64); ok {
if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3]); err != nil {
log.Error().Err(err).Msgf("swipe %v failed", d)
}
} else if d, ok := direction.([]interface{}); ok {
sx, _ := builtin.Interface2Float64(d[0])
sy, _ := builtin.Interface2Float64(d[1])
ex, _ := builtin.Interface2Float64(d[2])
ey, _ := builtin.Interface2Float64(d[3])
if err := dExt.SwipeRelative(sx, sy, ex, ey); err != nil {
log.Error().Err(err).Msgf("swipe (%v, %v) to (%v, %v) failed", sx, sy, ex, ey)
}
}
// wait for swipe action to completed and content to load completely
time.Sleep(time.Duration(1000*interval) * time.Millisecond)
}
return errors.Wrap(code.OCRTextNotFoundError,
fmt.Sprintf("swipe %v %d times, match condition failed", direction, maxRetryTimes))
}
func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error {
dataOptions := NewDataOptions(options...)
maxRetryTimes := dataOptions.MaxRetryTimes
interval := dataOptions.Interval
func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...ActionOption) error {
actionOptions := NewActionOptions(options...)
maxRetryTimes := actionOptions.MaxRetryTimes
interval := actionOptions.Interval
for i := 0; i < maxRetryTimes; i++ {
if err := findCondition(dExt); err == nil {
@@ -125,40 +88,74 @@ func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action,
fmt.Sprintf("loop %d times, match find condition failed", maxRetryTimes))
}
func (dExt *DriverExt) swipeToTapApp(appName string, action MobileAction) error {
if len(action.Scope) != 4 {
action.Scope = []float64{0, 0, 1, 1}
}
if len(action.Offset) != 2 {
action.Offset = []int{0, -25}
func (dExt *DriverExt) prepareSwipeAction(options ...ActionOption) func(d *DriverExt) error {
actionOptions := NewActionOptions(options...)
var swipeDirection interface{}
if actionOptions.Direction != nil {
swipeDirection = actionOptions.Direction
} else {
swipeDirection = "up" // default swipe up
}
identifierOption := WithDataIdentifier(action.Identifier)
indexOption := WithDataIndex(action.Index)
offsetOption := WithDataOffset(action.Offset[0], action.Offset[1])
scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3]))
// default to retry 5 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 5
if actionOptions.Steps == 0 {
actionOptions.Steps = 10
}
return func(d *DriverExt) error {
defer func() {
// wait for swipe action to completed and content to load completely
time.Sleep(time.Duration(1000*actionOptions.Interval) * time.Millisecond)
}()
if d, ok := swipeDirection.(string); ok {
// enum direction: up, down, left, right
if err := dExt.SwipeTo(d, options...); err != nil {
log.Error().Err(err).Msgf("swipe %s failed", d)
return err
}
} else if d, ok := swipeDirection.([]float64); ok {
// custom direction: [fromX, fromY, toX, toY]
if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3], options...); err != nil {
log.Error().Err(err).Msgf("swipe from (%v, %v) to (%v, %v) failed",
d[0], d[1], d[2], d[3])
return err
}
} else {
return fmt.Errorf("invalid swipe params %v", swipeDirection)
}
return nil
}
}
func (dExt *DriverExt) swipeToTapTexts(texts []string, options ...ActionOption) error {
if len(texts) == 0 {
return errors.New("no text to tap")
}
maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes)
waitTimeOption := WithDataWaitTime(action.WaitTime)
var point PointF
findAppAction := func(d *DriverExt) error {
return dExt.SwipeLeft()
}
findAppCondition := func(d *DriverExt) error {
findTexts := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY(appName, scopeOption, indexOption)
return err
ocrTexts, err := d.GetScreenTexts()
if err != nil {
return err
}
points, err := ocrTexts.FindTexts(texts, dExt.ParseActionOptions(options...)...)
if err != nil {
return err
}
point = points[0].Center() // FIXME
return nil
}
foundAppAction := func(d *DriverExt) error {
// click app to launch
return d.TapAbsXY(point.X, point.Y, identifierOption, offsetOption)
foundTextAction := func(d *DriverExt) error {
// tap text
return d.TapAbsXY(point.X, point.Y, options...)
}
findAction := dExt.prepareSwipeAction(options...)
return dExt.LoopUntil(findAction, findTexts, foundTextAction, options...)
}
func (dExt *DriverExt) swipeToTapApp(appName string, options ...ActionOption) error {
// go to home screen
if err := dExt.Driver.Homescreen(); err != nil {
return errors.Wrap(err, "go to home screen failed")
@@ -169,6 +166,8 @@ func (dExt *DriverExt) swipeToTapApp(appName string, action MobileAction) error
dExt.SwipeRight()
}
// swipe next screen until app found
return dExt.LoopUntil(findAppAction, findAppCondition, foundAppAction, maxRetryOption, waitTimeOption)
options = append(options, WithOffset(0, -25)) // tap app icon above the text
options = append(options, WithDirection("left"))
return dExt.swipeToTapTexts([]string{appName}, options...)
}

View File

@@ -6,43 +6,31 @@ import (
"testing"
)
func TestSwipeUntil(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
func TestAndroidSwipeAction(t *testing.T) {
setupAndroid(t)
swipeAction := driverExt.prepareSwipeAction(WithDirection("up"))
err := swipeAction(driverExt)
checkErr(t, err)
var point PointF
findApp := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY("抖音")
return err
}
foundAppAction := func(d *DriverExt) error {
// click app, launch douyin
return d.TapAbsXY(point.X, point.Y)
}
driverExt.Driver.Homescreen()
// swipe to first screen
for i := 0; i < 5; i++ {
driverExt.SwipeRight()
}
// swipe until app found
err = driverExt.SwipeUntil("left", findApp, foundAppAction, WithDataMaxRetryTimes(10))
checkErr(t, err)
findLive := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY("点击进入直播间")
return err
}
foundLiveAction := func(d *DriverExt) error {
// enter live room
return d.TapAbsXY(point.X, point.Y)
}
// swipe until live room found
err = driverExt.SwipeUntil("up", findLive, foundLiveAction, WithDataMaxRetryTimes(20))
swipeAction = driverExt.prepareSwipeAction(WithCustomDirection(0.5, 0.5, 0.5, 0.9))
err = swipeAction(driverExt)
checkErr(t, err)
}
func TestAndroidSwipeToTapApp(t *testing.T) {
setupAndroid(t)
err := driverExt.swipeToTapApp("抖音")
checkErr(t, err)
}
func TestAndroidSwipeToTapTexts(t *testing.T) {
setupAndroid(t)
err := driverExt.Driver.AppLaunch("com.ss.android.ugc.aweme")
checkErr(t, err)
err = driverExt.swipeToTapTexts([]string{"点击进入直播间", "直播中"}, WithDirection("up"))
checkErr(t, err)
}

View File

@@ -4,12 +4,12 @@ import (
"fmt"
)
func (dExt *DriverExt) TapAbsXY(x, y float64, options ...DataOption) error {
func (dExt *DriverExt) TapAbsXY(x, y float64, options ...ActionOption) error {
// tap on absolute coordinate [x, y]
return dExt.Driver.TapFloat(x, y, options...)
}
func (dExt *DriverExt) TapXY(x, y float64, options ...DataOption) error {
func (dExt *DriverExt) TapXY(x, y float64, options ...ActionOption) error {
// tap on [x, y] percent of window size
if x > 1 || y > 1 {
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
@@ -21,55 +21,12 @@ func (dExt *DriverExt) TapXY(x, y float64, options ...DataOption) error {
return dExt.TapAbsXY(x, y, options...)
}
func (dExt *DriverExt) GetTextXY(ocrText string, options ...DataOption) (point PointF, err error) {
x, y, width, height, err := dExt.FindTextByOCR(ocrText, options...)
func (dExt *DriverExt) TapByOCR(ocrText string, options ...ActionOption) error {
actionOptions := NewActionOptions(options...)
point, err := dExt.FindScreenText(ocrText, options...)
if err != nil {
return PointF{}, err
}
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
return point, nil
}
func (dExt *DriverExt) GetTextXYs(ocrText []string, options ...DataOption) (points []PointF, err error) {
ps, err := dExt.FindTextsByOCR(ocrText, options...)
if err != nil {
return nil, err
}
for _, point := range ps {
pointF := PointF{
X: point[0] + point[2]*0.5,
Y: point[1] + point[3]*0.5,
}
points = append(points, pointF)
}
return points, nil
}
func (dExt *DriverExt) GetImageXY(imagePath string, options ...DataOption) (point PointF, err error) {
x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, options...)
if err != nil {
return PointF{}, err
}
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
return point, nil
}
func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error {
dataOptions := NewDataOptions(options...)
point, err := dExt.GetTextXY(ocrText, options...)
if err != nil {
if dataOptions.IgnoreNotFoundError {
if actionOptions.IgnoreNotFoundError {
return nil
}
return err
@@ -78,12 +35,12 @@ func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error {
return dExt.TapAbsXY(point.X, point.Y, options...)
}
func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error {
dataOptions := NewDataOptions(options...)
func (dExt *DriverExt) TapByCV(imagePath string, options ...ActionOption) error {
actionOptions := NewActionOptions(options...)
point, err := dExt.GetImageXY(imagePath, options...)
point, err := dExt.FindImageRectInUIKit(imagePath, options...)
if err != nil {
if dataOptions.IgnoreNotFoundError {
if actionOptions.IgnoreNotFoundError {
return nil
}
return err
@@ -92,22 +49,22 @@ func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error {
return dExt.TapAbsXY(point.X, point.Y, options...)
}
func (dExt *DriverExt) Tap(param string, options ...DataOption) error {
return dExt.TapOffset(param, 0.5, 0.5, options...)
func (dExt *DriverExt) Tap(param string, options ...ActionOption) error {
return dExt.TapOffset(param, 0, 0, options...)
}
func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, options ...DataOption) (err error) {
dataOptions := NewDataOptions(options...)
func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
x, y, width, height, err := dExt.FindUIRectInUIKit(param, options...)
point, err := dExt.FindUIRectInUIKit(param, options...)
if err != nil {
if dataOptions.IgnoreNotFoundError {
if actionOptions.IgnoreNotFoundError {
return nil
}
return err
}
return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, options...)
return dExt.TapAbsXY(point.X+xOffset, point.Y+yOffset, options...)
}
func (dExt *DriverExt) DoubleTapXY(x, y float64) error {
@@ -122,14 +79,14 @@ func (dExt *DriverExt) DoubleTapXY(x, y float64) error {
}
func (dExt *DriverExt) DoubleTap(param string) (err error) {
return dExt.DoubleTapOffset(param, 0.5, 0.5)
return dExt.DoubleTapOffset(param, 0, 0)
}
func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64) (err error) {
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil {
point, err := dExt.FindUIRectInUIKit(param)
if err != nil {
return err
}
return dExt.Driver.DoubleTapFloat(x+width*xOffset, y+height*yOffset)
return dExt.Driver.DoubleTapFloat(point.X+xOffset, point.Y+yOffset)
}

View File

@@ -43,6 +43,6 @@ func TestDriverExt_TapWithOCR(t *testing.T) {
checkErr(t, err)
// 需要点击文字上方的图标
err = driverExt.TapOffset("抖音", 0.5, -1)
err = driverExt.TapOffset("抖音", 0, -20)
checkErr(t, err)
}

View File

@@ -0,0 +1,500 @@
package uixt
import (
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
)
type VideoStat struct {
configs *VideoCrawlerConfigs
timer *time.Timer
FeedCount int `json:"feed_count"`
FeedStat map[string]int `json:"feed_stat"` // 分类统计 feed 数量:视频/图文/广告/特效/模板/购物
LiveCount int `json:"live_count"`
LiveStat map[string]int `json:"live_stat"` // 分类统计 live 数量:秀场/游戏/电商/多人
}
func (s *VideoStat) isFeedTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range s.configs.Feed.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", s.FeedCount).
Interface("current_stat", s.FeedStat).
Int("target_total", s.configs.Feed.TargetCount).
Interface("target_stat", targetStat).
Msg("display feed crawler progress")
// check total feed count
if s.FeedCount < s.configs.Feed.TargetCount {
return false
}
// check each feed type's count
for _, targetLabel := range s.configs.Feed.TargetLabels {
if s.FeedStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (s *VideoStat) isLiveTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range s.configs.Live.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", s.LiveCount).
Interface("current_stat", s.LiveStat).
Int("target_total", s.configs.Live.TargetCount).
Interface("target_stat", targetStat).
Msg("display live crawler progress")
// check total live count
if s.LiveCount < s.configs.Live.TargetCount {
return false
}
// check each live type's count
for _, targetLabel := range s.configs.Live.TargetLabels {
if s.LiveStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (s *VideoStat) isTargetAchieved() bool {
return s.isFeedTargetAchieved() && s.isLiveTargetAchieved()
}
// incrFeed increases feed count and feed stat
func (s *VideoStat) incrFeed(screenResult *ScreenResult, driverExt *DriverExt) error {
// feed author
actionOptions := []ActionOption{
WithRegex(true),
driverExt.GenAbsScope(0, 0.5, 1, 1).Option(),
}
if ocrText, err := screenResult.Texts.FindText("^@", actionOptions...); err == nil {
log.Debug().Str("author", ocrText.Text).Msg("found feed author")
screenResult.Tags = append(screenResult.Tags, ocrText.Text)
}
for _, targetLabel := range s.configs.Feed.TargetLabels {
scope := targetLabel.Scope
actionOptions := []ActionOption{
WithRegex(targetLabel.Regex),
driverExt.GenAbsScope(scope[0], scope[1], scope[2], scope[3]).Option(),
}
if _, err := screenResult.Texts.FindText(targetLabel.Text, actionOptions...); err == nil {
key := targetLabel.Text
if _, ok := s.FeedStat[key]; !ok {
s.FeedStat[key] = 0
}
s.FeedStat[key]++
screenResult.Tags = append(screenResult.Tags, key)
}
}
// add popularity data for feed
popularityData := screenResult.Texts.FilterScope(driverExt.GenAbsScope(0.8, 0.5, 1, 0.8))
if len(popularityData) != 4 {
log.Warn().Interface("popularity", popularityData).Msg("get feed popularity data failed")
} else {
screenResult.Popularity = Popularity{
Stars: popularityData[0].Text,
Comments: popularityData[1].Text,
Favorites: popularityData[2].Text,
Shares: popularityData[3].Text,
}
}
log.Info().Strs("tags", screenResult.Tags).
Interface("popularity", screenResult.Popularity).
Msg("found feed success")
s.FeedCount++
return nil
}
// incrLive increases live count and live stat
func (s *VideoStat) incrLive(screenResult *ScreenResult, driverExt *DriverExt) error {
// TODO: check live type
// add popularity data for live
popularityData := screenResult.Texts.FilterScope(driverExt.GenAbsScope(0.7, 0.05, 1, 0.15))
if len(popularityData) != 1 {
log.Warn().Interface("popularity", popularityData).Msg("get live popularity data failed")
} else {
screenResult.Popularity = Popularity{
LiveUsers: popularityData[0].Text,
}
}
log.Info().Strs("tags", screenResult.Tags).
Interface("popularity", screenResult.Popularity).
Msg("found live success")
s.LiveCount++
return nil
}
type TargetLabel struct {
Text string `json:"text"`
Scope Scope `json:"scope"`
Regex bool `json:"regex"`
Target int `json:"target"` // target count for current label
}
type FeedConfig struct {
TargetCount int `json:"target_count"`
TargetLabels []TargetLabel `json:"target_labels"`
SleepRandom []interface{} `json:"sleep_random"`
}
type LiveConfig struct {
TargetCount int `json:"target_count"`
TargetLabels []TargetLabel `json:"target_labels"`
SleepRandom []interface{} `json:"sleep_random"`
}
type VideoCrawlerConfigs struct {
AppPackageName string `json:"app_package_name"`
Timeout int `json:"timeout"` // seconds
Feed FeedConfig `json:"feed"`
Live LiveConfig `json:"live"`
}
var androidActivities = map[string]map[string]string{
// DY
"com.ss.android.ugc.aweme": {
"feed": ".splash.SplashActivity",
"live": ".live.LivePlayActivity",
},
// KS
"com.smile.gifmaker": {
"feed": "com.yxcorp.gifshow.HomeActivity",
"live": "com.kuaishou.live.core.basic.activity.LiveSlideActivity",
},
// TODO: SPH, XHS
}
type LiveCrawler struct {
driver *DriverExt
configs *VideoCrawlerConfigs // target video count
currentStat *VideoStat // current video stat
}
func (l *LiveCrawler) checkLiveVideo(texts OCRTexts) (enterPoint PointF, yes bool) {
// 预览流入口DY/KS
points, err := texts.FindTexts([]string{".*点击进入直播间"}, WithRegex(true))
if err == nil {
return points[0].Center(), true
}
// 预览流入口KS
points, err = texts.FindTexts([]string{"和主播聊聊天.*"}, WithRegex(true))
if err == nil {
point := points[0].Center()
enterPoint = PointF{
X: point.X,
Y: point.Y - 100,
}
return enterPoint, true
}
// TODO: 头像入口
return PointF{}, false
}
// run live video crawler
func (l *LiveCrawler) Run(driver *DriverExt, enterPoint PointF) error {
log.Info().Msg("enter live room")
if err := driver.TapAbsXY(enterPoint.X, enterPoint.Y); err != nil {
log.Error().Err(err).Msg("tap live video failed")
return err
}
time.Sleep(5 * time.Second)
for !l.currentStat.isLiveTargetAchieved() {
select {
case <-l.currentStat.timer.C:
log.Warn().Msg("timeout in live crawler")
return errors.Wrap(code.TimeoutError, "live crawler timeout")
case <-l.driver.interruptSignal:
log.Warn().Msg("interrupted in live crawler")
return errors.Wrap(code.InterruptError, "live crawler interrupted")
default:
// check if live room
if err := l.driver.assertActivity(l.configs.AppPackageName, "live"); err != nil {
return err
}
// swipe to next live video
err := l.driver.SwipeUp()
if err != nil {
log.Error().Err(err).Msg("swipe up failed")
// TODO: retry maximum 3 times
continue
}
// sleep custom random time
if err := sleepRandom(l.configs.Live.SleepRandom); err != nil {
log.Error().Err(err).Msg("sleep random failed")
}
// take screenshot and get screen texts by OCR
imageResult, err := l.driver.GetScreenResult()
if err != nil {
log.Error().Err(err).Msg("OCR GetTexts failed")
time.Sleep(3 * time.Second)
continue
}
screenResult := l.driver.cacheStepData.screenResults[imageResult.imagePath]
screenResult.Tags = []string{"live"}
if imageResult.LiveType != "" {
screenResult.Tags = append(screenResult.Tags, imageResult.LiveType)
}
// check live type and incr live count
if err := l.currentStat.incrLive(screenResult, l.driver); err != nil {
log.Error().Err(err).Msg("incr live failed")
}
}
}
log.Info().Msg("live count achieved, exit live room")
return l.exitLiveRoom()
}
func (l *LiveCrawler) exitLiveRoom() error {
for i := 0; i < 3; i++ {
l.driver.SwipeRelative(0.1, 0.5, 0.9, 0.5)
time.Sleep(2 * time.Second)
// check if back to feed page
if err := l.driver.assertActivity(l.configs.AppPackageName, "feed"); err == nil {
return nil
}
}
// exit live room failed, while video count achieved
if l.currentStat.isTargetAchieved() {
return nil
}
// click X button on upper-right corner
if err := l.driver.TapXY(0.95, 0.05); err == nil {
log.Info().Msg("tap X button on upper-right corner to exit live room")
time.Sleep(2 * time.Second)
// check if back to feed page
if err := l.driver.assertActivity(l.configs.AppPackageName, "feed"); err == nil {
return nil
}
}
return errors.New("exit live room failed")
}
func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
// set default sleep random strategy if not set
if configs.Feed.SleepRandom == nil {
configs.Feed.SleepRandom = []interface{}{1, 5}
}
if configs.Live.SleepRandom == nil {
configs.Live.SleepRandom = []interface{}{10, 15}
}
currVideoStat := &VideoStat{
configs: configs,
FeedCount: 0,
FeedStat: make(map[string]int),
LiveCount: 0,
LiveStat: make(map[string]int),
}
defer func() {
dExt.cacheStepData.videoStat = currVideoStat
}()
// launch app
if configs.AppPackageName != "" {
if err = dExt.Driver.AppLaunch(configs.AppPackageName); err != nil {
return err
}
time.Sleep(5 * time.Second)
} else {
app, err := dExt.Driver.GetForegroundApp()
if err != nil {
return err
}
log.Info().
Str("packageName", app.PackageName).
Str("activity", app.Activity).
Msg("start to video crawler for current foreground app")
configs.AppPackageName = app.PackageName
}
liveCrawler := LiveCrawler{
driver: dExt,
configs: configs,
currentStat: currVideoStat,
}
// loop until target count achieved or timeout
// the main loop is feed crawler
currVideoStat.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
for {
select {
case <-currVideoStat.timer.C:
log.Warn().Msg("timeout in feed crawler")
return errors.Wrap(code.TimeoutError, "feed crawler timeout")
case <-dExt.interruptSignal:
log.Warn().Msg("interrupted in feed crawler")
return errors.Wrap(code.InterruptError, "feed crawler interrupted")
default:
// take screenshot and get screen texts by OCR
imageResult, err := dExt.GetScreenResult()
if err != nil {
log.Error().Err(err).Msg("OCR GetTexts failed")
time.Sleep(3 * time.Second)
continue
}
screenResult := dExt.cacheStepData.screenResults[imageResult.imagePath]
// automatic handling of pop-up windows
if err := dExt.autoPopupHandler(screenResult); err != nil {
log.Error().Err(err).Msg("auto handle popup failed")
return err
}
// check if live video && run live crawler
texts := imageResult.OCRResult.ToOCRTexts()
if enterPoint, isLive := liveCrawler.checkLiveVideo(texts); isLive {
log.Info().Msg("live video found")
if !liveCrawler.currentStat.isLiveTargetAchieved() {
if err := liveCrawler.Run(dExt, enterPoint); err != nil {
if errors.Is(err, code.TimeoutError) || errors.Is(err, code.InterruptError) {
return err
}
log.Error().Err(err).Msg("run live crawler failed, continue")
continue
}
}
screenResult.Tags = []string{"live-preview"}
} else {
screenResult.Tags = []string{"feed"}
// check feed type and incr feed count
if err := currVideoStat.incrFeed(screenResult, dExt); err != nil {
log.Error().Err(err).Msg("incr feed failed")
}
}
// sleep custom random time
if err := sleepRandom(configs.Feed.SleepRandom); err != nil {
log.Error().Err(err).Msg("sleep random failed")
}
// check if target count achieved
if currVideoStat.isTargetAchieved() {
log.Info().Msg("target count achieved, exit crawler")
return nil
}
// swipe to next feed video
log.Info().Msg("swipe to next feed video")
if err = dExt.SwipeUp(); err != nil {
log.Error().Err(err).Msg("swipe up failed")
return err
}
time.Sleep(1 * time.Second)
// check if feed page
if err := dExt.assertActivity(configs.AppPackageName, "feed"); err != nil {
return err
}
}
}
}
func (dExt *DriverExt) assertActivity(packageName, activityType string) error {
log.Debug().Str("pacakge_name", packageName).
Str("activity_type", activityType).Msg("assert activity")
app, err := dExt.Driver.GetForegroundApp()
if err != nil {
log.Warn().Err(err).Msg("get foreground app failed, skip app/activity assertion")
return nil // Notice: ignore error when get foreground app failed
}
if app.PackageName != packageName {
return errors.Wrap(code.MobileUIAppNotInForegroundError,
fmt.Sprintf("foreground app %s, expect %s", app.PackageName, packageName))
}
var expectActivity string
if activities, ok := androidActivities[app.PackageName]; ok {
if activity, ok := activities[activityType]; ok {
if strings.HasSuffix(app.Activity, activity) {
return nil
}
expectActivity = activity
}
}
log.Error().Interface("app", app.AppBaseInfo).Msg("app activity not match")
return errors.Wrap(code.MobileUIActivityNotMatchError,
fmt.Sprintf("foreground activity %s, expect %s %s",
app.Activity, activityType, expectActivity))
}
// TODO: add more popup texts
var popups = [][]string{
{".*青少年.*", "我知道了"}, // 青少年弹窗
{".*个人信息保护.*", "同意"},
{".*更新.*", "以后再说"},
{".*定位.*", ".*允许.*"},
{".*拍照.*", "仅.*允许"},
{".*录音.*", "仅.*允许"},
{"管理使用时间", ".*忽略.*"},
}
func (dExt *DriverExt) autoPopupHandler(screenResult *ScreenResult) error {
for _, popup := range popups {
if len(popup) != 2 {
continue
}
points, err := screenResult.Texts.FindTexts([]string{popup[0], popup[1]}, WithRegex(true))
if err == nil {
log.Warn().Interface("popup", popup).
Interface("texts", screenResult.Texts).Msg("text popup found")
point := points[1].Center()
log.Info().Str("text", points[1].Text).Msg("close popup")
if err := dExt.TapAbsXY(point.X, point.Y); err != nil {
log.Error().Err(err).Msg("tap popup failed")
return errors.Wrap(code.MobileUIPopupError, err.Error())
}
// tap popup success
return nil
}
}
// no popup found
return nil
}

View File

@@ -0,0 +1,32 @@
//go:build localtest
package uixt
import "testing"
func TestVideoCrawler(t *testing.T) {
setupAndroid(t)
configs := &VideoCrawlerConfigs{
AppPackageName: "com.ss.android.ugc.aweme",
Timeout: 600,
Feed: FeedConfig{
TargetCount: 5,
TargetLabels: []TargetLabel{
{Text: `^广告$`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
{Text: `^图文$`, Scope: Scope{0, 0.5, 1, 1}, Regex: true, Target: 2},
{Text: `^特效\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
{Text: `^模板\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
{Text: `^购物\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
},
SleepRandom: []interface{}{0, 5, 0.7, 5, 10, 0.3},
},
Live: LiveConfig{
TargetCount: 3,
SleepRandom: []interface{}{15, 20},
},
}
err := driverExt.VideoCrawler(configs)
checkErr(t, err)
}

View File

@@ -8,8 +8,11 @@ import (
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
@@ -21,6 +24,7 @@ import (
"golang.org/x/net/http2"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
@@ -38,6 +42,8 @@ func NewRunner(t *testing.T) *HRPRunner {
t = &testing.T{}
}
jar, _ := cookiejar.New(nil)
interruptSignal := make(chan os.Signal, 1)
signal.Notify(interruptSignal, syscall.SIGTERM, syscall.SIGINT)
return &HRPRunner{
t: t,
failfast: true, // default to failfast
@@ -60,22 +66,26 @@ func NewRunner(t *testing.T) *HRPRunner {
wsDialer: &websocket.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
caseTimeoutTimer: time.NewTimer(time.Hour * 2), // default case timeout to 2 hour
interruptSignal: interruptSignal,
}
}
type HRPRunner struct {
t *testing.T
failfast bool
httpStatOn bool
requestsLogOn bool
pluginLogOn bool
venv string
saveTests bool
genHTMLReport bool
httpClient *http.Client
http2Client *http.Client
wsDialer *websocket.Dialer
uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial
t *testing.T
failfast bool
httpStatOn bool
requestsLogOn bool
pluginLogOn bool
venv string
saveTests bool
genHTMLReport bool
httpClient *http.Client
http2Client *http.Client
wsDialer *websocket.Dialer
uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial
caseTimeoutTimer *time.Timer // case timeout timer
interruptSignal chan os.Signal // interrupt signal channel
}
// SetClientTransport configures transport of http client for high concurrency load testing
@@ -152,10 +162,17 @@ func (r *HRPRunner) SetProxyUrl(proxyUrl string) *HRPRunner {
return r
}
// SetTimeout configures global timeout in seconds.
func (r *HRPRunner) SetTimeout(timeout time.Duration) *HRPRunner {
log.Info().Float64("timeout(seconds)", timeout.Seconds()).Msg("[init] SetTimeout")
r.httpClient.Timeout = timeout
// SetRequestTimeout configures global request timeout in seconds.
func (r *HRPRunner) SetRequestTimeout(seconds float32) *HRPRunner {
log.Info().Float32("timeout_seconds", seconds).Msg("[init] SetRequestTimeout")
r.httpClient.Timeout = time.Duration(seconds*1000) * time.Millisecond
return r
}
// SetCaseTimeout configures global testcase timeout in seconds.
func (r *HRPRunner) SetCaseTimeout(seconds float32) *HRPRunner {
log.Info().Float32("timeout_seconds", seconds).Msg("[init] SetCaseTimeout")
r.caseTimeoutTimer = time.NewTimer(time.Duration(seconds*1000) * time.Millisecond)
return r
}
@@ -291,10 +308,13 @@ func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) {
return nil, errors.Wrap(err, "parse testcase config failed")
}
// set request timeout in seconds
if testcase.Config.RequestTimeout != 0 {
r.SetRequestTimeout(testcase.Config.RequestTimeout)
}
// set testcase timeout in seconds
if testcase.Config.Timeout != 0 {
timeout := time.Duration(testcase.Config.Timeout*1000) * time.Millisecond
r.SetTimeout(timeout)
if testcase.Config.CaseTimeout != 0 {
r.SetCaseTimeout(testcase.Config.CaseTimeout)
}
// load plugin info to testcase config
@@ -432,7 +452,7 @@ func (r *CaseRunner) parseConfig() error {
}
client, err := device.NewDriver(nil)
if err != nil {
return errors.Wrap(err, "init Android UIAutomator client failed")
return errors.Wrap(err, "init Android client failed")
}
r.hrpRunner.uiClients[device.SerialNumber] = client
}
@@ -505,66 +525,80 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
// run step in sequential order
for _, step := range r.caseRunner.testCase.TestSteps {
// TODO: parse step struct
// parse step name
parsedName, err := r.caseRunner.parser.ParseString(step.Name(), r.sessionVariables)
if err != nil {
parsedName = step.Name()
}
stepName := convertString(parsedName)
log.Info().Str("step", stepName).
Str("type", string(step.Type())).Msg("run step start")
select {
case <-r.caseRunner.hrpRunner.caseTimeoutTimer.C:
log.Warn().Msg("timeout in session runner")
return errors.Wrap(code.TimeoutError, "session runner timeout")
case <-r.caseRunner.hrpRunner.interruptSignal:
log.Warn().Msg("interrupted in session runner")
return errors.Wrap(code.InterruptError, "session runner interrupted")
default:
// TODO: parse step struct
// parse step name
parsedName, err := r.caseRunner.parser.ParseString(step.Name(), r.sessionVariables)
if err != nil {
parsedName = step.Name()
}
stepName := convertString(parsedName)
log.Info().Str("step", stepName).
Str("type", string(step.Type())).Msg("run step start")
// run times of step
loopTimes := step.Struct().Loops
if loopTimes < 0 {
log.Warn().Int("loops", loopTimes).Msg("loop times should be positive, set to 1")
loopTimes = 1
} else if loopTimes == 0 {
loopTimes = 1
} else if loopTimes > 1 {
log.Info().Int("loops", loopTimes).Msg("run step with specified loop times")
}
// run step with specified loop times
var stepResult *StepResult
for i := 1; i <= loopTimes; i++ {
var loopIndex string
if loopTimes > 1 {
log.Info().Int("index", i).Msg("start running step in loop")
loopIndex = fmt.Sprintf("_loop_%d", i)
// run times of step
loopTimes := step.Struct().Loops
if loopTimes < 0 {
log.Warn().Int("loops", loopTimes).Msg("loop times should be positive, set to 1")
loopTimes = 1
} else if loopTimes == 0 {
loopTimes = 1
} else if loopTimes > 1 {
log.Info().Int("loops", loopTimes).Msg("run step with specified loop times")
}
// run step
stepResult, err = step.Run(r)
stepResult.Name = stepName + loopIndex
// run step with specified loop times
var stepResult *StepResult
for i := 1; i <= loopTimes; i++ {
var loopIndex string
if loopTimes > 1 {
log.Info().Int("index", i).Msg("start running step in loop")
loopIndex = fmt.Sprintf("_loop_%d", i)
}
r.updateSummary(stepResult)
}
// run step
stepResult, err = step.Run(r)
stepResult.Name = stepName + loopIndex
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
}
r.updateSummary(stepResult)
}
if err == nil {
log.Info().Str("step", stepResult.Name).
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
}
if err == nil {
log.Info().Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", true).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
continue
}
// failed
log.Error().Err(err).Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", true).
Interface("exportVars", stepResult.ExportVars).
Bool("success", false).
Msg("run step end")
continue
}
// failed
log.Error().Err(err).Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", false).
Msg("run step end")
// interrupted or timeout, abort running
if errors.Is(err, code.InterruptError) || errors.Is(err, code.TimeoutError) {
return err
}
// check if failfast
if r.caseRunner.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting")
// check if failfast
if r.caseRunner.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting")
}
}
}

View File

@@ -12,7 +12,7 @@ import (
)
type MobileStep struct {
Serial string `json:"serial,omitempty" yaml:"serial,omitempty"`
Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` // android serial or ios udid
uixt.MobileAction `yaml:",inline"`
Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"`
}
@@ -36,7 +36,7 @@ func (s *StepMobile) Serial(serial string) *StepMobile {
func (s *StepMobile) InstallApp(path string) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.AppInstall,
Method: uixt.ACTION_AppInstall,
Params: path,
})
return s
@@ -44,7 +44,7 @@ func (s *StepMobile) InstallApp(path string) *StepMobile {
func (s *StepMobile) AppLaunch(bundleId string) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.AppLaunch,
Method: uixt.ACTION_AppLaunch,
Params: bundleId,
})
return s
@@ -52,7 +52,7 @@ func (s *StepMobile) AppLaunch(bundleId string) *StepMobile {
func (s *StepMobile) AppTerminate(bundleId string) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.AppTerminate,
Method: uixt.ACTION_AppTerminate,
Params: bundleId,
})
return s
@@ -69,12 +69,11 @@ func (s *StepMobile) Home() *StepMobile {
// TapXY taps the point {X,Y}, X & Y is percentage of coordinates
func (s *StepMobile) TapXY(x, y float64, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_TapXY,
Params: []float64{x, y},
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_TapXY,
Params: []float64{x, y},
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
@@ -82,12 +81,11 @@ func (s *StepMobile) TapXY(x, y float64, options ...uixt.ActionOption) *StepMobi
// TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates
func (s *StepMobile) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_TapAbsXY,
Params: []float64{x, y},
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_TapAbsXY,
Params: []float64{x, y},
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
@@ -95,12 +93,11 @@ func (s *StepMobile) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepM
// Tap taps on the target element
func (s *StepMobile) Tap(params string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Tap,
Params: params,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Tap,
Params: params,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
@@ -108,12 +105,11 @@ func (s *StepMobile) Tap(params string, options ...uixt.ActionOption) *StepMobil
// Tap taps on the target element by OCR recognition
func (s *StepMobile) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_TapByOCR,
Params: ocrText,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_TapByOCR,
Params: ocrText,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
@@ -121,153 +117,142 @@ func (s *StepMobile) TapByOCR(ocrText string, options ...uixt.ActionOption) *Ste
// Tap taps on the target element by CV recognition
func (s *StepMobile) TapByCV(imagePath string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_TapByCV,
Params: imagePath,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_TapByCV,
Params: imagePath,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
// DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates
func (s *StepMobile) DoubleTapXY(x, y float64) *StepMobile {
func (s *StepMobile) DoubleTapXY(x, y float64, options ...uixt.ActionOption) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.ACTION_DoubleTapXY,
Params: []float64{x, y},
Method: uixt.ACTION_DoubleTapXY,
Params: []float64{x, y},
Options: uixt.NewActionOptions(options...),
})
return &StepMobile{step: s.step}
}
func (s *StepMobile) DoubleTap(params string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_DoubleTap,
Params: params,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_DoubleTap,
Params: params,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) Back(options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Back,
Params: nil,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Back,
Params: nil,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: []float64{sx, sy, ex, ey},
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Swipe,
Params: []float64{sx, sy, ex, ey},
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeUp(options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "up",
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Swipe,
Params: "up",
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeDown(options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "down",
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Swipe,
Params: "down",
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeLeft(options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "left",
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Swipe,
Params: "left",
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeRight(options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "right",
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Swipe,
Params: "right",
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_SwipeToTapApp,
Params: appName,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_SwipeToTapApp,
Params: appName,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeToTapText(text string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_SwipeToTapText,
Params: text,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_SwipeToTapText,
Params: text,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) SwipeToTapTexts(texts interface{}, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_SwipeToTapTexts,
Params: texts,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_SwipeToTapTexts,
Params: texts,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
func (s *StepMobile) Input(text string, options ...uixt.ActionOption) *StepMobile {
action := uixt.MobileAction{
Method: uixt.ACTION_Input,
Params: text,
}
for _, option := range options {
option(&action)
Method: uixt.ACTION_Input,
Params: text,
Options: uixt.NewActionOptions(options...),
}
s.mobileStep().Actions = append(s.mobileStep().Actions, action)
return &StepMobile{step: s.step}
}
@@ -275,8 +260,9 @@ func (s *StepMobile) Input(text string, options ...uixt.ActionOption) *StepMobil
// Sleep specify sleep seconds after last action
func (s *StepMobile) Sleep(n float64) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlSleep,
Params: n,
Method: uixt.ACTION_Sleep,
Params: n,
Options: nil,
})
return &StepMobile{step: s.step}
}
@@ -287,32 +273,45 @@ func (s *StepMobile) Sleep(n float64) *StepMobile {
// 2. [min1, max1, weight1, min2, max2, weight2, ...] : weight is the probability of the time range
func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlSleepRandom,
Params: params,
Method: uixt.ACTION_SleepRandom,
Params: params,
Options: nil,
})
return &StepMobile{step: s.step}
}
func (s *StepMobile) VideoCrawler(params map[string]interface{}) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.ACTION_VideoCrawler,
Params: params,
Options: nil,
})
return &StepMobile{step: s.step}
}
func (s *StepMobile) ScreenShot() *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlScreenShot,
Params: nil,
Method: uixt.ACTION_ScreenShot,
Params: nil,
Options: nil,
})
return &StepMobile{step: s.step}
}
func (s *StepMobile) StartCamera() *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlStartCamera,
Params: nil,
Method: uixt.ACTION_StartCamera,
Params: nil,
Options: nil,
})
return &StepMobile{step: s.step}
}
func (s *StepMobile) StopCamera() *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlStopCamera,
Params: nil,
Method: uixt.ACTION_StopCamera,
Params: nil,
Options: nil,
})
return &StepMobile{step: s.step}
}
@@ -590,25 +589,20 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
attachments := make(map[string]interface{})
if err != nil {
attachments["error"] = err.Error()
// check if app is in foreground
packageName := uiDriver.Driver.GetLastLaunchedApp()
yes, err2 := uiDriver.Driver.IsAppInForeground(packageName)
if packageName != "" && (!yes || err2 != nil) {
log.Error().Err(err2).Str("packageName", packageName).Msg("app is not in foreground")
err = errors.Wrap(code.MobileUIAppNotInForegroundError, err.Error())
}
}
// take screenshot after each step
_, err := uiDriver.TakeScreenShot(
builtin.GenNameWithTimestamp("step_%d_") + step.Name)
_, _, err := uiDriver.TakeScreenShot(
builtin.GenNameWithTimestamp("%d_step_") + step.Name)
if err != nil {
log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed on step finished")
}
// save attachments
attachments["screenshots"] = uiDriver.GetScreenShots()
cacheData := uiDriver.GetStepCacheData()
for key, value := range cacheData {
attachments[key] = value
}
stepResult.Attachments = attachments
}()
@@ -627,18 +621,27 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
// run actions
for _, action := range actions {
if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil {
if !code.IsErrorPredefined(err) {
err = errors.Wrap(code.ParseError,
fmt.Sprintf("parse action params failed: %v", err))
select {
case <-s.caseRunner.hrpRunner.caseTimeoutTimer.C:
log.Warn().Msg("timeout in mobile UI runner")
return stepResult, errors.Wrap(code.TimeoutError, "mobile UI runner timeout")
case <-s.caseRunner.hrpRunner.interruptSignal:
log.Warn().Msg("interrupted in mobile UI runner")
return stepResult, errors.Wrap(code.InterruptError, "mobile UI runner interrupted")
default:
if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil {
if !code.IsErrorPredefined(err) {
err = errors.Wrap(code.ParseError,
fmt.Sprintf("parse action params failed: %v", err))
}
return stepResult, err
}
return stepResult, err
}
if err := uiDriver.DoAction(action); err != nil {
if !code.IsErrorPredefined(err) {
err = errors.Wrap(code.MobileUIDriverError, err.Error())
if err := uiDriver.DoAction(action); err != nil {
if !code.IsErrorPredefined(err) {
err = errors.Wrap(code.MobileUIDriverError, err.Error())
}
return stepResult, err
}
return stepResult, err
}
}

View File

@@ -164,7 +164,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
// global timeout
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetTimeout(10 * time.Second). // set global timeout to 10s
SetRequestTimeout(10). // set global timeout to 10s
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step1").
@@ -180,7 +180,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase2 := &TestCase{
Config: NewConfig("TestCase2").
SetTimeout(10 * time.Second). // set global timeout to 10s
SetRequestTimeout(10). // set global timeout to 10s
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step1").
@@ -197,7 +197,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
// step timeout
testcase3 := &TestCase{
Config: NewConfig("TestCase3").
SetTimeout(10 * time.Second).
SetRequestTimeout(10).
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step2").