mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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").
|
||||
|
||||
120
examples/uitest/demo_android_video_crawler.json
Normal file
120
examples/uitest/demo_android_video_crawler.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "抓取抖音视频信息",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "滑动消费 feed 至少 10 个,live 至少 3 个;滑动过程中,70% 随机间隔 0-5s,30% 随机间隔 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
104
examples/uitest/demo_android_video_crawler_test.go
Normal file
104
examples/uitest/demo_android_video_crawler_test.go
Normal 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-5s,30% 随机间隔 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-5s,30% 随机间隔 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)
|
||||
}
|
||||
}
|
||||
98
examples/uitest/demo_android_video_ks_crawler.json
Normal file
98
examples/uitest/demo_android_video_ks_crawler.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "抓取 KS 视频信息",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "滑动消费 feed 至少 100 个;滑动过程中,70% 随机间隔 0-5s,30% 随机间隔 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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分钟").
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
38
go.mod
@@ -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
349
go.sum
@@ -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=
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
6
hrp/internal/env/env.go
vendored
6
hrp/internal/env/env.go
vendored
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1 +1 @@
|
||||
v4.3.3
|
||||
v4.3.4
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
591
hrp/pkg/uixt/action.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
500
hrp/pkg/uixt/video_crawler.go
Normal file
500
hrp/pkg/uixt/video_crawler.go
Normal 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
|
||||
}
|
||||
32
hrp/pkg/uixt/video_crawler_test.go
Normal file
32
hrp/pkg/uixt/video_crawler_test.go
Normal 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)
|
||||
}
|
||||
174
hrp/runner.go
174
hrp/runner.go
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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").
|
||||
|
||||
Reference in New Issue
Block a user