{{ .Attachment }}
+ {{ .Attachments }}
diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index e513435a..2d0ab07d 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -44,7 +44,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index f914858a..efca234e 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 77e221fc..e8b5d983 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -63,8 +63,6 @@ jobs: fail-fast: false matrix: go-version: - - 1.16.x - - 1.17.x - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 0de9fa23..6d2852b5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ logs reports *.xml htmlcov/ +screenshots/ # built plugins debugtalk.bin diff --git a/Makefile b/Makefile index b094ea9c..f2f15434 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ bump: ## bump hrp version, e.g. make bump version=4.0.0 .PHONY: build build: ## build hrp cli tool @echo "[info] build hrp cli tool" - @. scripts/build.sh + @. scripts/build.sh $(tags) .PHONY: install-hooks install-hooks: ## install git hooks diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 802fbe46..f3aaf35c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,13 @@ # Release History +## v4.3.0 (2022-10-21) + +- feat: support iOS UI automation with [WebDriverAgent] +- feat support Android UI automation with [uiautomator2] +- feat: integrage ios device management with [gidevice] +- feat: add specified exit code for different exceptions +- refactor: make boomer/uixt/httpstat as sub package + ## v4.2.1 (2022-09-01) **go version** @@ -669,4 +677,7 @@ reference: [v2-changelog] [locust]: https://locust.io/ [black]: https://github.com/psf/black [loguru]: https://github.com/Delgan/loguru -[v2-changelog]: https://github.com/httprunner/httprunner/blob/v2/docs/CHANGELOG.md \ No newline at end of file +[v2-changelog]: https://github.com/httprunner/httprunner/blob/v2/docs/CHANGELOG.md +[WebDriverAgent]: https://github.com/appium/WebDriverAgent +[uiautomator2]: https://github.com/appium/appium-uiautomator2-server +[gidevice]: https://github.com/electricbubble/gidevice diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index fae4a004..e60bfc4c 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -41,4 +41,4 @@ Copyright 2017 debugtalk * [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-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 20adfde0..c5c92782 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -44,6 +44,7 @@ hrp boom [flags] --profile string profile for load testing --prometheus-gateway string Prometheus Pushgateway url. --request-increase-rate string Request increase rate, disabled by default. (default "-1") + --run-time int Stop after the specified amount of time(s), Only used --autostart. Defaults to run forever. --spawn-count int The number of users to spawn for load testing (default 1) --spawn-rate float The rate for spawning users (default 1) --worker worker of distributed testing @@ -54,4 +55,4 @@ hrp boom [flags] * [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-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_boom_curl.md b/docs/cmd/hrp_boom_curl.md index 87a0a66f..f8c7dcc5 100644 --- a/docs/cmd/hrp_boom_curl.md +++ b/docs/cmd/hrp_boom_curl.md @@ -16,4 +16,4 @@ hrp boom curl URLs [flags] * [hrp boom](hrp_boom.md) - run load test with boomer -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md index 0be74f06..69f024ff 100644 --- a/docs/cmd/hrp_build.md +++ b/docs/cmd/hrp_build.md @@ -28,4 +28,4 @@ hrp build $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 24b7ecd9..5f47069a 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -23,4 +23,4 @@ hrp convert $path... [flags] * [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-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_convert_curl.md b/docs/cmd/hrp_convert_curl.md index 9880780a..c6c2dde3 100644 --- a/docs/cmd/hrp_convert_curl.md +++ b/docs/cmd/hrp_convert_curl.md @@ -16,4 +16,4 @@ hrp convert curl URLs [flags] * [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_curl.md b/docs/cmd/hrp_curl.md index 82fe7511..5ef6e71d 100644 --- a/docs/cmd/hrp_curl.md +++ b/docs/cmd/hrp_curl.md @@ -16,4 +16,4 @@ hrp curl $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_dns.md b/docs/cmd/hrp_dns.md index 57f051d3..d2aaef60 100644 --- a/docs/cmd/hrp_dns.md +++ b/docs/cmd/hrp_dns.md @@ -26,4 +26,4 @@ hrp dns $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_ping.md b/docs/cmd/hrp_ping.md index 164652ad..0475d93b 100644 --- a/docs/cmd/hrp_ping.md +++ b/docs/cmd/hrp_ping.md @@ -20,4 +20,4 @@ hrp ping $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index b5eced08..3512a8ea 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -16,4 +16,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 7079fc39..a0f70751 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -36,4 +36,4 @@ hrp run $path... [flags] * [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-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_run_curl.md b/docs/cmd/hrp_run_curl.md index cdc3f54c..672a26c2 100644 --- a/docs/cmd/hrp_run_curl.md +++ b/docs/cmd/hrp_run_curl.md @@ -16,4 +16,4 @@ hrp run curl URLs [flags] * [hrp run](hrp_run.md) - run API test with go engine -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index d64f16dc..888c7457 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -21,4 +21,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_traceroute.md b/docs/cmd/hrp_traceroute.md index f959d880..a31d1dd1 100644 --- a/docs/cmd/hrp_traceroute.md +++ b/docs/cmd/hrp_traceroute.md @@ -19,4 +19,4 @@ hrp traceroute $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md index 56669a98..97986ea5 100644 --- a/docs/cmd/hrp_wiki.md +++ b/docs/cmd/hrp_wiki.md @@ -16,4 +16,4 @@ hrp wiki [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/examples/data/postman/__init__.py b/examples/data/postman/__init__.py new file mode 100644 index 00000000..70cfba53 --- /dev/null +++ b/examples/data/postman/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/demo-empty-project/proj.json b/examples/demo-empty-project/proj.json index b2b376f6..edf45464 100644 --- a/examples/demo-empty-project/proj.json +++ b/examples/demo-empty-project/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-empty-project", - "create_time": "2022-07-11T11:45:29.942532+08:00", - "hrp_version": "v4.1.6" + "create_time": "2022-10-21T21:54:56.252853+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go b/examples/demo-with-go-plugin/plugin/debugtalk_gen.go deleted file mode 100644 index 0ee1ae22..00000000 --- a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go +++ /dev/null @@ -1,16 +0,0 @@ -// NOTE: Generated By hrp v4.1.5, DO NOT EDIT! -package main - -import ( - "github.com/httprunner/funplugin/fungo" -) - -func main() { - fungo.Register("SumTwoInt", SumTwoInt) - fungo.Register("SumInts", SumInts) - fungo.Register("Sum", Sum) - fungo.Register("SetupHookExample", SetupHookExample) - fungo.Register("TeardownHookExample", TeardownHookExample) - fungo.Register("GetUserAgent", GetUserAgent) - fungo.Serve() -} diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index ecc3509d..867531eb 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-go-plugin", - "create_time": "2022-07-26T10:30:29.31361+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-10-21T21:52:38.979867+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-py-plugin/.debugtalk_gen.py b/examples/demo-with-py-plugin/.debugtalk_gen.py deleted file mode 100644 index 50f50e5f..00000000 --- a/examples/demo-with-py-plugin/.debugtalk_gen.py +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: Generated By hrp v4.1.6, DO NOT EDIT! - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from debugtalk import * - - -if __name__ == "__main__": - import funppy - funppy.register("get_user_agent", get_user_agent) - funppy.register("sleep", sleep) - funppy.register("sum", sum) - funppy.register("sum_ints", sum_ints) - funppy.register("sum_two_int", sum_two_int) - funppy.register("sum_two_string", sum_two_string) - funppy.register("sum_strings", sum_strings) - funppy.register("concatenate", concatenate) - funppy.register("setup_hook_example", setup_hook_example) - funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index 8140ae8b..c0aeb776 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-py-plugin", - "create_time": "2022-07-26T10:30:30.601095+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-10-21T21:52:39.555851+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json index 50c06186..593129a3 100644 --- a/examples/demo-without-plugin/proj.json +++ b/examples/demo-without-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-without-plugin", - "create_time": "2022-07-11T11:45:29.800018+08:00", - "hrp_version": "v4.1.6" + "create_time": "2022-10-21T21:54:56.136458+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/postman_echo/.debugtalk_gen.py b/examples/postman_echo/.debugtalk_gen.py new file mode 100644 index 00000000..f3e58877 --- /dev/null +++ b/examples/postman_echo/.debugtalk_gen.py @@ -0,0 +1,20 @@ +# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_httprunner_version", get_httprunner_version) + funppy.register("sum_two", sum_two) + funppy.register("get_testcase_config_variables", get_testcase_config_variables) + funppy.register("get_testsuite_config_variables", get_testsuite_config_variables) + funppy.register("get_app_version", get_app_version) + funppy.register("calculate_two_nums", calculate_two_nums) + funppy.register("fake_rand_count", fake_rand_count) + funppy.serve() diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go new file mode 100644 index 00000000..01fa3f94 --- /dev/null +++ b/examples/uitest/demo_android_douyin_test.go @@ -0,0 +1,50 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestAndroidDouYinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). + SetAndroid(hrp.WithAdbLogOn(true), hrp.WithSerialNumber("2d06bf70")), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + Android(). + Home(). + AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)). + Sleep(10), + hrp.NewStep("处理青少年弹窗"). + Android(). + Tap("推荐"). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)). + Validate(). + AssertOCRExists("首页", "抖音启动失败,「首页」不存在"), + hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」"). + Android(). + SwipeToTapText("直播", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), + hrp.NewStep("向上滑动,等待 10s"). + Android(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + if err := testCase.Dump2JSON("demo_android_douyin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_android_douyin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/demo_douyin_follow_live.json b/examples/uitest/demo_douyin_follow_live.json new file mode 100644 index 00000000..171ef268 --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live.json @@ -0,0 +1,152 @@ +{ + "config": { + "name": "通过 关注天窗 进入指定主播抖音直播间", + "variables": { + "app_name": "抖音" + }, + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "$app_name", + "identifier": "启动抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "点击首页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "首页", + "index": -1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击关注页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "关注", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "向上滑动 2 次", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_texts", + "params": [ + "理肤泉", + "婉宝" + ], + "identifier": "click_live", + "direction": [ + 0.6, + 0.2, + 0.2, + 0.2 + ] + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_douyin_follow_live.yaml b/examples/uitest/demo_douyin_follow_live.yaml new file mode 100644 index 00000000..f6c34c3f --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live.yaml @@ -0,0 +1,83 @@ +config: + name: 通过 关注天窗 进入指定主播抖音直播间 + variables: + app_name: 抖音 + ios: + - port: 8700 + mjpeg_port: 8800 + log_on: true +teststeps: + - name: 启动抖音 + ios: + actions: + - method: home + - method: app_terminate + params: com.ss.iphone.ugc.Aweme + - method: swipe_to_tap_app + params: $app_name + identifier: 启动抖音 + max_retry_times: 5 + - method: sleep + params: 5 + validate: + - check: ui_ocr + assert: exists + expect: 推荐 + msg: 抖音启动失败,「推荐」不存在 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true + - name: 点击首页 + ios: + actions: + - method: tap_ocr + params: 首页 + index: -1 + - method: sleep + params: 10 + - name: 点击关注页 + ios: + actions: + - method: tap_ocr + params: 关注 + index: 1 + - method: sleep + params: 10 + - name: 向上滑动 2 次 + ios: + actions: + - method: swipe_to_tap_texts + params: + - 理肤泉 + - 婉宝 + identifier: click_live + direction: + - 0.6 + - 0.2 + - 0.2 + - 0.2 + - method: sleep + params: 10 + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 10 + - method: screenshot + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 10 + - method: screenshot diff --git a/examples/uitest/demo_douyin_follow_live_test.go b/examples/uitest/demo_douyin_follow_live_test.go new file mode 100644 index 00000000..1eb42f93 --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live_test.go @@ -0,0 +1,58 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouyinFollowLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 关注天窗 进入指定主播抖音直播间"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). + SetIOS( + hrp.WithLogOn(true), + hrp.WithWDAPort(8700), + hrp.WithWDAMjpegPort(8800), + ), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("点击首页"). + IOS(). + TapByOCR("首页", hrp.WithIndex(-1)).Sleep(10), + hrp.NewStep("点击关注页"). + IOS(). + TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("向上滑动 2 次"). + IOS().SwipeToTapTexts([]string{"理肤泉", "婉宝"}, hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(10). + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + 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) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json new file mode 100644 index 00000000..7bc7efab --- /dev/null +++ b/examples/uitest/demo_douyin_live.json @@ -0,0 +1,109 @@ +{ + "config": { + "name": "通过 feed 卡片进入抖音直播间", + "variables": { + "app_name": "抖音" + }, + "ios": [ + { + "perf_options": { + "sys_cpu": true, + "sys_mem": true + }, + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "$app_name", + "identifier": "启动抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "向上滑动 2 次", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第一次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": "up", + "identifier": "第二次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "screenshot" + } + ] + } + }, + { + "name": "在推荐页上划,直到出现「点击进入直播间」", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": "点击进入直播间", + "identifier": "进入直播间", + "max_retry_times": 10 + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml new file mode 100644 index 00000000..e20f426c --- /dev/null +++ b/examples/uitest/demo_douyin_live.yaml @@ -0,0 +1,57 @@ +config: + name: 通过 feed 卡片进入抖音直播间 + variables: + app_name: 抖音 + ios: + - perf_options: + sys_cpu: true + sys_mem: true + port: 8700 + mjpeg_port: 8800 + log_on: true +teststeps: + - name: 启动抖音 + ios: + actions: + - method: home + - method: app_terminate + params: com.ss.iphone.ugc.Aweme + - method: swipe_to_tap_app + params: $app_name + identifier: 启动抖音 + max_retry_times: 5 + - method: sleep + params: 5 + validate: + - check: ui_ocr + assert: exists + expect: 推荐 + msg: 抖音启动失败,「推荐」不存在 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true + - name: 向上滑动 2 次 + ios: + actions: + - method: swipe + params: up + identifier: 第一次上划 + - method: sleep + params: 2 + - method: screenshot + - method: swipe + params: up + identifier: 第二次上划 + - method: sleep + params: 2 + - method: screenshot + - name: 在推荐页上划,直到出现「点击进入直播间」 + ios: + actions: + - method: swipe_to_tap_text + params: 点击进入直播间 + identifier: 进入直播间 + max_retry_times: 10 diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go new file mode 100644 index 00000000..4d3215bb --- /dev/null +++ b/examples/uitest/demo_douyin_test.go @@ -0,0 +1,59 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouyinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 卡片进入抖音直播间"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). + SetIOS( + hrp.WithLogOn(true), + hrp.WithWDAPort(8700), + hrp.WithWDAMjpegPort(8800), + hrp.WithPerfOptions( + hrp.WithPerfSystemCPU(true), + hrp.WithPerfSystemMem(true), + ), + ), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("向上滑动 2 次"). + IOS(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(2).ScreenShot(). // 上划 1 次,等待 2s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(2).ScreenShot(), // 再上划 1 次,等待 2s,截图保存 + hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」"). + IOS(). + SwipeToTapText("点击进入直播间", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), + }, + } + + if err := testCase.Dump2JSON("demo_douyin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_douyin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json new file mode 100644 index 00000000..618ec703 --- /dev/null +++ b/examples/uitest/demo_weixin_live.json @@ -0,0 +1,114 @@ +{ + "config": { + "name": "通过 feed 卡片进入微信直播间", + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动微信", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.tencent.xin" + }, + { + "method": "swipe_to_tap_app", + "params": "微信", + "max_retry_times": 5 + } + ] + }, + "validate": [ + { + "check": "ui_label", + "assert": "exists", + "expect": "通讯录", + "msg": "微信启动失败,「通讯录」不存在" + } + ] + }, + { + "name": "进入直播页", + "ios": { + "actions": [ + { + "method": "tap", + "params": "发现" + }, + { + "method": "tap_ocr", + "params": "视频号", + "identifier": "进入视频号", + "index": -1 + } + ] + } + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "在推荐页上划,直到出现「轻触进入直播间」", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": "轻触进入直播间", + "identifier": "进入直播间", + "max_retry_times": 10 + } + ] + } + }, + { + "name": "向上滑动,等待 10s", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第一次上划" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": "up", + "identifier": "第二次上划" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml new file mode 100644 index 00000000..3b064f09 --- /dev/null +++ b/examples/uitest/demo_weixin_live.yaml @@ -0,0 +1,58 @@ +config: + name: 通过 feed 卡片进入微信直播间 + ios: + - port: 8700 + mjpeg_port: 8800 + log_on: true +teststeps: + - name: 启动微信 + ios: + actions: + - method: home + - method: app_terminate + params: com.tencent.xin + - method: swipe_to_tap_app + params: 微信 + max_retry_times: 5 + validate: + - check: ui_label + assert: exists + expect: 通讯录 + msg: 微信启动失败,「通讯录」不存在 + - name: 进入直播页 + ios: + actions: + - method: tap + params: 发现 + - method: tap_ocr + params: 视频号 + identifier: 进入视频号 + index: -1 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true + - name: 在推荐页上划,直到出现「轻触进入直播间」 + ios: + actions: + - method: swipe_to_tap_text + params: 轻触进入直播间 + identifier: 进入直播间 + max_retry_times: 10 + - name: 向上滑动,等待 10s + ios: + actions: + - method: swipe + params: up + identifier: 第一次上划 + - method: sleep + params: 10 + - method: screenshot + - method: swipe + params: up + identifier: 第二次上划 + - method: sleep + params: 10 + - method: screenshot diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go new file mode 100644 index 00000000..835411c5 --- /dev/null +++ b/examples/uitest/demo_weixin_test.go @@ -0,0 +1,52 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSWeixinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). + SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动微信"). + IOS(). + Home(). + AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 + SwipeToTapApp("微信", hrp.WithMaxRetryTimes(5)). + Validate(). + AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), + hrp.NewStep("进入直播页"). + IOS(). + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号", hrp.WithIdentifier("进入视频号"), hrp.WithIndex(-1)), // 通过 OCR 识别「视频号」 + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("在推荐页上划,直到出现「轻触进入直播间」"). + IOS(). + SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), + hrp.NewStep("向上滑动,等待 10s"). + IOS(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + if err := testCase.Dump2JSON("demo_weixin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_weixin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/ios_kuaishou_follow_live_test.go b/examples/uitest/ios_kuaishou_follow_live_test.go new file mode 100644 index 00000000..e16b5be1 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.go @@ -0,0 +1,60 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSKuaiShouLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("直播_快手_关注天窗_ios"). + WithVariables(map[string]interface{}{ + "device": "${ENV(UDID)}", + "ups": "${ENV(LIVEUPLIST)}", + }). + SetIOS(hrp.WithUDID("$device"), hrp.WithLogOn(true), hrp.WithWDAPort(8100), hrp.WithWDAMjpegPort(9100)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动快手"). + IOS(). + AppTerminate("com.jiangjia.gif"). + AppLaunch("com.jiangjia.gif"). + Home(). + SwipeToTapApp("快手", hrp.WithMaxRetryTimes(5)).Sleep(10). + Validate(). + AssertOCRExists("精选", "进入快手失败"), + hrp.NewStep("点击首页"). + IOS(). + TapByOCR("首页", hrp.WithIndex(-1)).Sleep(10), + hrp.NewStep("点击发现页"). + IOS(). + TapByOCR("发现", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("点击关注页"). + IOS(). + TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("点击直播标签,进入直播间"). + IOS(). + SwipeToTapTexts("${split_by_comma($ups)}", hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(60). + Validate(). + AssertOCRExists("说点什么", "进入直播间失败"), + hrp.NewStep("下滑进入下一个直播间"). + IOS(). + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(60), + }, + } + + if err := testCase.Dump2JSON("ios_kuaishou_follow_live_test.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("ios_kuaishou_follow_live_test.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/ios_kuaishou_follow_live_test.json b/examples/uitest/ios_kuaishou_follow_live_test.json new file mode 100644 index 00000000..3817b167 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.json @@ -0,0 +1,153 @@ +{ + "config": { + "name": "直播_快手_关注天窗_ios", + "variables": { + "device": "${ENV(UDID)}", + "ups": "${ENV(LIVEUPLIST)}" + }, + "ios": [ + { + "udid": "$device", + "port": 8100, + "mjpeg_port": 9100, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动快手", + "ios": { + "actions": [ + { + "method": "app_terminate", + "params": "com.jiangjia.gif" + }, + { + "method": "app_launch", + "params": "com.jiangjia.gif" + }, + { + "method": "home" + }, + { + "method": "swipe_to_tap_app", + "params": "快手", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 10 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "精选", + "msg": "进入快手失败" + } + ] + }, + { + "name": "点击首页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "首页", + "index": -1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击发现页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "发现", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击关注页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "关注", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击直播标签,进入直播间", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_texts", + "params": "${split_by_comma($ups)}", + "identifier": "click_live", + "direction": [ + 0.6, + 0.2, + 0.2, + 0.2 + ] + }, + { + "method": "sleep", + "params": 60 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "说点什么", + "msg": "进入直播间失败" + } + ] + }, + { + "name": "下滑进入下一个直播间", + "ios": { + "actions": [ + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 60 + } + ] + } + } + ] +} diff --git a/examples/uitest/ios_kuaishou_follow_live_test.yaml b/examples/uitest/ios_kuaishou_follow_live_test.yaml new file mode 100644 index 00000000..484f0815 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.yaml @@ -0,0 +1,83 @@ +config: + name: 直播_快手_关注天窗_ios + variables: + device: ${ENV(UDID)} + ups: ${ENV(LIVEUPLIST)} + ios: + - udid: $device + port: 8100 + mjpeg_port: 9100 + log_on: true +teststeps: + - name: 启动快手 + ios: + actions: + - method: app_terminate + params: com.jiangjia.gif + - method: app_launch + params: com.jiangjia.gif + - method: home + - method: swipe_to_tap_app + params: 快手 + max_retry_times: 5 + - method: sleep + params: 10 + validate: + - check: ui_ocr + assert: exists + expect: 精选 + msg: 进入快手失败 + - name: 点击首页 + ios: + actions: + - method: tap_ocr + params: 首页 + index: -1 + - method: sleep + params: 10 + - name: 点击发现页 + ios: + actions: + - method: tap_ocr + params: 发现 + index: 1 + - method: sleep + params: 10 + - name: 点击关注页 + ios: + actions: + - method: tap_ocr + params: 关注 + index: 1 + - method: sleep + params: 10 + - name: 点击直播标签,进入直播间 + ios: + actions: + - method: swipe_to_tap_texts + params: ${split_by_comma($ups)} + identifier: click_live + direction: + - 0.6 + - 0.2 + - 0.2 + - 0.2 + - method: sleep + params: 60 + validate: + - check: ui_ocr + assert: exists + expect: 说点什么 + msg: 进入直播间失败 + - name: 下滑进入下一个直播间 + ios: + actions: + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 60 diff --git a/examples/uitest/wda_log_data.json b/examples/uitest/wda_log_data.json new file mode 100644 index 00000000..0237e46a --- /dev/null +++ b/examples/uitest/wda_log_data.json @@ -0,0 +1,144 @@ +{ + "config": { + "name": "验证 WDA 打点数据准确性", + "variables": { + "app_name": "抖音" + }, + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "$app_name", + "identifier": "启动抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "进入购物页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "购物", + "identifier": "点击购物" + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "进入推荐页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "推荐", + "identifier": "点击推荐" + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "向上滑动 2 次", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第 1 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "swipe", + "params": "up", + "identifier": "第 2 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "swipe", + "params": "up", + "identifier": "第 3 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "tap_xy", + "params": [ + 0.9, + 0.1 + ], + "identifier": "点击进入搜索框" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "input", + "params": "httprunner", + "identifier": "输入搜索关键词" + } + ] + } + } + ] +} diff --git a/examples/uitest/wda_log_test.go b/examples/uitest/wda_log_test.go new file mode 100644 index 00000000..54e7a027 --- /dev/null +++ b/examples/uitest/wda_log_test.go @@ -0,0 +1,52 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestWDALog(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("验证 WDA 打点数据准确性"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). + SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("进入购物页"). + IOS().TapByOCR("购物", hrp.WithIdentifier("点击购物")).Sleep(5), + hrp.NewStep("进入推荐页"). + IOS().TapByOCR("推荐", hrp.WithIdentifier("点击推荐")).Sleep(5), + hrp.NewStep("向上滑动 2 次"). + IOS(). + SwipeUp(hrp.WithIdentifier("第 1 次上划")).Sleep(2). + SwipeUp(hrp.WithIdentifier("第 2 次上划")).Sleep(2). + SwipeUp(hrp.WithIdentifier("第 3 次上划")).Sleep(2). + TapXY(0.9, 0.1, hrp.WithIdentifier("点击进入搜索框")).Sleep(2). + Input("httprunner", hrp.WithIdentifier("输入搜索关键词")), + }, + } + + if err := testCase.Dump2JSON("wda_log_data.json"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 32da26b0..29a113b8 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,91 @@ module github.com/httprunner/httprunner/v4 -go 1.16 +go 1.18 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 + github.com/electricbubble/gadb v0.0.7 + github.com/electricbubble/gidevice v0.6.2 + github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 - github.com/go-errors/errors v1.0.1 - github.com/go-openapi/spec v0.20.6 + 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/google/uuid v1.3.0 - github.com/gorilla/websocket v1.4.1 + github.com/gorilla/websocket v1.5.0 github.com/httprunner/funplugin v0.5.0 - github.com/jinzhu/copier v0.3.2 + github.com/jinzhu/copier v0.3.5 github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 github.com/maja42/goval v1.2.1 - github.com/miekg/dns v1.1.25 - github.com/mitchellh/mapstructure v1.4.1 + github.com/miekg/dns v1.1.50 + github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.11.0 - github.com/rs/zerolog v1.26.1 + github.com/prometheus/client_golang v1.13.0 + github.com/rs/zerolog v1.28.0 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/spf13/cobra v1.2.1 - github.com/stretchr/testify v1.7.0 + github.com/spf13/cobra v1.5.0 + github.com/stretchr/testify v1.8.0 + golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1 + golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + cloud.google.com/go/compute v1.7.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // 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 + github.com/go-ole/go-ole v1.2.6 // indirect + 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/hashicorp/go-hclog v1.3.0 // indirect + github.com/hashicorp/go-plugin v1.4.5 // 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 + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect 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/net v0.0.0-20220225172249-27dd8689420f - golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 - google.golang.org/grpc v1.45.0 - google.golang.org/protobuf v1.28.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gocv.io/x/gocv v0.31.0 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect + golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.7 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + howett.net/plist v1.0.0 // indirect ) // replace github.com/httprunner/funplugin => ../funplugin +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 diff --git a/go.sum b/go.sum index cdc0824f..f9df4ccf 100644 --- a/go.sum +++ b/go.sum @@ -17,17 +17,33 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP 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 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= 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/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/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +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= @@ -37,15 +53,11 @@ 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/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 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/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +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= @@ -54,20 +66,15 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 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/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +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/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= @@ -76,63 +83,56 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX 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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/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= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 h1:wP/2aKW6YV0ityxp0Ecv8JDwA/cy6gayVhA/t+roO+w= +github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= +github.com/electricbubble/gadb v0.0.7/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8= +github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= +github.com/electricbubble/opencv-helper v0.0.3/go.mod h1:VHB21p5xsIjXUsUleWSaKGJosRsRAO7cuJoZKf7uCcc= 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/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 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/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 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/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +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= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -140,25 +140,17 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -171,6 +163,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt 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= @@ -189,7 +182,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS 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/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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= @@ -202,13 +195,16 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +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/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= @@ -220,6 +216,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe 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= @@ -227,56 +226,43 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw= github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +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-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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/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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= 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/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/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= -github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= -github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +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.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= -github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= +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= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -285,82 +271,56 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= -github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= -github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= -github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= -github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 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 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= -github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +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.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -368,37 +328,29 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 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/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -406,80 +358,58 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +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/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= +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/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +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/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 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A= +github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo= 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= @@ -488,9 +418,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 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.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -499,24 +426,15 @@ 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= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +gocv.io/x/gocv v0.27.0/go.mod h1:n4LnYjykU6y9gn48yZf4eLCdtuSb77XxSkW6g0wGf/A= +gocv.io/x/gocv v0.31.0 h1:BHDtK8v+YPvoSPQTTiZB2fM/7BLg6511JqkruY2z6LQ= +gocv.io/x/gocv v0.31.0/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-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU= 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= @@ -552,18 +470,15 @@ 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 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -572,8 +487,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -596,10 +509,18 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v 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-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +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.0.0-20220919232410-f2f64ebce3c1 h1:TWZxd/th7FbRSMret2MVQdlI8uT49QEtwZdvJrxjEHU= +golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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= @@ -611,8 +532,17 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ 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-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/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/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= @@ -623,14 +553,13 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ 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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/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-20181026203630-95b1ffbd15a5/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-20181205085412-a5c9d58dba9a/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= @@ -640,14 +569,9 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/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= @@ -680,19 +604,36 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210403161142-5e06dd20ab57/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-20211007075335-d3039528d8ac/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-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +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.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -708,16 +649,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -725,10 +662,8 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -751,7 +686,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 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= @@ -760,15 +694,23 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f 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.0.0-20210106214847-113979e3529a/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 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 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 h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +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= @@ -790,7 +732,24 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q 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.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +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= @@ -839,10 +798,48 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D 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-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE= +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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -863,9 +860,21 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 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.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +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/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= @@ -879,18 +888,17 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba 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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 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= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +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= @@ -900,11 +908,11 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 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= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -912,6 +920,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/hrp/README.md b/hrp/README.md index 38a41391..683a0e1a 100644 --- a/hrp/README.md +++ b/hrp/README.md @@ -51,28 +51,50 @@ type HRPRunner struct { } func (r *HRPRunner) Run(testcases ...ITestCase) error -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) *SessionRunner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) ``` 重点关注两个方法: - Run:测试执行的主入口,支持运行一个或多个测试用例 -- NewSessionRunner:针对给定的测试用例初始化一个 SessionRunner +- NewCaseRunner:针对给定的测试用例初始化一个 CaseRunner -### 用例执行器 SessionRunner +### 用例执行器 CaseRunner -测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 +针对每个测试用例,采用 CaseRunner 存储其公共信息,包括 plugin/parser + +```go +type CaseRunner struct { + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + + parsedConfig *TConfig + parametersIterator *ParametersIterator + rootDir string // project root dir +} + +func (r *CaseRunner) NewSession() *SessionRunner { +``` + +重点关注一个方法: + +- NewSession:测试用例的每一次执行对应一个 SessionRunner + +### SessionRunner + +测试用例的具体执行都由 `SessionRunner` 完成,每个 session 实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 ```go type SessionRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser + caseRunner *CaseRunner sessionVariables map[string]interface{} - transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary + transactions map[string]map[transactionType]time.Time + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary } + +func (r *SessionRunner) Start(givenVars map[string]interface{}) error ``` 重点关注一个方法: @@ -80,12 +102,29 @@ type SessionRunner struct { - Start:启动执行用例,依次执行所有测试步骤 ```go -func (r *SessionRunner) Start() error { +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { ... + r.resetSession() + + r.InitWithParameters(givenVars) + // run step in sequential order for _, step := range r.testCase.TestSteps { - _, err := step.Run(r) - if err != nil && r.hrpRunner.failfast { + // parse step + + // run step + stepResult, err := step.Run(r) + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + + // update extracted variables + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } + + // check if failfast + if err != nil && r.caseRunner.hrpRunner.failfast { return errors.Wrap(err, "abort running due to failfast setting") } } diff --git a/hrp/boomer.go b/hrp/boomer.go index e8ea55fc..4e3ecef5 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -10,12 +10,14 @@ import ( "time" "github.com/httprunner/funplugin" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/json" - "github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/rs/zerolog/log" "golang.org/x/net/context" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *HRPBoomer { @@ -120,7 +122,7 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS testCases, err := LoadTestCases(testcases...) if err != nil { log.Error().Err(err).Msg("failed to load testcases") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } for _, testcase := range testCases { @@ -135,10 +137,10 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TCase { var parsedTestCases []*TCase for _, tc := range testCases { - caseRunner, err := b.hrpRunner.newCaseRunner(tc) + caseRunner, err := b.hrpRunner.NewCaseRunner(tc) if err != nil { log.Error().Err(err).Msg("failed to create runner") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } caseRunner.parsedConfig.Parameters = caseRunner.parametersIterator.outParameters() parsedTestCases = append(parsedTestCases, &TCase{ @@ -154,7 +156,7 @@ func (b *HRPBoomer) TestCasesToBytes(testcases ...ITestCase) []byte { testCases, err := LoadTestCases(testcases...) if err != nil { log.Error().Err(err).Msg("failed to load testcases") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } tcs := b.ParseTestCases(testCases) testCasesBytes, err := json.Marshal(tcs) @@ -252,7 +254,6 @@ func (b *HRPBoomer) rebalanceRunner(profile *boomer.Profile) { log.Info().Interface("profile", profile).Msg("rebalance tasks successfully") } - func (b *HRPBoomer) PollTasks(ctx context.Context) { for { select { @@ -261,7 +262,7 @@ func (b *HRPBoomer) PollTasks(ctx context.Context) { if len(b.Boomer.GetTasksChan()) > 0 { continue } - //Todo: 过滤掉已经传输过的task + // Todo: 过滤掉已经传输过的task if task.TestCasesBytes != nil { // init boomer with profile b.initWorker(task.Profile) @@ -313,12 +314,12 @@ func (b *HRPBoomer) PollTestCases(ctx context.Context) { } func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task { - // init runner for testcase + // init case runner for testcase // this runner is shared by multiple session runners - caseRunner, err := b.hrpRunner.newCaseRunner(testcase) + caseRunner, err := b.hrpRunner.NewCaseRunner(testcase) if err != nil { log.Error().Err(err).Msg("failed to create runner") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } if caseRunner.parser.plugin != nil { b.pluginsMutex.Lock() @@ -352,18 +353,19 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend transactionSuccess := true // flag current transaction result // init session runner - sessionRunner := caseRunner.newSession() + sessionRunner := caseRunner.NewSession() mutex.Lock() if parametersIterator.HasNext() { - sessionRunner.updateSessionVariables(parametersIterator.Next()) + sessionRunner.InitWithParameters(parametersIterator.Next()) } mutex.Unlock() startTime := time.Now() for _, step := range testcase.TestSteps { + // TODO: parse step struct // parse step name - parsedName, err := sessionRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) + parsedName, err := caseRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) if err != nil { parsedName = step.Name() } @@ -382,7 +384,8 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend if result.Success { b.RecordSuccess(string(result.StepType), result.Name, result.Elapsed, result.ContentSize) } else { - b.RecordFailure(string(result.StepType), result.Name, result.Elapsed, result.Attachment) + exception, _ := result.Attachments.(string) + b.RecordFailure(string(result.StepType), result.Name, result.Elapsed, exception) } } } diff --git a/hrp/build.go b/hrp/build.go index 9cecccfb..491bdb8f 100644 --- a/hrp/build.go +++ b/hrp/build.go @@ -6,7 +6,6 @@ import ( "fmt" "html/template" "os" - "os/exec" "path/filepath" "regexp" "strings" @@ -16,6 +15,8 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -128,26 +129,26 @@ func (pt *pluginTemplate) generateGo(output string) error { } // check go sdk in tempDir - if err := builtin.ExecCommandInDir(exec.Command("go", "version"), pluginDir); err != nil { + if err := myexec.RunCommand("go", "version"); err != nil { return errors.Wrap(err, "go sdk not installed") } if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) { // create go mod - if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "main"), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "init", "main"), pluginDir); err != nil { return err } // download plugin dependency // funplugin version should be locked funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version) - if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "get", funplugin), pluginDir); err != nil { return errors.Wrap(err, "go get funplugin failed") } } // add missing and remove unused modules - if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "tidy"), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "tidy"), pluginDir); err != nil { return errors.Wrap(err, "go mod tidy failed") } @@ -161,8 +162,8 @@ func (pt *pluginTemplate) generateGo(output string) error { outputPath, _ := filepath.Abs(output) // build go plugin to debugtalk.bin - cmd := exec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path)) - if err := builtin.ExecCommandInDir(cmd, pluginDir); err != nil { + cmd := myexec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path)) + if err := myexec.ExecCommandInDir(cmd, pluginDir); err != nil { return errors.Wrap(err, "go build plugin failed") } log.Info().Str("output", outputPath).Str("plugin", pt.path).Msg("build go plugin successfully") @@ -175,11 +176,11 @@ func buildGo(path string, output string) error { content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") - return errors.Wrap(err, "read file failed") + return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexGoFunctionName.findAllFunctionNames(string(content)) if err != nil { - return err + return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ @@ -187,26 +188,31 @@ func buildGo(path string, output string) error { Version: version.VERSION, FunctionNames: functionNames, } - return templateContent.generateGo(output) + err = templateContent.generateGo(output) + if err != nil { + return errors.Wrap(code.BuildGoPluginFailed, err.Error()) + } + return nil } // buildPy completes funppy information in debugtalk.py func buildPy(path string, output string) error { log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin") // check the syntax of debugtalk.py - err := builtin.ExecPython3Command("py_compile", path) + err := myexec.ExecPython3Command("py_compile", path) if err != nil { - return errors.Wrap(err, "python plugin syntax invalid") + return errors.Wrap(code.InvalidPluginFile, + fmt.Sprintf("python plugin syntax invalid: %s", err.Error())) } content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") - return errors.Wrap(err, "read file failed") + return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexPyFunctionName.findAllFunctionNames(string(content)) if err != nil { - return err + return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ @@ -214,7 +220,11 @@ func buildPy(path string, output string) error { Version: version.VERSION, FunctionNames: functionNames, } - return templateContent.generatePy(output) + err = templateContent.generatePy(output) + if err != nil { + return errors.Wrap(code.BuildPyPluginFailed, err.Error()) + } + return nil } func BuildPlugin(path string, output string) (err error) { @@ -225,11 +235,12 @@ func BuildPlugin(path string, output string) (err error) { case ".go": err = buildGo(path, output) default: - return errors.New("type error, expected .py or .go") + return errors.Wrap(code.UnsupportedFileExtension, + "type error, expected .py or .go") } if err != nil { log.Error().Err(err).Str("path", path).Msg("build plugin failed") - os.Exit(1) + return err } return nil } diff --git a/hrp/cmd/adb/devices.go b/hrp/cmd/adb/devices.go new file mode 100644 index 00000000..7cd53b69 --- /dev/null +++ b/hrp/cmd/adb/devices.go @@ -0,0 +1,62 @@ +package adb + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func format(data map[string]string) string { + result, _ := json.MarshalIndent(data, "", "\t") + return string(result) +} + +var listAndroidDevicesCmd = &cobra.Command{ + Use: "devices", + Short: "List all Android devices", + RunE: func(cmd *cobra.Command, args []string) error { + devices, err := uixt.DeviceList() + if err != nil { + return errors.Wrap(err, "list android devices failed") + } + + var deviceList []gadb.Device + // filter by serial + for _, d := range devices { + if serial != "" && serial != d.Serial() { + continue + } + deviceList = append(deviceList, d) + } + if serial != "" && len(deviceList) == 0 { + fmt.Printf("no android device found for serial: %s\n", serial) + os.Exit(1) + } + + for _, d := range deviceList { + if isDetail { + fmt.Println(format(d.DeviceInfo())) + } else { + fmt.Println(d.Serial(), d.Usb()) + } + } + return nil + }, +} + +var ( + serial string + isDetail bool +) + +func init() { + listAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial") + listAndroidDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") + androidRootCmd.AddCommand(listAndroidDevicesCmd) +} diff --git a/hrp/cmd/adb/init.go b/hrp/cmd/adb/init.go new file mode 100644 index 00000000..9025ef70 --- /dev/null +++ b/hrp/cmd/adb/init.go @@ -0,0 +1,13 @@ +package adb + +import "github.com/spf13/cobra" + +var androidRootCmd = &cobra.Command{ + Use: "adb", + Short: "simple utils for android device management", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, +} + +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(androidRootCmd) +} diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index afe10253..08cff659 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -1,7 +1,6 @@ package cmd import ( - "os" "strings" "time" @@ -10,8 +9,8 @@ import ( "golang.org/x/net/context" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) // boomCmd represents the boom command @@ -30,7 +29,7 @@ var boomCmd = &cobra.Command{ } setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { var paths []hrp.ITestCase for _, arg := range args { path := hrp.TestCasePath(arg) @@ -42,7 +41,7 @@ var boomCmd = &cobra.Command{ err := builtin.LoadFile(boomArgs.profile, &boomArgs.Profile) if err != nil { log.Error().Err(err).Msg("failed to load profile") - os.Exit(1) + return err } } @@ -89,6 +88,7 @@ var boomCmd = &cobra.Command{ hrpBoomer.InitBoomer() hrpBoomer.Run(paths...) } + return nil }, } @@ -141,13 +141,13 @@ func init() { boomCmd.Flags().IntVar(&boomArgs.expectWorkersMaxWait, "expect-workers-max-wait", 120, "How many workers master should expect to connect before starting the test (only when --autostart is used") } -func makeHRPBoomer() *hrp.HRPBoomer { +func makeHRPBoomer() (*hrp.HRPBoomer, error) { // if set profile, the priority is higher than the other commands if boomArgs.profile != "" { err := builtin.LoadFile(boomArgs.profile, &boomArgs) if err != nil { log.Error().Err(err).Msg("failed to load profile") - os.Exit(1) + return nil, err } } hrpBoomer := hrp.NewStandaloneBoomer(boomArgs.SpawnCount, boomArgs.SpawnRate) @@ -157,5 +157,5 @@ func makeHRPBoomer() *hrp.HRPBoomer { hrpBoomer.SetProfile(&boomArgs.Profile) hrpBoomer.EnableGracefulQuit(context.Background()) hrpBoomer.InitBoomer() - return hrpBoomer + return hrpBoomer, nil } diff --git a/hrp/cmd/cli/main.go b/hrp/cmd/cli/main.go index 63f5c9bd..453891a1 100644 --- a/hrp/cmd/cli/main.go +++ b/hrp/cmd/cli/main.go @@ -1,6 +1,7 @@ package main import ( + "os" "time" "github.com/getsentry/sentry-go" @@ -20,5 +21,5 @@ func main() { } }() - cmd.Execute() + os.Exit(cmd.Execute()) } diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 27163f60..5ec00480 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -7,9 +7,9 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/convert" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/version" + "github.com/httprunner/httprunner/v4/hrp/pkg/convert" ) var convertCmd = &cobra.Command{ @@ -61,9 +61,9 @@ func convertRun(cmd *cobra.Command, args []string) error { outputType = convert.OutputTypePyTest packages := []string{ - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } - _, err := builtin.EnsurePython3Venv(venv, packages...) + _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err).Msg("python3 venv is not ready") return err diff --git a/hrp/cmd/curl.go b/hrp/cmd/curl.go index 214c880d..2fdda0cc 100644 --- a/hrp/cmd/curl.go +++ b/hrp/cmd/curl.go @@ -9,8 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" - "github.com/httprunner/httprunner/v4/hrp/internal/convert" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" + "github.com/httprunner/httprunner/v4/hrp/pkg/convert" ) var runCurlCmd = &cobra.Command{ @@ -21,11 +21,9 @@ var runCurlCmd = &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { runner := makeHRPRunner() - if runner.Run(makeCurlTestCase(args)) != nil { - os.Exit(1) - } + return runner.Run(makeCurlTestCase(args)) }, } @@ -41,9 +39,13 @@ var boomCurlCmd = &cobra.Command{ } setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { - boomer := makeHRPBoomer() + RunE: func(cmd *cobra.Command, args []string) error { + boomer, err := makeHRPBoomer() + if err != nil { + return err + } boomer.Run(makeCurlTestCase(args)) + return nil }, } diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go new file mode 100644 index 00000000..d8a62f41 --- /dev/null +++ b/hrp/cmd/ios/apps.go @@ -0,0 +1,63 @@ +package ios + +import ( + "fmt" + + giDevice "github.com/electricbubble/gidevice" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" +) + +type Application struct { + CFBundleVersion string `json:"version"` + CFBundleDisplayName string `json:"name"` + CFBundleIdentifier string `json:"bundleId"` +} + +var listAppsCmd = &cobra.Command{ + Use: "apps", + Short: "List all iOS installed apps", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + var applicationType giDevice.ApplicationType + switch appType { + case "user": + applicationType = giDevice.ApplicationTypeUser + case "system": + applicationType = giDevice.ApplicationTypeSystem + case "internal": + applicationType = giDevice.ApplicationTypeInternal + case "all": + applicationType = giDevice.ApplicationTypeAny + } + + result, errList := device.InstallationProxyBrowse( + giDevice.WithApplicationType(applicationType), + giDevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier")) + if errList != nil { + return fmt.Errorf("get app list failed") + } + + for _, app := range result { + a := Application{} + mapstructure.Decode(app, &a) + + fmt.Printf("%-30.30s %-50.50s %-s\n", + a.CFBundleDisplayName, a.CFBundleIdentifier, a.CFBundleVersion) + } + return nil + }, +} + +var appType string + +func init() { + listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|internal|all]") + iosRootCmd.AddCommand(listAppsCmd) +} diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go new file mode 100644 index 00000000..0553d82a --- /dev/null +++ b/hrp/cmd/ios/devices.go @@ -0,0 +1,118 @@ +package ios + +import ( + "encoding/json" + "fmt" + "os" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +type Device struct { + d giDevice.Device + UDID string `json:"UDID"` + Status string `json:"status"` + ConnectionType string `json:"connectionType"` + ConnectionSpeed int `json:"connectionSpeed"` + DeviceDetail *DeviceDetail `json:"deviceDetail,omitempty"` +} + +type DeviceDetail struct { + DeviceName string `json:"deviceName,omitempty"` + DeviceClass string `json:"deviceClass,omitempty"` + ProductVersion string `json:"productVersion,omitempty"` + ProductType string `json:"productType,omitempty"` + ProductName string `json:"productName,omitempty"` + PasswordProtected bool `json:"passwordProtected,omitempty"` + ModelNumber string `json:"modelNumber,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + SIMStatus string `json:"simStatus,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` + CPUArchitecture string `json:"cpuArchitecture,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + RegionInfo string `json:"regionInfo,omitempty"` + TimeZone string `json:"timeZone,omitempty"` + UniqueDeviceID string `json:"uniqueDeviceID,omitempty"` + WiFiAddress string `json:"wifiAddress,omitempty"` + BuildVersion string `json:"buildVersion,omitempty"` +} + +func (device *Device) GetDetail() (*DeviceDetail, error) { + value, err := device.d.GetValue("", "") + if err != nil { + return nil, errors.Wrap(err, "get device detail failed") + } + detailByte, _ := json.Marshal(value) + detail := &DeviceDetail{} + json.Unmarshal(detailByte, detail) + return detail, nil +} + +func (device *Device) GetStatus() string { + if device.ConnectionType != "" { + return "online" + } else { + return "offline" + } +} + +func (device *Device) ToFormat() string { + result, _ := json.MarshalIndent(device, "", "\t") + return string(result) +} + +var listDevicesCmd = &cobra.Command{ + Use: "devices", + Short: "List all iOS devices", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(cmd *cobra.Command, args []string) error { + devices, err := uixt.IOSDevices(udid) + if err != nil { + return err + } + if len(devices) == 0 { + if udid != "" { + fmt.Printf("no ios device found for udid: %s\n", udid) + os.Exit(1) + } else { + fmt.Println("no ios device found") + os.Exit(0) + } + } + + for _, d := range devices { + deviceByte, _ := json.Marshal(d.Properties()) + device := &Device{ + d: d, + } + json.Unmarshal(deviceByte, device) + device.Status = device.GetStatus() + + if isDetail { + device.DeviceDetail, err = device.GetDetail() + if err != nil { + return err + } + fmt.Println(device.ToFormat()) + } else { + fmt.Println(device.UDID, device.ConnectionType, device.Status) + } + } + return nil + }, +} + +var ( + udid string + isDetail bool +) + +func init() { + listDevicesCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") + iosRootCmd.AddCommand(listDevicesCmd) +} diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go new file mode 100644 index 00000000..db1f63a9 --- /dev/null +++ b/hrp/cmd/ios/init.go @@ -0,0 +1,35 @@ +package ios + +import ( + "fmt" + "os" + + giDevice "github.com/electricbubble/gidevice" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +var iosRootCmd = &cobra.Command{ + Use: "ios", + Short: "simple utils for ios device management", +} + +func getDevice(udid string) (giDevice.Device, error) { + devices, err := uixt.IOSDevices(udid) + if err != nil { + return nil, err + } + if len(devices) == 0 { + fmt.Println("no ios device found") + os.Exit(1) + } + if len(devices) > 1 { + return nil, fmt.Errorf("multiple devices found, please specify udid") + } + return devices[0], nil +} + +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(iosRootCmd) +} diff --git a/hrp/cmd/ios/mount.go b/hrp/cmd/ios/mount.go new file mode 100644 index 00000000..53972220 --- /dev/null +++ b/hrp/cmd/ios/mount.go @@ -0,0 +1,90 @@ +package ios + +import ( + "encoding/base64" + "fmt" + "path/filepath" + "strings" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +// mountCmd represents the mount command +var mountCmd = &cobra.Command{ + Use: "mount", + Short: "A brief description of your command", + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + value, err := device.GetValue("", "ProductVersion") + if err != nil { + return fmt.Errorf("get device ProductVersion failed: %v", err) + } + log.Info().Str("version", value.(string)).Msg("get device version") + + imageSignatures, errImage := device.Images() + + if listDeveloperDiskImage { + for i, imgSign := range imageSignatures { + fmt.Printf("[%d] %s\n", i+1, base64.StdEncoding.EncodeToString(imgSign)) + } + return nil + } + + if errImage == nil && len(imageSignatures) > 0 { + log.Info().Msg("ios developer image is already mounted") + return nil + } + + log.Info().Str("dir", developerDiskImageDir).Msg("start to mount ios developer image") + + if !builtin.IsFolderPathExists(developerDiskImageDir) { + return fmt.Errorf("developer disk image directory not exist: %s", developerDiskImageDir) + } + + ver := strings.Split(value.(string), ".") + if len(ver) < 2 { + return fmt.Errorf("got invalid device ProductVersion: %v", value) + } + version := ver[0] + "." + ver[1] + + var dmgPath, signaturePath string + if builtin.IsFilePathExists(filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg")) { + dmgPath = filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg") + signaturePath = filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg.signature") + } else if builtin.IsFilePathExists(filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg.")) { + dmgPath = filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg") + signaturePath = filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg.signature") + } else { + log.Error().Str("dir", developerDiskImageDir).Msg("developer disk image not found in directory") + return fmt.Errorf("developer disk image not found") + } + + if err = device.MountDeveloperDiskImage(dmgPath, signaturePath); err != nil { + return fmt.Errorf("mount developer disk image failed: %s", err) + } + + log.Info().Msg("mount developer disk image successfully") + return nil + }, +} + +const defaultDeveloperDiskImageDir = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/" + +var ( + developerDiskImageDir string + listDeveloperDiskImage bool +) + +func init() { + mountCmd.Flags().BoolVar(&listDeveloperDiskImage, "list", false, "list developer disk images") + mountCmd.Flags().StringVarP(&developerDiskImageDir, "dir", "d", defaultDeveloperDiskImageDir, "specify DeveloperDiskImage directory") + mountCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + iosRootCmd.AddCommand(mountCmd) +} diff --git a/hrp/cmd/ios/ps.go b/hrp/cmd/ios/ps.go new file mode 100644 index 00000000..d40dcc4d --- /dev/null +++ b/hrp/cmd/ios/ps.go @@ -0,0 +1,61 @@ +package ios + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var psCmd = &cobra.Command{ + Use: "ps", + Short: "show running processes", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + apps, err := device.AppList() + if err != nil { + return errors.Wrap(err, "get ios apps failed") + } + + maxNameLen := 0 + mapper := make(map[string]interface{}) + for _, app := range apps { + mapper[app.ExecutableName] = app.CFBundleIdentifier + if len(app.ExecutableName) > maxNameLen { + maxNameLen = len(app.ExecutableName) + } + } + + runningProcesses, err := device.AppRunningProcesses() + if err != nil { + return errors.Wrap(err, "get running processes failed") + } + for _, p := range runningProcesses { + if !isAll && !p.IsApplication { + continue + } + bundleID, ok := mapper[p.Name] + if !ok { + bundleID = "" + } + + fmt.Printf("%4d %-"+fmt.Sprintf("%d", maxNameLen)+"s %20s %s\n", + p.Pid, p.Name, time.Since(p.StartDate).String(), bundleID) + } + return nil + }, +} + +var isAll bool + +func init() { + psCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + psCmd.Flags().BoolVarP(&isAll, "all", "a", false, "print all processes including system processes") + iosRootCmd.AddCommand(psCmd) +} diff --git a/hrp/cmd/ios/reboot.go b/hrp/cmd/ios/reboot.go new file mode 100644 index 00000000..d38d9db8 --- /dev/null +++ b/hrp/cmd/ios/reboot.go @@ -0,0 +1,38 @@ +package ios + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var rebootCmd = &cobra.Command{ + Use: "reboot", + Short: "reboot or shutdown ios device", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + if isShutdown { + err = device.Shutdown() + } else { + err = device.Reboot() + } + if err != nil { + return err + } + fmt.Printf("reboot %s success\n", device.Properties().UDID) + return nil + }, +} + +var isShutdown bool + +func init() { + rebootCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + rebootCmd.Flags().BoolVarP(&isShutdown, "shutdown", "s", false, "shutdown ios device") + iosRootCmd.AddCommand(rebootCmd) +} diff --git a/hrp/cmd/ios/xctest.go b/hrp/cmd/ios/xctest.go new file mode 100644 index 00000000..e2b8d343 --- /dev/null +++ b/hrp/cmd/ios/xctest.go @@ -0,0 +1,56 @@ +package ios + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var xctestCmd = &cobra.Command{ + Use: "xctest", + Short: "run xctest", + RunE: func(cmd *cobra.Command, args []string) error { + if bundleID == "" { + return fmt.Errorf("bundleID is required") + } + device, err := getDevice(udid) + if err != nil { + return err + } + + log.Info().Str("bundleID", bundleID).Msg("run xctest") + out, cancel, err := device.XCTest(bundleID) + if err != nil { + return errors.Wrap(err, "run xctest failed") + } + + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGTERM, syscall.SIGINT) + + // print xctest running logs + go func() { + for s := range out { + fmt.Print(s) + } + done <- os.Interrupt + }() + + <-done + cancel() + + return nil + }, +} + +var bundleID string + +func init() { + xctestCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + xctestCmd.Flags().StringVarP(&bundleID, "bundleID", "b", "", "specify ios bundleID") + iosRootCmd.AddCommand(xctestCmd) +} diff --git a/hrp/cmd/pytest.go b/hrp/cmd/pytest.go index 2e1933ad..55538e78 100644 --- a/hrp/cmd/pytest.go +++ b/hrp/cmd/pytest.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/pytest" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -21,9 +21,9 @@ var pytestCmd = &cobra.Command{ DisableFlagParsing: true, // allow to pass any args to pytest RunE: func(cmd *cobra.Command, args []string) error { packages := []string{ - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } - _, err := builtin.EnsurePython3Venv(venv, packages...) + _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err).Msg("python3 venv is not ready") return err diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index 4ecb63f8..2cc888e8 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -9,6 +9,9 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/httprunner/httprunner/v4/hrp/cmd/adb" + "github.com/httprunner/httprunner/v4/hrp/cmd/ios" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -39,11 +42,12 @@ Copyright 2017 debugtalk`, } if !logJSON { log.Logger = zerolog.New(zerolog.ConsoleWriter{NoColor: noColor, Out: os.Stderr}).With().Timestamp().Logger() - log.Info().Msg("Set log to color console other than JSON format.") + log.Info().Msg("Set log to color console") } }, Version: version.VERSION, - TraverseChildren: true, + TraverseChildren: true, // parses flags on all parents before executing child command + SilenceUsage: true, // silence usage when an error occurs } var ( @@ -54,14 +58,16 @@ var ( // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { +func Execute() int { rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "INFO", "set log level") rootCmd.PersistentFlags().BoolVar(&logJSON, "log-json", false, "set log to json format") rootCmd.PersistentFlags().StringVar(&venv, "venv", "", "specify python3 venv path") - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } + ios.Init(rootCmd) + adb.Init(rootCmd) + + err := rootCmd.Execute() + return code.GetErrorCode(err) } func setLogLevel(level string) { diff --git a/hrp/cmd/run.go b/hrp/cmd/run.go index c628f93a..5a7b4f8e 100644 --- a/hrp/cmd/run.go +++ b/hrp/cmd/run.go @@ -1,8 +1,6 @@ package cmd import ( - "os" - "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" @@ -20,17 +18,14 @@ var runCmd = &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { var paths []hrp.ITestCase for _, arg := range args { path := hrp.TestCasePath(arg) paths = append(paths, &path) } runner := makeHRPRunner() - err := runner.Run(paths...) - if err != nil { - os.Exit(1) - } + return runner.Run(paths...) }, } diff --git a/hrp/cmd/scaffold.go b/hrp/cmd/scaffold.go index 86830725..b127b0ab 100644 --- a/hrp/cmd/scaffold.go +++ b/hrp/cmd/scaffold.go @@ -2,7 +2,6 @@ package cmd import ( "errors" - "os" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -37,7 +36,7 @@ var scaffoldCmd = &cobra.Command{ err := scaffold.CreateScaffold(args[0], pluginType, venv, force) if err != nil { log.Error().Err(err).Msg("create scaffold project failed") - os.Exit(1) + return err } log.Info().Str("projectName", args[0]).Msg("create scaffold success") return nil diff --git a/hrp/config.go b/hrp/config.go index 3ee1264a..9c03278f 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -5,6 +5,7 @@ import ( "time" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) // NewConfig returns a new constructed testcase config with specified testcase name. @@ -29,6 +30,8 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` 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 Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -90,12 +93,55 @@ func (c *TConfig) SetWeight(weight int) *TConfig { return c } -func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) { +func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { c.WebSocketSetting = &WebSocketConfig{ ReconnectionTimes: times, ReconnectionInterval: interval, MaxMessageSize: size, } + return c +} + +func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig { + wdaOptions := &uixt.IOSDevice{} + for _, option := range options { + option(wdaOptions) + } + + // each device can have its own settings + if wdaOptions.UDID != "" { + c.IOS = append(c.IOS, wdaOptions) + return c + } + + // device UDID is not specified, settings will be shared + if len(c.IOS) == 0 { + c.IOS = append(c.IOS, wdaOptions) + } else { + c.IOS[0] = wdaOptions + } + return c +} + +func (c *TConfig) SetAndroid(options ...uixt.AndroidDeviceOption) *TConfig { + uiaOptions := &uixt.AndroidDevice{} + for _, option := range options { + option(uiaOptions) + } + + // each device can have its own settings + if uiaOptions.SerialNumber != "" { + c.Android = append(c.Android, uiaOptions) + return c + } + + // device UDID is not specified, settings will be shared + if len(c.Android) == 0 { + c.Android = append(c.Android, uiaOptions) + } else { + c.Android[0] = uiaOptions + } + return c } type ThinkTimeConfig struct { diff --git a/hrp/internal/builtin/function.go b/hrp/internal/builtin/function.go index a5b3c36f..00d01d97 100644 --- a/hrp/internal/builtin/function.go +++ b/hrp/internal/builtin/function.go @@ -28,6 +28,7 @@ var Functions = map[string]interface{}{ "md5": MD5, // call with one argument "parameterize": loadFromCSV, "P": loadFromCSV, + "split_by_comma": splitByComma, // call with one argument "environ": os.Getenv, "ENV": os.Getenv, "load_ws_message": loadMessage, @@ -225,3 +226,7 @@ func multipartContentType(w *TFormDataWriter) string { } return w.Writer.FormDataContentType() } + +func splitByComma(s string) []string { + return strings.Split(s, ",") +} diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 07157cdb..2f109a5b 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -3,6 +3,8 @@ package builtin import ( "bufio" "bytes" + "crypto/hmac" + "crypto/sha256" "encoding/binary" "encoding/csv" builtinJSON "encoding/json" @@ -10,16 +12,17 @@ import ( "math" "math/rand" "os" - "os/exec" "path/filepath" "reflect" "strconv" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -90,117 +93,6 @@ func FormatResponse(raw interface{}) interface{} { return formattedResponse } -var python3Executable string = "python3" // system default python3 - -// EnsurePython3Venv ensures python3 venv with specified packages -// venv should be directory path of target venv -func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) { - // priority: specified > $HOME/.hrp/venv - if venv == "" { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.Wrap(err, "get user home dir failed") - } - venv = filepath.Join(home, ".hrp", "venv") - } - python3, err = ensurePython3Venv(venv, packages...) - if err != nil { - return "", errors.Wrap(err, "prepare python3 venv failed") - } - python3Executable = python3 - log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path") - return python3, nil -} - -func ExecPython3Command(cmdName string, args ...string) error { - args = append([]string{"-m", cmdName}, args...) - return ExecCommand(python3Executable, args...) -} - -func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error { - out, err := exec.Command( - python3, "-c", fmt.Sprintf("import %s; print(%s.__version__)", pkgName, pkgName), - ).Output() - if err != nil { - return fmt.Errorf("python package %s not found", pkgName) - } - - // do not check version if pkgVersion is empty - if pkgVersion == "" { - log.Info().Str("name", pkgName).Msg("python package is ready") - return nil - } - - // check package version equality - version := strings.TrimSpace(string(out)) - if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") { - return fmt.Errorf("python package %s version %s not matched, please upgrade to %s", - pkgName, version, pkgVersion) - } - - log.Info().Str("name", pkgName).Str("version", pkgVersion).Msg("python package is ready") - return nil -} - -func InstallPythonPackage(python3 string, pkg string) (err error) { - var pkgName, pkgVersion string - if strings.Contains(pkg, "==") { - // funppy==0.5.0 - pkgInfo := strings.Split(pkg, "==") - pkgName = pkgInfo[0] - pkgVersion = pkgInfo[1] - } else { - // funppy - pkgName = pkg - } - - // check if package installed and version matched - err = AssertPythonPackage(python3, pkgName, pkgVersion) - if err == nil { - return nil - } - - // check if pip available - err = ExecCommand(python3, "-m", "pip", "--version") - if err != nil { - log.Warn().Msg("pip is not available") - return errors.Wrap(err, "pip is not available") - } - - log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package") - - // install package - pypiIndexURL := os.Getenv("PYPI_INDEX_URL") - if pypiIndexURL == "" { - pypiIndexURL = "https://pypi.org/simple" // default - } - err = ExecCommand(python3, "-m", "pip", "install", "--upgrade", pkg, - "--index-url", pypiIndexURL, - "--quiet", "--disable-pip-version-check") - if err != nil { - return errors.Wrap(err, "pip install package failed") - } - - return AssertPythonPackage(python3, pkgName, pkgVersion) -} - -func ExecCommandInDir(cmd *exec.Cmd, dir string) error { - log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command") - cmd.Dir = dir - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err - } - - return nil -} - func CreateFolder(folderPath string) error { log.Info().Str("path", folderPath).Msg("create folder") err := os.MkdirAll(folderPath, os.ModePerm) @@ -285,19 +177,19 @@ func GetRandomNumber(min, max int) int { } func Interface2Float64(i interface{}) (float64, error) { - switch i.(type) { + switch v := i.(type) { case int: - return float64(i.(int)), nil + return float64(v), nil case int32: - return float64(i.(int32)), nil + return float64(v), nil case int64: - return float64(i.(int64)), nil + return float64(v), nil case float32: - return float64(i.(float32)), nil + return float64(v), nil case float64: - return i.(float64), nil + return v, nil case string: - intVar, err := strconv.Atoi(i.(string)) + intVar, err := strconv.Atoi(v) if err != nil { return 0, err } @@ -344,8 +236,6 @@ func InterfaceType(raw interface{}) string { return reflect.TypeOf(raw).String() } -var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension") - // LoadFile loads file content with file extension and assigns to structObj func LoadFile(path string, structObj interface{}) (err error) { log.Info().Str("path", path).Msg("load file") @@ -361,12 +251,21 @@ func LoadFile(path string, structObj interface{}) (err error) { decoder := json.NewDecoder(bytes.NewReader(file)) decoder.UseNumber() err = decoder.Decode(structObj) + if err != nil { + err = errors.Wrap(code.LoadJSONError, err.Error()) + } case ".yaml", ".yml": err = yaml.Unmarshal(file, structObj) + if err != nil { + err = errors.Wrap(code.LoadYAMLError, err.Error()) + } case ".env": err = parseEnvContent(file, structObj) + if err != nil { + err = errors.Wrap(code.LoadEnvError, err.Error()) + } default: - err = ErrUnsupportedFileExt + err = code.UnsupportedFileExtension } return err } @@ -406,14 +305,14 @@ func loadFromCSV(path string) []map[string]interface{} { file, err := ReadFile(path) if err != nil { log.Error().Err(err).Msg("read csv file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } r := csv.NewReader(strings.NewReader(string(file))) content, err := r.ReadAll() if err != nil { log.Error().Err(err).Msg("parse csv file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } firstLine := content[0] // parameter names var result []map[string]interface{} @@ -432,7 +331,7 @@ func loadMessage(path string) []byte { file, err := ReadFile(path) if err != nil { log.Error().Err(err).Msg("read message file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return file } @@ -442,13 +341,13 @@ func ReadFile(path string) ([]byte, error) { path, err = filepath.Abs(path) if err != nil { log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") - return nil, err + return nil, errors.Wrap(code.LoadFileError, err.Error()) } file, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("read file failed") - return nil, err + return nil, errors.Wrap(code.LoadFileError, err.Error()) } return file, nil } @@ -494,7 +393,7 @@ func GetFileNameWithoutExtension(path string) string { } func Bytes2File(data []byte, filename string) error { - file, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755) + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o755) defer file.Close() if err != nil { log.Error().Err(err).Msg("failed to generate file") @@ -564,3 +463,18 @@ func SplitInteger(m, n int) (ints []int) { } return } + +func sha256HMAC(key []byte, data []byte) []byte { + mac := hmac.New(sha256.New, key) + mac.Write(data) + return []byte(fmt.Sprintf("%x", mac.Sum(nil))) +} + +// ver: auth-v1or auth-v2 +func Sign(ver string, ak string, sk string, body []byte) string { + expiration := 1800 + signKeyInfo := fmt.Sprintf("%s/%s/%d/%d", ver, ak, time.Now().Unix(), expiration) + signKey := sha256HMAC([]byte(sk), []byte(signKeyInfo)) + signResult := sha256HMAC(signKey, body) + return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult)) +} diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go new file mode 100644 index 00000000..48557401 --- /dev/null +++ b/hrp/internal/code/code.go @@ -0,0 +1,156 @@ +package code + +import ( + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// general: [0, 2) +const ( + Success = 0 + GeneralFail = 1 +) + +// environment: [2, 10) +var ( + InvalidPython3Venv = errors.New("prepare python3 venv failed") // 9 +) + +// loader: [10, 20) +var ( + LoadFileError = errors.New("load file error") // 10 + LoadJSONError = errors.New("load json error") // 11 + LoadYAMLError = errors.New("load yaml error") // 12 + LoadEnvError = errors.New("load .env error") // 13 + LoadCSVError = errors.New("load csv error") // 14 + InvalidCaseFormat = errors.New("invalid case format") // 15 + UnsupportedFileExtension = errors.New("unsupported file extension") // 16 + ReferencedFileNotFound = errors.New("referenced file not found") // 17 + InvalidPluginFile = errors.New("invalid plugin file") // 18 +) + +// parser: [20, 30) +var ( + ParseError = errors.New("parse error") // 20 + VariableNotFound = errors.New("variable not found") // 21 + ParseFunctionError = errors.New("parse function failed") // 22 + CallFunctionError = errors.New("call function failed") // 23 + ParseVariablesError = errors.New("parse variables failed") // 24 +) + +// runner: [30, 40) + +var ( + InitPluginFailed = errors.New("init plugin failed") // 31 + BuildGoPluginFailed = errors.New("build go plugin failed") // 32 + BuildPyPluginFailed = errors.New("build py plugin failed") // 33 +) + +// summary: [40, 50) + +// ios device related: [50, 60) +var ( + IOSDeviceConnectionError = errors.New("ios device connection error") // 50 + IOSDeviceHTTPDriverError = errors.New("ios device HTTP driver error") // 51 + IOSDeviceUSBDriverError = errors.New("ios device USB driver error") // 52 + IOSScreenShotError = errors.New("ios screenshot error") // 55 + IOSCaptureLogError = errors.New("ios capture log error") // 56 +) + +// android device related: [60, 70) +var ( + AndroidDeviceConnectionError = errors.New("android device connection error") // 60 + AndroidDeviceDriverError = errors.New("android device driver error") // 61 + AndroidScreenShotError = errors.New("android screenshot error") // 65 + AndroidCaptureLogError = errors.New("android capture log error") // 66 +) + +// UI automation related: [70, 80) +var ( + MobileUIDriverError = errors.New("mobile UI driver error") // 70 + MobileUIValidationError = errors.New("mobile UI validation error") // 75 +) + +// OCR related: [80, 90) +var ( + OCREnvMissedError = errors.New("OCR env missed error") // 80 + OCRRequestError = errors.New("OCR prepare request error") // 81 + OCRServiceConnectionError = errors.New("OCR service connect error") // 82 + OCRResponseError = errors.New("OCR parse response error") // 83 + OCRTextNotFoundError = errors.New("OCR text not found") // 84 +) + +// CV related: [90, 100) + +var errorsMap = map[error]int{ + // environment + InvalidPython3Venv: 9, + + // loader + LoadFileError: 10, + LoadJSONError: 11, + LoadYAMLError: 12, + LoadEnvError: 13, + LoadCSVError: 14, + InvalidCaseFormat: 15, + UnsupportedFileExtension: 16, + ReferencedFileNotFound: 17, + InvalidPluginFile: 18, + + // parser + ParseError: 20, + VariableNotFound: 21, + ParseFunctionError: 22, + CallFunctionError: 23, + ParseVariablesError: 24, + + // runner + InitPluginFailed: 31, + BuildGoPluginFailed: 32, + BuildPyPluginFailed: 33, + + // ios related + IOSDeviceConnectionError: 50, + IOSDeviceHTTPDriverError: 51, + IOSDeviceUSBDriverError: 52, + IOSScreenShotError: 55, + IOSCaptureLogError: 56, + + // android related + AndroidDeviceConnectionError: 60, + AndroidDeviceDriverError: 61, + AndroidScreenShotError: 65, + AndroidCaptureLogError: 66, + + // UI automation related + MobileUIDriverError: 70, + MobileUIValidationError: 75, + + // OCR related + OCREnvMissedError: 80, + OCRRequestError: 81, + OCRServiceConnectionError: 82, + OCRResponseError: 83, + OCRTextNotFoundError: 84, +} + +func IsErrorPredefined(err error) bool { + _, ok := errorsMap[err] + return ok +} + +func GetErrorCode(err error) (exitCode int) { + if err == nil { + return Success + } + + e := errors.Cause(err) + if code, ok := errorsMap[e]; ok { + exitCode = code + } else { + exitCode = GeneralFail + } + + log.Warn().Int("code", exitCode).Msg("hrp exit") + return +} diff --git a/hrp/internal/code/code_test.go b/hrp/internal/code/code_test.go new file mode 100644 index 00000000..bbf538bf --- /dev/null +++ b/hrp/internal/code/code_test.go @@ -0,0 +1,12 @@ +package code + +import ( + "fmt" + "testing" +) + +func TestGetErrorCode(t *testing.T) { + err := LoadYAMLError + code := GetErrorCode(err) + fmt.Println(code) +} diff --git a/hrp/internal/dial/curl.go b/hrp/internal/dial/curl.go index 8cd3436a..f3940799 100644 --- a/hrp/internal/dial/curl.go +++ b/hrp/internal/dial/curl.go @@ -4,13 +4,13 @@ import ( "bytes" "fmt" "os" - "os/exec" "path/filepath" "time" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) const ( @@ -46,7 +46,7 @@ func DoCurl(args []string) (err error) { } }() - cmd := exec.Command("curl", args...) + cmd := myexec.Command("curl", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -60,11 +60,11 @@ func DoCurl(args []string) (err error) { return } if stdout.String() != "" { - fmt.Printf(stdout.String()) + fmt.Println(stdout.String()) curlResult.Result = stdout.String() curlResult.ResultType = normalResult } else if stderr.String() != "" { - fmt.Printf(stderr.String()) + fmt.Println(stderr.String()) curlResult.ErrorMsg = stderr.String() curlResult.ResultType = errorResult } diff --git a/hrp/internal/dial/dns.go b/hrp/internal/dial/dns.go index 7f1e7e03..20a3f3d4 100644 --- a/hrp/internal/dial/dns.go +++ b/hrp/internal/dial/dns.go @@ -1,7 +1,6 @@ package dial import ( - "encoding/json" "fmt" "io/ioutil" "net" @@ -18,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( diff --git a/hrp/internal/dial/traceroute_unix.go b/hrp/internal/dial/traceroute_unix.go index b6621592..52742666 100644 --- a/hrp/internal/dial/traceroute_unix.go +++ b/hrp/internal/dial/traceroute_unix.go @@ -1,5 +1,4 @@ //go:build darwin || linux -// +build darwin linux package dial @@ -8,7 +7,6 @@ import ( "fmt" "net/url" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -19,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) var ( @@ -52,7 +51,7 @@ func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err erro traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] } - cmd := exec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL), + cmd := myexec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL), "-q", strconv.Itoa(traceRouteOptions.Queries), traceRouteTarget) stdout, _ := cmd.StdoutPipe() diff --git a/hrp/internal/dial/traceroute_windows.go b/hrp/internal/dial/traceroute_windows.go index a1b4b37b..b80d199e 100644 --- a/hrp/internal/dial/traceroute_windows.go +++ b/hrp/internal/dial/traceroute_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package dial @@ -8,7 +7,6 @@ import ( "fmt" "net/url" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -19,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) var ( @@ -50,7 +49,7 @@ func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err erro traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] } - cmd := exec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget) + cmd := myexec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget) stdout, _ := cmd.StdoutPipe() startT := time.Now() diff --git a/hrp/internal/env/env.go b/hrp/internal/env/env.go new file mode 100644 index 00000000..4fbb84e0 --- /dev/null +++ b/hrp/internal/env/env.go @@ -0,0 +1,14 @@ +package env + +import "os" + +var ( + WDA_USB_DRIVER = os.Getenv("WDA_USB_DRIVER") + VEDEM_OCR_URL = os.Getenv("VEDEM_OCR_URL") + VEDEM_OCR_AK = os.Getenv("VEDEM_OCR_AK") + VEDEM_OCR_SK = os.Getenv("VEDEM_OCR_SK") + DISABLE_GA = os.Getenv("DISABLE_GA") + DISABLE_SENTRY = os.Getenv("DISABLE_SENTRY") + PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL") + PATH = os.Getenv("PATH") +) diff --git a/hrp/internal/myexec/cmd.go b/hrp/internal/myexec/cmd.go new file mode 100644 index 00000000..b92e312e --- /dev/null +++ b/hrp/internal/myexec/cmd.go @@ -0,0 +1,163 @@ +package myexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/env" +) + +var python3Executable string = "python3" // system default python3 + +func isPython3(python string) bool { + out, err := Command(python, "--version").Output() + if err != nil { + return false + } + if strings.HasPrefix(string(out), "Python 3") { + return true + } + return false +} + +// EnsurePython3Venv ensures python3 venv with specified packages +// venv should be directory path of target venv +func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) { + // priority: specified > $HOME/.hrp/venv + if venv == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "get user home dir failed") + } + venv = filepath.Join(home, ".hrp", "venv") + } + python3, err = ensurePython3Venv(venv, packages...) + if err != nil { + return "", errors.Wrap(code.InvalidPython3Venv, err.Error()) + } + python3Executable = python3 + log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path") + return python3, nil +} + +func ExecPython3Command(cmdName string, args ...string) error { + args = append([]string{"-m", cmdName}, args...) + return RunCommand(python3Executable, args...) +} + +func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error { + out, err := exec.Command( + python3, "-c", fmt.Sprintf("import %s; print(%s.__version__)", pkgName, pkgName), + ).Output() + if err != nil { + return fmt.Errorf("python package %s not found", pkgName) + } + + // do not check version if pkgVersion is empty + if pkgVersion == "" { + log.Info().Str("name", pkgName).Msg("python package is ready") + return nil + } + + // check package version equality + version := strings.TrimSpace(string(out)) + if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") { + return fmt.Errorf("python package %s version %s not matched, please upgrade to %s", + pkgName, version, pkgVersion) + } + + log.Info().Str("name", pkgName).Str("version", pkgVersion).Msg("python package is ready") + return nil +} + +func InstallPythonPackage(python3 string, pkg string) (err error) { + var pkgName, pkgVersion string + if strings.Contains(pkg, "==") { + // funppy==0.5.0 + pkgInfo := strings.Split(pkg, "==") + pkgName = pkgInfo[0] + pkgVersion = pkgInfo[1] + } else { + // funppy + pkgName = pkg + } + + // check if package installed and version matched + err = AssertPythonPackage(python3, pkgName, pkgVersion) + if err == nil { + return nil + } + + // check if pip available + err = RunCommand(python3, "-m", "pip", "--version") + if err != nil { + log.Warn().Msg("pip is not available") + return errors.Wrap(err, "pip is not available") + } + + log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package") + + // install package + pypiIndexURL := env.PYPI_INDEX_URL + if pypiIndexURL == "" { + pypiIndexURL = "https://pypi.org/simple" // default + } + err = RunCommand(python3, "-m", "pip", "install", "--upgrade", pkg, + "--index-url", pypiIndexURL, + "--quiet", "--disable-pip-version-check") + if err != nil { + return errors.Wrap(err, "pip install package failed") + } + + return AssertPythonPackage(python3, pkgName, pkgVersion) +} + +func RunCommand(cmdName string, args ...string) error { + cmd := Command(cmdName, args...) + log.Info().Str("cmd", cmd.String()).Msg("exec command") + + // add cmd dir path to $PATH + if cmdDir := filepath.Dir(cmdName); cmdDir != "" { + path := fmt.Sprintf("%s:%s", cmdDir, env.PATH) + if err := os.Setenv("PATH", path); err != nil { + log.Error().Err(err).Msg("set env $PATH failed") + return err + } + } + + // print output with colors + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + log.Error().Err(err).Msg("exec command failed") + return err + } + + return nil +} + +func ExecCommandInDir(cmd *exec.Cmd, dir string) error { + log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command") + cmd.Dir = dir + + // print output with colors + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + log.Error().Err(err).Msg("exec command failed") + return err + } + + return nil +} diff --git a/hrp/internal/builtin/utils_unix.go b/hrp/internal/myexec/cmd_uixt.go similarity index 53% rename from hrp/internal/builtin/utils_unix.go rename to hrp/internal/myexec/cmd_uixt.go index 9c0feb2c..39c64085 100644 --- a/hrp/internal/builtin/utils_unix.go +++ b/hrp/internal/myexec/cmd_uixt.go @@ -1,30 +1,18 @@ //go:build darwin || linux -// +build darwin linux -package builtin +package myexec import ( "fmt" "os" "os/exec" "path/filepath" - "strings" + "syscall" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) -func isPython3(python string) bool { - out, err := exec.Command(python, "--version").Output() - if err != nil { - return false - } - if strings.HasPrefix(string(out), "Python 3") { - return true - } - return false -} - func getPython3Executable(venvDir string) string { return filepath.Join(venvDir, "bin", "python3") } @@ -41,20 +29,20 @@ func ensurePython3Venv(venv string, packages ...string) (python3 string, err err if !isPython3(python3) { // python3 venv not available, create one // check if system python3 is available - if err := ExecCommand("python3", "--version"); err != nil { + if err := RunCommand("python3", "--version"); err != nil { return "", errors.Wrap(err, "python3 not found") } // check if .venv exists if _, err := os.Stat(venv); err == nil { // .venv exists, remove first - if err := ExecCommand("rm", "-rf", venv); err != nil { + if err := RunCommand("rm", "-rf", venv); err != nil { return "", errors.Wrap(err, "remove existed venv failed") } } // create python3 .venv - if err := ExecCommand("python3", "-m", "venv", venv); err != nil { + if err := RunCommand("python3", "-m", "venv", venv); err != nil { return "", errors.Wrap(err, "create python3 venv failed") } } @@ -70,28 +58,14 @@ func ensurePython3Venv(venv string, packages ...string) (python3 string, err err return python3, nil } -func ExecCommand(cmdName string, args ...string) error { - cmd := exec.Command(cmdName, args...) - log.Info().Str("cmd", cmd.String()).Msg("exec command") - - // add cmd dir path to $PATH - if cmdDir := filepath.Dir(cmdName); cmdDir != "" { - PATH := fmt.Sprintf("%s:%s", cmdDir, os.Getenv("PATH")) - if err := os.Setenv("PATH", PATH); err != nil { - log.Error().Err(err).Msg("set env $PATH failed") - return err - } +func Command(name string, arg ...string) *exec.Cmd { + cmd := exec.Command(name, arg...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, } - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err - } - - return nil + return cmd +} + +func KillProcessesByGpid(cmd *exec.Cmd) error { + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) } diff --git a/hrp/internal/builtin/utils_windows.go b/hrp/internal/myexec/cmd_windows.go similarity index 73% rename from hrp/internal/builtin/utils_windows.go rename to hrp/internal/myexec/cmd_windows.go index 6c8ae13e..3e202002 100644 --- a/hrp/internal/builtin/utils_windows.go +++ b/hrp/internal/myexec/cmd_windows.go @@ -1,14 +1,15 @@ //go:build windows -// +build windows -package builtin +package myexec import ( "fmt" "os" "os/exec" "path/filepath" + "strconv" "strings" + "syscall" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -21,17 +22,6 @@ func init() { } } -func isPython3(python string) bool { - out, err := exec.Command("cmd", "/c", python, "--version").Output() - if err != nil { - return false - } - if strings.HasPrefix(string(out), "Python 3") { - return true - } - return false -} - func getPython3Executable(venvDir string) string { python := filepath.Join(venvDir, "Scripts", "python3.exe") if isPython3(python) { @@ -64,7 +54,7 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err // check if .venv exists if _, err := os.Stat(venvDir); err == nil { // .venv exists, remove first - if err := ExecCommand("del", "/q", venvDir); err != nil { + if err := RunCommand("del", "/q", venvDir); err != nil { return "", errors.Wrap(err, "remove existed venv failed") } } @@ -72,10 +62,10 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err // create python3 .venv // notice: --symlinks should be specified for windows // https://github.com/actions/virtual-environments/issues/2690 - if err := ExecCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil { + if err := RunCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil { // fix: failed to symlink on Windows log.Warn().Msg("failed to create python3 .venv by using --symlinks, try to use --copies") - if err := ExecCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil { + if err := RunCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil { return "", errors.Wrap(err, "create python3 venv failed") } } @@ -101,22 +91,18 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err return python3, nil } -func ExecCommand(cmdName string, args ...string) error { +func Command(name string, arg ...string) *exec.Cmd { // "cmd /c" carries out the command specified by string and then stops // refer: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd - cmdStr := fmt.Sprintf("%s %s", cmdName, strings.Join(args, " ")) - cmd := exec.Command("cmd", "/c", cmdStr) - log.Info().Str("cmd", cmd.String()).Msg("exec command") - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err + cmd := exec.Command("cmd.exe") + cmd.SysProcAttr = &syscall.SysProcAttr{ + CmdLine: strings.Join(append([]string{"/c", name}, arg...), " "), + HideWindow: true, } - - return nil + return cmd +} + +func KillProcessesByGpid(cmd *exec.Cmd) error { + killCmd := Command("taskkill", "/T", "/F", "/PID ", strconv.Itoa(cmd.Process.Pid)) + return killCmd.Run() } diff --git a/hrp/internal/pytest/main.go b/hrp/internal/pytest/main.go index dde5f0ae..c9bec74b 100644 --- a/hrp/internal/pytest/main.go +++ b/hrp/internal/pytest/main.go @@ -1,7 +1,7 @@ package pytest import ( - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -12,5 +12,5 @@ func RunPytest(args []string) error { }) args = append([]string{"run"}, args...) - return builtin.ExecPython3Command("httprunner", args...) + return myexec.ExecPython3Command("httprunner", args...) } diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index f27a035a..13a81696 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -4,7 +4,6 @@ import ( "embed" "fmt" "os" - "os/exec" "path/filepath" "time" @@ -14,6 +13,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -177,7 +177,7 @@ func CreateScaffold(projectName string, pluginType PluginType, venv string, forc func createGoPlugin(projectName string) error { log.Info().Msg("start to create hashicorp go plugin") // check go sdk - if err := builtin.ExecCommandInDir(exec.Command("go", "version"), projectName); err != nil { + if err := myexec.RunCommand("go", "version"); err != nil { return errors.Wrap(err, "go sdk not installed") } @@ -207,9 +207,9 @@ func createPythonPlugin(projectName, venv string) error { packages := []string{ fmt.Sprintf("funppy==%s", fungo.Version), - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } - _, err = builtin.EnsurePython3Venv(venv, packages...) + _, err = myexec.EnsurePython3Venv(venv, packages...) if err != nil { return err } diff --git a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py index 708b20f9..d5c51015 100644 --- a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py +++ b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py @@ -1,4 +1,4 @@ -# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! +# NOTE: Generated By hrp v4.3.0, DO NOT EDIT! import sys import os @@ -10,6 +10,7 @@ from debugtalk import * if __name__ == "__main__": import funppy + funppy.register("get_user_agent", get_user_agent) funppy.register("sleep", sleep) funppy.register("sum", sum) diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go index fd479e84..9d08c9a0 100644 --- a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go +++ b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go @@ -1,4 +1,4 @@ -// NOTE: Generated By hrp v4.2.0, DO NOT EDIT! +// NOTE: Generated By hrp v4.3.0, DO NOT EDIT! package main import ( diff --git a/hrp/internal/scaffold/templates/report/template.html b/hrp/internal/scaffold/templates/report/template.html index 4bff6c65..0575c021 100644 --- a/hrp/internal/scaffold/templates/report/template.html +++ b/hrp/internal/scaffold/templates/report/template.html @@ -338,14 +338,14 @@ - {{ if .Attachment }} + {{ if .Attachments }} traceback
diff --git a/hrp/internal/sdk/init.go b/hrp/internal/sdk/init.go index 8e69d5e2..ddb12087 100644 --- a/hrp/internal/sdk/init.go +++ b/hrp/internal/sdk/init.go @@ -2,13 +2,13 @@ package sdk import ( "fmt" - "os" "github.com/denisbrodbeck/machineid" "github.com/getsentry/sentry-go" "github.com/google/uuid" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -29,7 +29,7 @@ func init() { gaClient = NewGAClient(trackingID, clientID) // init sentry sdk - if os.Getenv("DISABLE_SENTRY") == "true" { + if env.DISABLE_SENTRY == "true" { return } err = sentry.Init(sentry.ClientOptions{ @@ -50,7 +50,7 @@ func init() { } func SendEvent(e IEvent) error { - if os.Getenv("DISABLE_GA") == "true" { + if env.DISABLE_GA == "true" { // do not send GA events in CI environment return nil } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 15a2b33b..1ddc0f60 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.2.0 \ No newline at end of file +v4.3.0 \ No newline at end of file diff --git a/hrp/internal/version/init.go b/hrp/internal/version/init.go index 4887463b..d1f1513e 100644 --- a/hrp/internal/version/init.go +++ b/hrp/internal/version/init.go @@ -6,3 +6,6 @@ import ( //go:embed VERSION var VERSION string + +// httprunner python version +const HttpRunnerMinimumVersion = "v4.2.0" diff --git a/hrp/internal/wiki/main.go b/hrp/internal/wiki/main.go index 108edca6..2557e499 100644 --- a/hrp/internal/wiki/main.go +++ b/hrp/internal/wiki/main.go @@ -1,10 +1,9 @@ package wiki import ( - "os/exec" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -14,5 +13,5 @@ func OpenWiki() error { Action: "hrp wiki", }) log.Info().Msgf("%s https://httprunner.com", openCmd) - return exec.Command(openCmd, "https://httprunner.com").Run() + return myexec.RunCommand(openCmd, "https://httprunner.com") } diff --git a/hrp/loader.go b/hrp/loader.go new file mode 100644 index 00000000..ff737c6d --- /dev/null +++ b/hrp/loader.go @@ -0,0 +1,68 @@ +package hrp + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { + testCases := make([]*TestCase, 0) + + for _, iTestCase := range iTestCases { + if _, ok := iTestCase.(*TestCase); ok { + testcase, err := iTestCase.ToTestCase() + if err != nil { + log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct") + return nil, err + } + testCases = append(testCases, testcase) + continue + } + + // iTestCase should be a TestCasePath, file path or folder path + tcPath, ok := iTestCase.(*TestCasePath) + if !ok { + return nil, errors.New("invalid iTestCase type") + } + + casePath := tcPath.GetPath() + err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error { + if dir == nil { + // casePath is a file other than a dir + path = casePath + } else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") { + // skip hidden folders + return fs.SkipDir + } else { + // casePath is a dir + path = filepath.Join(casePath, path) + } + + // ignore non-testcase files + ext := filepath.Ext(path) + if ext != ".yml" && ext != ".yaml" && ext != ".json" { + return nil + } + + // filtered testcases + testCasePath := TestCasePath(path) + tc, err := testCasePath.ToTestCase() + if err != nil { + return nil + } + testCases = append(testCases, tc) + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "read dir failed") + } + } + + log.Info().Int("count", len(testCases)).Msg("load testcases successfully") + return testCases, nil +} diff --git a/hrp/parser.go b/hrp/parser.go index 99e4359b..34fb4f73 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -13,9 +13,11 @@ import ( "github.com/httprunner/funplugin" "github.com/httprunner/funplugin/shared" "github.com/maja42/goval" + "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" ) func newParser() *Parser { @@ -123,14 +125,19 @@ func (p *Parser) Parse(raw interface{}, variablesMapping map[string]interface{}) } } -func parseJSONNumber(raw builtinJSON.Number) (interface{}, error) { +func parseJSONNumber(raw builtinJSON.Number) (value interface{}, err error) { if strings.Contains(raw.String(), ".") { // float64 - return raw.Float64() + value, err = raw.Float64() } else { // int64 - return raw.Int64() + value, err = raw.Int64() } + if err != nil { + return nil, errors.Wrap(code.ParseError, + fmt.Sprintf("parse json number failed: %v", err)) + } + return value, nil } const ( @@ -183,18 +190,18 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} argsStr := funcMatched[2] arguments, err := parseFunctionArguments(argsStr) if err != nil { - return raw, err + return raw, errors.Wrap(code.ParseFunctionError, err.Error()) } parsedArgs, err := p.Parse(arguments, variablesMapping) if err != nil { return raw, err } - result, err := p.CallFunc(funcName, parsedArgs.([]interface{})...) + result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) if err != nil { log.Error().Str("funcName", funcName).Interface("arguments", arguments). Err(err).Msg("call function failed") - return raw, err + return raw, errors.Wrap(code.CallFunctionError, err.Error()) } log.Info().Str("funcName", funcName).Interface("arguments", arguments). Interface("output", result).Msg("call function success") @@ -226,7 +233,8 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} } varValue, ok := variablesMapping[varName] if !ok { - return raw, fmt.Errorf("variable %s not found", varName) + return raw, errors.Wrap(code.VariableNotFound, + fmt.Sprintf("variable %s not found", varName)) } if fmt.Sprintf("${%s}", varName) == raw || fmt.Sprintf("$%s", varName) == raw { @@ -251,9 +259,9 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} return parsedString, nil } -// CallFunc calls function with arguments +// callFunc calls function with arguments // only support return at most one result value -func (p *Parser) CallFunc(funcName string, arguments ...interface{}) (interface{}, error) { +func (p *Parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { // call with plugin function if p.plugin != nil { if p.plugin.Has(funcName) { @@ -428,7 +436,8 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in // variables = {"key": ["$key", 2]} if _, ok := extractVarsSet[varName]; ok { log.Error().Interface("variables", variables).Msg("[parseVariables] variable self reference error") - return variables, fmt.Errorf("variable self reference: %v", varName) + return variables, errors.Wrap(code.ParseVariablesError, + fmt.Sprintf("variable self reference: %v", varName)) } // check if reference variable not in variables mapping @@ -443,7 +452,8 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in } if len(undefinedVars) > 0 { log.Error().Interface("undefinedVars", undefinedVars).Msg("[parseVariables] variable not defined error") - return variables, fmt.Errorf("variable not defined: %v", undefinedVars) + return variables, errors.Wrap(code.ParseVariablesError, + fmt.Sprintf("variable not defined: %v", undefinedVars)) } parsedValue, err := p.Parse(varValue, parsedVariables) @@ -456,7 +466,7 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in // check if circular reference exists if traverseRounds > len(variables) { log.Error().Msg("[parseVariables] circular reference error, break infinite loop!") - return variables, fmt.Errorf("circular reference") + return variables, errors.Wrap(code.ParseVariablesError, "circular reference") } } diff --git a/hrp/parser_test.go b/hrp/parser_test.go index 5f27010b..79756c43 100644 --- a/hrp/parser_test.go +++ b/hrp/parser_test.go @@ -478,14 +478,14 @@ func TestCallBuiltinFunction(t *testing.T) { parser := newParser() // call function without arguments - _, err := parser.CallFunc("get_timestamp") + _, err := parser.callFunc("get_timestamp") if !assert.NoError(t, err) { t.Fatal() } // call function with one argument timeStart := time.Now() - _, err = parser.CallFunc("sleep", 1) + _, err = parser.callFunc("sleep", 1) if !assert.NoError(t, err) { t.Fatal() } @@ -494,7 +494,7 @@ func TestCallBuiltinFunction(t *testing.T) { } // call function with one argument - result, err := parser.CallFunc("gen_random_string", 10) + result, err := parser.callFunc("gen_random_string", 10) if !assert.NoError(t, err) { t.Fatal() } @@ -503,7 +503,7 @@ func TestCallBuiltinFunction(t *testing.T) { } // call function with two argument - result, err = parser.CallFunc("max", float64(10), 9.99) + result, err = parser.callFunc("max", float64(10), 9.99) if !assert.NoError(t, err) { t.Fatal() } diff --git a/hrp/internal/boomer/README.md b/hrp/pkg/boomer/README.md similarity index 100% rename from hrp/internal/boomer/README.md rename to hrp/pkg/boomer/README.md diff --git a/hrp/internal/boomer/boomer.go b/hrp/pkg/boomer/boomer.go similarity index 100% rename from hrp/internal/boomer/boomer.go rename to hrp/pkg/boomer/boomer.go index cdf8e708..baac35e3 100644 --- a/hrp/internal/boomer/boomer.go +++ b/hrp/pkg/boomer/boomer.go @@ -1,8 +1,6 @@ package boomer import ( - "github.com/httprunner/httprunner/v4/hrp/internal/json" - "golang.org/x/net/context" "math" "os" "os/signal" @@ -11,6 +9,9 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "golang.org/x/net/context" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // Mode is the running mode of boomer, both standalone and distributed are supported. @@ -156,7 +157,6 @@ func NewWorkerBoomer(masterHost string, masterPort int) *Boomer { // SetAutoStart auto start to load testing func (b *Boomer) SetAutoStart() { b.masterRunner.autoStart = true - } // RunMaster start to run master runner diff --git a/hrp/internal/boomer/boomer_test.go b/hrp/pkg/boomer/boomer_test.go similarity index 100% rename from hrp/internal/boomer/boomer_test.go rename to hrp/pkg/boomer/boomer_test.go diff --git a/hrp/internal/boomer/client_grpc.go b/hrp/pkg/boomer/client_grpc.go similarity index 96% rename from hrp/internal/boomer/client_grpc.go rename to hrp/pkg/boomer/client_grpc.go index 82d4241b..67484003 100644 --- a/hrp/internal/boomer/client_grpc.go +++ b/hrp/pkg/boomer/client_grpc.go @@ -8,6 +8,8 @@ import ( "sync/atomic" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/backoff" @@ -15,10 +17,8 @@ import ( "google.golang.org/grpc/credentials/oauth" "google.golang.org/grpc/metadata" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/data" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type grpcClient struct { @@ -247,7 +247,7 @@ func (c *grpcClient) recv() { msg, err := c.config.getBiStreamClient().Recv() if err != nil { time.Sleep(1 * time.Second) - //log.Error().Err(err).Msg("failed to get message") + // log.Error().Err(err).Msg("failed to get message") continue } if msg == nil { @@ -317,7 +317,7 @@ func (c *grpcClient) sendMessage(msg *genericMessage) { atomic.StoreInt32(&c.failCount, 0) return } - //log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message") + // log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message") if msg.Type == "heartbeat" { atomic.AddInt32(&c.failCount, 1) } diff --git a/hrp/internal/boomer/client_grpc_test.go b/hrp/pkg/boomer/client_grpc_test.go similarity index 100% rename from hrp/internal/boomer/client_grpc_test.go rename to hrp/pkg/boomer/client_grpc_test.go diff --git a/hrp/internal/boomer/data/data.go b/hrp/pkg/boomer/data/data.go similarity index 99% rename from hrp/internal/boomer/data/data.go rename to hrp/pkg/boomer/data/data.go index 9e0a21ea..bdb1f48c 100644 --- a/hrp/internal/boomer/data/data.go +++ b/hrp/pkg/boomer/data/data.go @@ -38,7 +38,6 @@ func init() { } hrpPath = filepath.Join(home, ".hrp") _ = builtin.EnsureFolderExists(filepath.Join(hrpPath, "x509")) - } // Path returns the absolute path the given relative file or directory path diff --git a/hrp/internal/boomer/data/x509/README.md b/hrp/pkg/boomer/data/x509/README.md similarity index 100% rename from hrp/internal/boomer/data/x509/README.md rename to hrp/pkg/boomer/data/x509/README.md diff --git a/hrp/internal/boomer/data/x509/ca_cert.pem b/hrp/pkg/boomer/data/x509/ca_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/ca_cert.pem rename to hrp/pkg/boomer/data/x509/ca_cert.pem diff --git a/hrp/internal/boomer/data/x509/ca_key.pem b/hrp/pkg/boomer/data/x509/ca_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/ca_key.pem rename to hrp/pkg/boomer/data/x509/ca_key.pem diff --git a/hrp/internal/boomer/data/x509/client_ca_cert.pem b/hrp/pkg/boomer/data/x509/client_ca_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_ca_cert.pem rename to hrp/pkg/boomer/data/x509/client_ca_cert.pem diff --git a/hrp/internal/boomer/data/x509/client_ca_key.pem b/hrp/pkg/boomer/data/x509/client_ca_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_ca_key.pem rename to hrp/pkg/boomer/data/x509/client_ca_key.pem diff --git a/hrp/internal/boomer/data/x509/client_cert.pem b/hrp/pkg/boomer/data/x509/client_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_cert.pem rename to hrp/pkg/boomer/data/x509/client_cert.pem diff --git a/hrp/internal/boomer/data/x509/client_key.pem b/hrp/pkg/boomer/data/x509/client_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_key.pem rename to hrp/pkg/boomer/data/x509/client_key.pem diff --git a/hrp/internal/boomer/data/x509/create.sh b/hrp/pkg/boomer/data/x509/create.sh similarity index 100% rename from hrp/internal/boomer/data/x509/create.sh rename to hrp/pkg/boomer/data/x509/create.sh diff --git a/hrp/internal/boomer/data/x509/openssl.cnf b/hrp/pkg/boomer/data/x509/openssl.cnf similarity index 100% rename from hrp/internal/boomer/data/x509/openssl.cnf rename to hrp/pkg/boomer/data/x509/openssl.cnf diff --git a/hrp/internal/boomer/data/x509/server_cert.pem b/hrp/pkg/boomer/data/x509/server_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/server_cert.pem rename to hrp/pkg/boomer/data/x509/server_cert.pem diff --git a/hrp/internal/boomer/data/x509/server_key.pem b/hrp/pkg/boomer/data/x509/server_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/server_key.pem rename to hrp/pkg/boomer/data/x509/server_key.pem diff --git a/hrp/internal/boomer/grpc/messager/messager.pb.go b/hrp/pkg/boomer/grpc/messager/messager.pb.go similarity index 96% rename from hrp/internal/boomer/grpc/messager/messager.pb.go rename to hrp/pkg/boomer/grpc/messager/messager.pb.go index 66a20108..3ad177c5 100644 --- a/hrp/internal/boomer/grpc/messager/messager.pb.go +++ b/hrp/pkg/boomer/grpc/messager/messager.pb.go @@ -7,10 +7,11 @@ package messager import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -458,17 +459,20 @@ func file_grpc_proto_messager_proto_rawDescGZIP() []byte { return file_grpc_proto_messager_proto_rawDescData } -var file_grpc_proto_messager_proto_msgTypes = make([]protoimpl.MessageInfo, 8) -var file_grpc_proto_messager_proto_goTypes = []interface{}{ - (*StreamRequest)(nil), // 0: message.StreamRequest - (*StreamResponse)(nil), // 1: message.StreamResponse - (*RegisterRequest)(nil), // 2: message.RegisterRequest - (*RegisterResponse)(nil), // 3: message.RegisterResponse - (*SignOutRequest)(nil), // 4: message.SignOutRequest - (*SignOutResponse)(nil), // 5: message.SignOutResponse - nil, // 6: message.StreamRequest.DataEntry - nil, // 7: message.StreamResponse.DataEntry -} +var ( + file_grpc_proto_messager_proto_msgTypes = make([]protoimpl.MessageInfo, 8) + file_grpc_proto_messager_proto_goTypes = []interface{}{ + (*StreamRequest)(nil), // 0: message.StreamRequest + (*StreamResponse)(nil), // 1: message.StreamResponse + (*RegisterRequest)(nil), // 2: message.RegisterRequest + (*RegisterResponse)(nil), // 3: message.RegisterResponse + (*SignOutRequest)(nil), // 4: message.SignOutRequest + (*SignOutResponse)(nil), // 5: message.SignOutResponse + nil, // 6: message.StreamRequest.DataEntry + nil, // 7: message.StreamResponse.DataEntry + } +) + var file_grpc_proto_messager_proto_depIdxs = []int32{ 6, // 0: message.StreamRequest.data:type_name -> message.StreamRequest.DataEntry 7, // 1: message.StreamResponse.data:type_name -> message.StreamResponse.DataEntry diff --git a/hrp/internal/boomer/grpc/messager/messager_grpc.pb.go b/hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go similarity index 99% rename from hrp/internal/boomer/grpc/messager/messager_grpc.pb.go rename to hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go index b4bbad60..80e1a6b5 100644 --- a/hrp/internal/boomer/grpc/messager/messager_grpc.pb.go +++ b/hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go @@ -8,6 +8,7 @@ package messager import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -95,15 +96,16 @@ type MessageServer interface { } // UnimplementedMessageServer must be embedded to have forward compatible implementations. -type UnimplementedMessageServer struct { -} +type UnimplementedMessageServer struct{} func (UnimplementedMessageServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") } + func (UnimplementedMessageServer) SignOut(context.Context, *SignOutRequest) (*SignOutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignOut not implemented") } + func (UnimplementedMessageServer) BidirectionalStreamingMessage(Message_BidirectionalStreamingMessageServer) error { return status.Errorf(codes.Unimplemented, "method BidirectionalStreamingMessage not implemented") } diff --git a/hrp/internal/boomer/grpc/proto/messager.proto b/hrp/pkg/boomer/grpc/proto/messager.proto similarity index 100% rename from hrp/internal/boomer/grpc/proto/messager.proto rename to hrp/pkg/boomer/grpc/proto/messager.proto diff --git a/hrp/internal/boomer/message.go b/hrp/pkg/boomer/message.go similarity index 100% rename from hrp/internal/boomer/message.go rename to hrp/pkg/boomer/message.go diff --git a/hrp/internal/boomer/message_test.go b/hrp/pkg/boomer/message_test.go similarity index 100% rename from hrp/internal/boomer/message_test.go rename to hrp/pkg/boomer/message_test.go diff --git a/hrp/internal/boomer/output.go b/hrp/pkg/boomer/output.go similarity index 100% rename from hrp/internal/boomer/output.go rename to hrp/pkg/boomer/output.go diff --git a/hrp/internal/boomer/output_test.go b/hrp/pkg/boomer/output_test.go similarity index 100% rename from hrp/internal/boomer/output_test.go rename to hrp/pkg/boomer/output_test.go diff --git a/hrp/internal/boomer/ratelimiter.go b/hrp/pkg/boomer/ratelimiter.go similarity index 100% rename from hrp/internal/boomer/ratelimiter.go rename to hrp/pkg/boomer/ratelimiter.go diff --git a/hrp/internal/boomer/ratelimiter_test.go b/hrp/pkg/boomer/ratelimiter_test.go similarity index 100% rename from hrp/internal/boomer/ratelimiter_test.go rename to hrp/pkg/boomer/ratelimiter_test.go diff --git a/hrp/internal/boomer/runner.go b/hrp/pkg/boomer/runner.go similarity index 99% rename from hrp/internal/boomer/runner.go rename to hrp/pkg/boomer/runner.go index 506e3dc5..d9e5d5f4 100644 --- a/hrp/internal/boomer/runner.go +++ b/hrp/pkg/boomer/runner.go @@ -10,13 +10,13 @@ import ( "sync/atomic" "time" - "github.com/go-errors/errors" "github.com/jinzhu/copier" "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) const ( diff --git a/hrp/internal/boomer/runner_test.go b/hrp/pkg/boomer/runner_test.go similarity index 99% rename from hrp/internal/boomer/runner_test.go rename to hrp/pkg/boomer/runner_test.go index 62d772d5..76b44cfa 100644 --- a/hrp/internal/boomer/runner_test.go +++ b/hrp/pkg/boomer/runner_test.go @@ -6,9 +6,10 @@ import ( "testing" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/stretchr/testify/assert" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type HitOutput struct { diff --git a/hrp/internal/boomer/server_grpc.go b/hrp/pkg/boomer/server_grpc.go similarity index 99% rename from hrp/internal/boomer/server_grpc.go rename to hrp/pkg/boomer/server_grpc.go index 2a9c9d00..f795cac5 100644 --- a/hrp/internal/boomer/server_grpc.go +++ b/hrp/pkg/boomer/server_grpc.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -17,9 +18,8 @@ import ( "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/data" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type WorkerNode struct { diff --git a/hrp/internal/boomer/server_grpc_test.go b/hrp/pkg/boomer/server_grpc_test.go similarity index 100% rename from hrp/internal/boomer/server_grpc_test.go rename to hrp/pkg/boomer/server_grpc_test.go diff --git a/hrp/internal/boomer/stats.go b/hrp/pkg/boomer/stats.go similarity index 100% rename from hrp/internal/boomer/stats.go rename to hrp/pkg/boomer/stats.go diff --git a/hrp/internal/boomer/stats_test.go b/hrp/pkg/boomer/stats_test.go similarity index 99% rename from hrp/internal/boomer/stats_test.go rename to hrp/pkg/boomer/stats_test.go index 1d4806a2..4a8491ca 100644 --- a/hrp/internal/boomer/stats_test.go +++ b/hrp/pkg/boomer/stats_test.go @@ -110,7 +110,6 @@ func TestLogError(t *testing.T) { if err400.occurrences != 2 { t.Error("Error occurrences is wrong, expected: 2, got:", err400.occurrences) } - } func BenchmarkLogError(b *testing.B) { diff --git a/hrp/internal/boomer/task.go b/hrp/pkg/boomer/task.go similarity index 100% rename from hrp/internal/boomer/task.go rename to hrp/pkg/boomer/task.go diff --git a/hrp/internal/boomer/ulimit.go b/hrp/pkg/boomer/ulimit.go similarity index 97% rename from hrp/internal/boomer/ulimit.go rename to hrp/pkg/boomer/ulimit.go index 40f0c0cc..bc62a218 100644 --- a/hrp/internal/boomer/ulimit.go +++ b/hrp/pkg/boomer/ulimit.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package boomer diff --git a/hrp/internal/boomer/ulimit_windows.go b/hrp/pkg/boomer/ulimit_windows.go similarity index 91% rename from hrp/internal/boomer/ulimit_windows.go rename to hrp/pkg/boomer/ulimit_windows.go index d02840dc..8641b111 100644 --- a/hrp/internal/boomer/ulimit_windows.go +++ b/hrp/pkg/boomer/ulimit_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package boomer diff --git a/hrp/internal/boomer/utils.go b/hrp/pkg/boomer/utils.go similarity index 99% rename from hrp/internal/boomer/utils.go rename to hrp/pkg/boomer/utils.go index 94fad13b..f10a90d0 100644 --- a/hrp/internal/boomer/utils.go +++ b/hrp/pkg/boomer/utils.go @@ -11,7 +11,6 @@ import ( "time" "github.com/google/uuid" - "github.com/rs/zerolog/log" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/mem" diff --git a/hrp/internal/boomer/utils_test.go b/hrp/pkg/boomer/utils_test.go similarity index 99% rename from hrp/internal/boomer/utils_test.go rename to hrp/pkg/boomer/utils_test.go index c56d1457..a8448a97 100644 --- a/hrp/internal/boomer/utils_test.go +++ b/hrp/pkg/boomer/utils_test.go @@ -32,7 +32,6 @@ func TestRound(t *testing.T) { if roundOne != roundTwo { t.Error("round(58360) should be equal to round(58460)") } - } func TestGenMD5(t *testing.T) { diff --git a/hrp/internal/convert/README.md b/hrp/pkg/convert/README.md similarity index 100% rename from hrp/internal/convert/README.md rename to hrp/pkg/convert/README.md diff --git a/hrp/internal/convert/asset/flowgram.png b/hrp/pkg/convert/asset/flowgram.png similarity index 100% rename from hrp/internal/convert/asset/flowgram.png rename to hrp/pkg/convert/asset/flowgram.png diff --git a/hrp/internal/convert/converter.go b/hrp/pkg/convert/converter.go similarity index 98% rename from hrp/internal/convert/converter.go rename to hrp/pkg/convert/converter.go index 4c69296e..cf537869 100644 --- a/hrp/internal/convert/converter.go +++ b/hrp/pkg/convert/converter.go @@ -13,6 +13,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -212,7 +213,7 @@ func (c *TCaseConverter) ToPyTest() (string, error) { } args := append([]string{"make"}, jsonPath) - err = builtin.ExecPython3Command("httprunner", args...) + err = myexec.ExecPython3Command("httprunner", args...) if err != nil { return "", err } diff --git a/hrp/internal/convert/converter_test.go b/hrp/pkg/convert/converter_test.go similarity index 100% rename from hrp/internal/convert/converter_test.go rename to hrp/pkg/convert/converter_test.go diff --git a/hrp/internal/convert/from_curl.go b/hrp/pkg/convert/from_curl.go similarity index 100% rename from hrp/internal/convert/from_curl.go rename to hrp/pkg/convert/from_curl.go diff --git a/hrp/internal/convert/from_curl_test.go b/hrp/pkg/convert/from_curl_test.go similarity index 100% rename from hrp/internal/convert/from_curl_test.go rename to hrp/pkg/convert/from_curl_test.go diff --git a/hrp/internal/convert/from_gotest.go b/hrp/pkg/convert/from_gotest.go similarity index 90% rename from hrp/internal/convert/from_gotest.go rename to hrp/pkg/convert/from_gotest.go index 04897c76..eecde5a5 100644 --- a/hrp/internal/convert/from_gotest.go +++ b/hrp/pkg/convert/from_gotest.go @@ -7,7 +7,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) func convert2GoTestScripts(paths ...string) error { @@ -48,7 +48,7 @@ func convert2GoTestScripts(paths ...string) error { } // format pytest scripts with black - return builtin.ExecPython3Command("black", pytestPaths...) + return myexec.ExecPython3Command("black", pytestPaths...) } //go:embed testcase.tmpl diff --git a/hrp/internal/convert/from_har.go b/hrp/pkg/convert/from_har.go similarity index 100% rename from hrp/internal/convert/from_har.go rename to hrp/pkg/convert/from_har.go diff --git a/hrp/internal/convert/from_har_test.go b/hrp/pkg/convert/from_har_test.go similarity index 100% rename from hrp/internal/convert/from_har_test.go rename to hrp/pkg/convert/from_har_test.go diff --git a/hrp/internal/convert/from_json.go b/hrp/pkg/convert/from_json.go similarity index 100% rename from hrp/internal/convert/from_json.go rename to hrp/pkg/convert/from_json.go diff --git a/hrp/internal/convert/from_postman.go b/hrp/pkg/convert/from_postman.go similarity index 100% rename from hrp/internal/convert/from_postman.go rename to hrp/pkg/convert/from_postman.go diff --git a/hrp/internal/convert/from_postman_test.go b/hrp/pkg/convert/from_postman_test.go similarity index 100% rename from hrp/internal/convert/from_postman_test.go rename to hrp/pkg/convert/from_postman_test.go diff --git a/hrp/internal/convert/from_pytest.go b/hrp/pkg/convert/from_pytest.go similarity index 100% rename from hrp/internal/convert/from_pytest.go rename to hrp/pkg/convert/from_pytest.go diff --git a/hrp/internal/convert/from_swagger.go b/hrp/pkg/convert/from_swagger.go similarity index 100% rename from hrp/internal/convert/from_swagger.go rename to hrp/pkg/convert/from_swagger.go diff --git a/hrp/internal/convert/from_yaml.go b/hrp/pkg/convert/from_yaml.go similarity index 100% rename from hrp/internal/convert/from_yaml.go rename to hrp/pkg/convert/from_yaml.go diff --git a/hrp/internal/convert/testcase.tmpl b/hrp/pkg/convert/testcase.tmpl similarity index 100% rename from hrp/internal/convert/testcase.tmpl rename to hrp/pkg/convert/testcase.tmpl diff --git a/hrp/pkg/httpstat/demo/main_test.go b/hrp/pkg/httpstat/demo/main_test.go new file mode 100644 index 00000000..e14bfed7 --- /dev/null +++ b/hrp/pkg/httpstat/demo/main_test.go @@ -0,0 +1,38 @@ +package demo + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/httpstat" +) + +func TestMain(t *testing.T) { + var httpStat httpstat.Stat + + req, _ := http.NewRequest("GET", "https://httprunner.com", nil) + ctx := httpstat.WithHTTPStat(req, &httpStat) + + client := &http.Client{ + Timeout: time.Second * 10, + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + if resp != nil { + defer resp.Body.Close() + } + + // get stat + httpStat.Finish() + result := httpStat.Durations() + fmt.Println(result) + + // print stat + httpStat.Print() +} diff --git a/hrp/internal/httpstat/main.go b/hrp/pkg/httpstat/main.go similarity index 98% rename from hrp/internal/httpstat/main.go rename to hrp/pkg/httpstat/main.go index 49242193..8688bf25 100644 --- a/hrp/internal/httpstat/main.go +++ b/hrp/pkg/httpstat/main.go @@ -11,6 +11,7 @@ import ( "net/http/httptrace" "strconv" "strings" + "sync" "time" "github.com/fatih/color" @@ -100,6 +101,8 @@ type Stat struct { // connected network info network, addr string + + mux *sync.RWMutex // avoid data race } // Finish sets the time when reading response is done. @@ -176,6 +179,7 @@ func (s *Stat) Print() { // WithHTTPStat is a wrapper of httptrace.WithClientTrace. // It records the time of each httptrace hooks. func WithHTTPStat(req *http.Request, s *Stat) context.Context { + s.mux = new(sync.RWMutex) s.schema = req.URL.Scheme return httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{ DNSStart: func(i httptrace.DNSStartInfo) { @@ -228,6 +232,8 @@ func WithHTTPStat(req *http.Request, s *Stat) context.Context { }, WroteRequest: func(info httptrace.WroteRequestInfo) { + s.mux.Lock() + defer s.mux.Unlock() now := time.Now() s.serverStart = now @@ -259,6 +265,8 @@ func WithHTTPStat(req *http.Request, s *Stat) context.Context { }, GotFirstResponseByte: func() { + s.mux.Lock() + defer s.mux.Unlock() s.serverDone = time.Now() s.ServerProcessing = s.serverDone.Sub(s.serverStart) s.StartTransfer = s.serverDone.Sub(s.dnsStart) diff --git a/hrp/pkg/uixt/README.md b/hrp/pkg/uixt/README.md new file mode 100644 index 00000000..c422d5d6 --- /dev/null +++ b/hrp/pkg/uixt/README.md @@ -0,0 +1,51 @@ +# uixt + +From v4.3.0,HttpRunner will support mobile UI automation testing: + +- iOS: based on [appium/WebDriverAgent], with forked client library [electricbubble/gwda] in golang +- Android: based on [appium-uiautomator2-server], with forked client library [electricbubble/guia2] in golang + +Some UI recognition algorithms are also introduced for both iOS and Android: + +- OpenCV: based on [OpenCV 4], with golang bindings [hybridgroup/gocv] and helper utils [electricbubble/gwda-ext-opencv] +- OCR: based on OCR API service from [volcengine], other API service may be extended + +## Dependencies + +### OpenCV + +[OpenCV 4] should be pre-installed. + +You can install OpenCV 4.6.0 using Homebrew on macOS. + +```bash +$ brew install opencv +``` + +You can get more installation introduction on [hybridgroup/gocv]. + +### OCR + +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 + +## Thanks + +This uixt module is initially forked from the following repos and made a lot of changes. + +- [electricbubble/gwda-ext-opencv] +- [electricbubble/gwda] +- [electricbubble/guia2] + + +[electricbubble/gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv +[appium/WebDriverAgent]: https://github.com/appium/WebDriverAgent +[electricbubble/gwda]: https://github.com/electricbubble/gwda +[electricbubble/guia2]: https://github.com/electricbubble/guia2 +[OpenCV 4]: https://opencv.org/ +[hybridgroup/gocv]: https://github.com/hybridgroup/gocv +[volcengine]: https://www.volcengine.com/product/text-recognition +[appium-uiautomator2-server]: https://github.com/appium/appium-uiautomator2-server diff --git a/hrp/pkg/uixt/android_action.go b/hrp/pkg/uixt/android_action.go new file mode 100644 index 00000000..b8081614 --- /dev/null +++ b/hrp/pkg/uixt/android_action.go @@ -0,0 +1,158 @@ +package uixt + +import "strings" + +type touchGesture struct { + Touch PointF `json:"touch"` + Time float64 `json:"time"` +} + +type TouchAction []touchGesture + +func NewTouchAction(cap ...int) *TouchAction { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(TouchAction, 0, cap[0]) + return &tmp +} + +func (ta *TouchAction) Add(x, y int, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(x), float64(y), startTime...) +} + +func (ta *TouchAction) AddFloat(x, y float64, startTime ...float64) *TouchAction { + if len(startTime) == 0 { + var tmp float64 = 0 + if len(*ta) != 0 { + g := (*ta)[len(*ta)-1] + tmp = g.Time + 0.05 + } + startTime = []float64{tmp} + } + *ta = append(*ta, touchGesture{Touch: PointF{x, y}, Time: startTime[0]}) + return ta +} + +func (ta *TouchAction) AddPoint(point Point, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(point.X), float64(point.Y), startTime...) +} + +func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchAction { + return ta.AddFloat(point.X, point.Y, startTime...) +} + +func (ud *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { + // Must provide coordinates for at least 2 pointers + actions := make([]*TouchAction, 0) + actions = append(actions, gesture1, gesture2) + if len(tas) != 0 { + actions = append(actions, tas...) + } + data := map[string]interface{}{ + "actions": actions, + } + // register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/touch/multi/perform") + return +} + +type w3cGesture map[string]interface{} + +func _newW3CGesture() w3cGesture { + return make(w3cGesture) +} + +func (g w3cGesture) _set(key string, value interface{}) w3cGesture { + g[key] = value + return g +} + +func (g w3cGesture) pause(duration float64) w3cGesture { + return g._set("type", "pause"). + _set("duration", duration) +} + +func (g w3cGesture) keyDown(value string) w3cGesture { + return g._set("type", "keyDown"). + _set("value", value) +} + +func (g w3cGesture) keyUp(value string) w3cGesture { + return g._set("type", "keyUp"). + _set("value", value) +} + +func (g w3cGesture) pointerDown(button int) w3cGesture { + return g._set("type", "pointerDown")._set("button", button) +} + +func (g w3cGesture) pointerUp(button int) w3cGesture { + return g._set("type", "pointerUp")._set("button", button) +} + +func (g w3cGesture) pointerMove(x, y float64, origin string, duration float64, pressureAndSize ...float64) w3cGesture { + switch len(pressureAndSize) { + case 1: + g._set("pressure", pressureAndSize[0]) + case 2: + g._set("pressure", pressureAndSize[0]) + g._set("size", pressureAndSize[1]) + } + return g._set("type", "pointerMove"). + _set("duration", duration). + _set("origin", origin). + _set("x", x). + _set("y", y) +} + +func (g w3cGesture) size(size ...float64) w3cGesture { + if len(size) == 0 { + size = []float64{1.0} + } + return g._set("size", size[0]) +} + +func (g w3cGesture) pressure(pressure ...float64) w3cGesture { + if len(pressure) == 0 { + pressure = []float64{1.0} + } + return g._set("pressure", pressure[0]) +} + +type W3CGestures []w3cGesture + +func NewW3CGestures(cap ...int) *W3CGestures { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(W3CGestures, 0, cap[0]) + return &tmp +} + +func (g *W3CGestures) Pause(duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pause(duration[0]*1000)) + return g +} + +func (g *W3CGestures) KeyDown(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyDown(value)) + return g +} + +func (g *W3CGestures) KeyUp(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyUp(value)) + return g +} + +func (g *W3CGestures) SendKeys(text string) *W3CGestures { + ss := strings.Split(text, "") + for i := range ss { + g.KeyDown(ss[i]) + g.KeyUp(ss[i]) + } + return g +} diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go new file mode 100644 index 00000000..492bf765 --- /dev/null +++ b/hrp/pkg/uixt/android_device.go @@ -0,0 +1,689 @@ +package uixt + +import ( + "bytes" + "context" + "fmt" + "net" + "os/exec" + "reflect" + "strings" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" +) + +var ( + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerPort = 6790 + DeviceTempPath = "/data/local/tmp" +) + +const forwardToPrefix = "forward-to-" + +type AndroidDeviceOption func(*AndroidDevice) + +func WithSerialNumber(serial string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.SerialNumber = serial + } +} + +func WithAdbIP(ip string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.IP = ip + } +} + +func WithAdbPort(port int) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.Port = port + } +} + +func WithAdbLogOn(logOn bool) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.LogOn = logOn + } +} + +func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceOption) { + if dev.SerialNumber != "" { + deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) + } + if dev.IP != "" { + deviceOptions = append(deviceOptions, WithAdbIP(dev.IP)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithAdbPort(dev.Port)) + } + if dev.LogOn { + deviceOptions = append(deviceOptions, WithAdbLogOn(true)) + } + return +} + +func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) { + deviceList, err := DeviceList() + if err != nil { + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("get attached devices failed: %v", err)) + } + + device = &AndroidDevice{ + Port: UIA2ServerPort, + IP: AdbServerHost, + } + for _, option := range options { + option(device) + } + + serialNumber := device.SerialNumber + for _, dev := range deviceList { + // find device by serial number if specified + if serialNumber != "" && dev.Serial() != serialNumber { + continue + } + + device.SerialNumber = dev.Serial() + device.d = dev + device.logcat = NewAdbLogcat(serialNumber) + return device, nil + } + + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("device %s not found", device.SerialNumber)) +} + +func DeviceList() (devices []gadb.Device, err error) { + var adbClient gadb.Client + if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { + return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error()) + } + + return adbClient.DeviceList() +} + +type AndroidDevice struct { + d gadb.Device + logcat *DeviceLogcat + SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` + IP string `json:"ip,omitempty" yaml:"ip,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +func (dev *AndroidDevice) UUID() string { + return dev.SerialNumber +} + +func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { + driver, err := dev.NewUSBDriver(capabilities) + if err != nil { + return nil, errors.Wrap(err, "failed to init UIA driver") + } + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend UIA Driver") + } + + if dev.LogOn { + err = driverExt.Driver.StartCaptureLog("hrp_adb_log") + if err != nil { + return nil, err + } + } + + driverExt.UUID = dev.UUID() + return driverExt, err +} + +// NewUSBDriver creates new client via USB connected device, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + var localPort int + if localPort, err = getFreePort(); err != nil { + return nil, err + } + if err = dev.d.Forward(localPort, UIA2ServerPort); err != nil { + return nil, err + } + + rawURL := fmt.Sprintf("http://%s%d:6790/wd/hub", forwardToPrefix, localPort) + driver, err = NewUIADriver(capabilities, rawURL) + if err != nil { + _ = dev.d.ForwardKill(localPort) + return nil, err + } + driver.adbDevice = dev.d + driver.logcat = dev.logcat + driver.localPort = localPort + + return driver, nil +} + +// NewHTTPDriver creates new remote HTTP client, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.IP, dev.Port) + if driver, err = NewUIADriver(capabilities, rawURL); err != nil { + return nil, err + } + driver.adbDevice = dev.d + return driver, nil +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, errors.Wrap(err, "resolve tcp addr failed") + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, errors.Wrap(err, "listen tcp addr failed") + } + defer func() { _ = l.Close() }() + return l.Addr().(*net.TCPAddr).Port, nil +} + +type DeviceLogcat struct { + serial string + logBuffer *bytes.Buffer + errs []error + stopping chan struct{} + done chan struct{} + cmd *exec.Cmd +} + +func NewAdbLogcat(serial string) *DeviceLogcat { + return &DeviceLogcat{ + serial: serial, + logBuffer: new(bytes.Buffer), + stopping: make(chan struct{}), + done: make(chan struct{}), + } +} + +// CatchLogcatContext starts logcat with timeout context +func (l *DeviceLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) { + if err = l.CatchLogcat(); err != nil { + return + } + go func() { + select { + case <-timeoutCtx.Done(): + _ = l.Stop() + case <-l.stopping: + } + }() + return +} + +func (l *DeviceLogcat) Stop() error { + select { + case <-l.stopping: + default: + close(l.stopping) + <-l.done + close(l.done) + } + return l.Errors() +} + +func (l *DeviceLogcat) Errors() (err error) { + for _, e := range l.errs { + if err != nil { + err = fmt.Errorf("%v |[DeviceLogcatErr] %v", err, e) + } else { + err = fmt.Errorf("[DeviceLogcatErr] %v", e) + } + } + return +} + +func (l *DeviceLogcat) CatchLogcat() (err error) { + if l.cmd != nil { + log.Warn().Msg("logcat already start") + return nil + } + + // clear logcat + if err = myexec.RunCommand("adb", "-s", l.serial, "logcat", "-c"); err != nil { + return + } + + // start logcat + l.cmd = myexec.Command("adb", "-s", l.serial, "logcat", "-v", "time", "-s", "iesqaMonitor:V") + l.cmd.Stderr = l.logBuffer + l.cmd.Stdout = l.logBuffer + if err = l.cmd.Start(); err != nil { + return + } + go func() { + <-l.stopping + if e := myexec.KillProcessesByGpid(l.cmd); e != nil { + l.errs = append(l.errs, fmt.Errorf("kill logcat process err:%v", e)) + } + l.done <- struct{}{} + }() + return +} + +func (l *DeviceLogcat) BufferedLogcat() (err error) { + // -d: dump the current buffered logcat result and exits + cmd := myexec.Command("adb", "-s", l.serial, "logcat", "-d") + cmd.Stdout = l.logBuffer + cmd.Stderr = l.logBuffer + if err = cmd.Run(); err != nil { + return + } + return +} + +type ExportPoint struct { + Start int `json:"start" yaml:"start"` + End int `json:"end" yaml:"end"` + From interface{} `json:"from" yaml:"from"` + To interface{} `json:"to" yaml:"to"` + Operation string `json:"operation" yaml:"operation"` + Ext string `json:"ext" yaml:"ext"` + RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"` +} + +func ConvertPoints(data string) (eps []ExportPoint) { + lines := strings.Split(data, "\n") + for _, line := range lines { + if strings.Contains(line, "ext") { + idx := strings.Index(line, "{") + line = line[idx:] + p := ExportPoint{} + err := json.Unmarshal([]byte(line), &p) + if err != nil { + log.Error().Msg("failed to parse point data") + continue + } + eps = append(eps, p) + } + } + return +} + +type UiSelectorHelper struct { + value *bytes.Buffer +} + +func NewUiSelectorHelper() UiSelectorHelper { + return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")} +} + +func (s UiSelectorHelper) String() string { + return s.value.String() + ";" +} + +// Text Set the search criteria to match the visible text displayed +// in a widget (for example, the text label to launch an app). +// +// The text for the element must match exactly with the string in your input +// argument. Matching is case-sensitive. +func (s UiSelectorHelper) Text(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.text("%s")`, text)) + return s +} + +// TextMatches Set the search criteria to match the visible text displayed in a layout +// element, using a regular expression. +// +// The text in the widget must match exactly with the string in your +// input argument. +func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex)) + return s +} + +// TextStartsWith Set the search criteria to match visible text in a widget that is +// prefixed by the text parameter. +// +// The matching is case-insensitive. +func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text)) + return s +} + +// TextContains Set the search criteria to match the visible text in a widget +// where the visible text must contain the string in your input argument. +// +// The matching is case-sensitive. +func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text)) + return s +} + +// ClassName Set the search criteria to match the class property +// for a widget (for example, "android.widget.Button"). +func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.className("%s")`, className)) + return s +} + +// ClassNameMatches Set the search criteria to match the class property +// for a widget, using a regular expression. +func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex)) + return s +} + +// Description Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +// +// Matching is case-sensitive. +func (s UiSelectorHelper) Description(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc)) + return s +} + +// DescriptionMatches Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex)) + return s +} + +// DescriptionStartsWith Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must start +// with the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc)) + return s +} + +// DescriptionContains Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must contain +// the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc)) + return s +} + +// ResourceId Set the search criteria to match the given resource ID. +func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id)) + return s +} + +// ResourceIdMatches Set the search criteria to match the resource ID +// of the widget, using a regular expression. +func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex)) + return s +} + +// Index Set the search criteria to match the widget by its node +// index in the layout hierarchy. +// +// The index value must be 0 or greater. +// +// Using the index can be unreliable and should only +// be used as a last resort for matching. Instead, +// consider using the `Instance(int)` method. +func (s UiSelectorHelper) Index(index int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.index(%d)`, index)) + return s +} + +// Instance Set the search criteria to match the +// widget by its instance number. +// +// The instance value must be 0 or greater, where +// the first instance is 0. +// +// For example, to simulate a user click on +// the third image that is enabled in a UI screen, you +// could specify a a search criteria where the instance is +// 2, the `className(String)` matches the image +// widget class, and `enabled(boolean)` is true. +// The code would look like this: +// `new UiSelector().className("android.widget.ImageView") +// .enabled(true).instance(2);` +func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance)) + return s +} + +// Enabled Set the search criteria to match widgets that are enabled. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b)) + return s +} + +// Focused Set the search criteria to match widgets that have focus. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b)) + return s +} + +// Focusable Set the search criteria to match widgets that are focusable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b)) + return s +} + +// Scrollable Set the search criteria to match widgets that are scrollable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b)) + return s +} + +// Selected Set the search criteria to match widgets that +// are currently selected. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b)) + return s +} + +// Checked Set the search criteria to match widgets that +// are currently checked (usually for checkboxes). +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b)) + return s +} + +// Checkable Set the search criteria to match widgets that are checkable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b)) + return s +} + +// Clickable Set the search criteria to match widgets that are clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b)) + return s +} + +// LongClickable Set the search criteria to match widgets that are long-clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b)) + return s +} + +// packageName Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) packageName(name string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name)) + return s +} + +// PackageNameMatches Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex)) + return s +} + +// ChildSelector Adds a child UiSelector criteria to this selector. +// +// Use this selector to narrow the search scope to +// child widgets under a specific parent widget. +func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String())) + return s +} + +// FromParent Adds a child UiSelector criteria to this selector which is used to +// start search from the parent widget. +// +// Use this selector to narrow the search scope to +// sibling widgets as well all child widgets under a parent. +func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String())) + return s +} + +type AndroidBySelector struct { + // Set the search criteria to match the given resource ResourceIdID. + ResourceIdID string `json:"id"` + // Set the search criteria to match the content-description property for a widget. + ContentDescription string `json:"accessibility id"` + XPath string `json:"xpath"` + // Set the search criteria to match the class property for a widget (for example, "android.widget.Button"). + ClassName string `json:"class name"` + UiAutomator string `json:"-android uiautomator"` +} + +func (by AndroidBySelector) getMethodAndSelector() (method, selector string) { + vBy := reflect.ValueOf(by) + tBy := reflect.TypeOf(by) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + // switch vi := vi.(type) { + // case string: + // selector = vi + // } + selector = vi.(string) + if selector != "" && selector != "UNKNOWN" { + method = tBy.Field(i).Tag.Get("json") + return + } + } + return +} diff --git a/hrp/pkg/uixt/android_device_test.go b/hrp/pkg/uixt/android_device_test.go new file mode 100644 index 00000000..2167d2f5 --- /dev/null +++ b/hrp/pkg/uixt/android_device_test.go @@ -0,0 +1,18 @@ +package uixt + +import ( + "fmt" + "testing" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +func TestConvertPoints(t *testing.T) { + data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}" + eps := ConvertPoints(data) + if len(eps) != 3 { + t.Fatal() + } + jsons, _ := json.Marshal(eps) + println(fmt.Sprintf("%v", string(jsons))) +} diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go new file mode 100644 index 00000000..904b550f --- /dev/null +++ b/hrp/pkg/uixt/android_driver.go @@ -0,0 +1,985 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/url" + "strconv" + "strings" + "time" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" +) + +var errDriverNotImplemented = errors.New("driver method not implemented") + +type uiaDriver struct { + Driver + + adbDevice gadb.Device + logcat *DeviceLogcat + localPort int +} + +func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDriver, err error) { + if capabilities == nil { + capabilities = NewCapabilities() + } + driver = new(uiaDriver) + if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { + return nil, err + } + var localPort int + { + tmpURL, _ := url.Parse(driver.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) + } + } + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + driver.client = convertToHTTPClient(conn) + if session, err := driver.NewSession(capabilities); err != nil { + return nil, err + } else { + driver.sessionId = session.SessionId + } + return +} + +type BatteryStatus int + +const ( + _ = iota + BatteryStatusUnknown BatteryStatus = iota + BatteryStatusCharging + BatteryStatusDischarging + BatteryStatusNotCharging + BatteryStatusFull +) + +func (bs BatteryStatus) String() string { + switch bs { + case BatteryStatusUnknown: + return "unknown" + case BatteryStatusCharging: + return "charging" + case BatteryStatusDischarging: + return "discharging" + case BatteryStatusNotCharging: + return "not charging" + case BatteryStatusFull: + return "full" + default: + return fmt.Sprintf("unknown status code (%d)", bs) + } +} + +func (ud *uiaDriver) Close() (err error) { + if ud.sessionId == "" { + return nil + } + if _, err = ud.httpDELETE("/session", ud.sessionId); err == nil { + ud.sessionId = "" + } + + return err +} + +func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { + // register(postHandler, new NewSession("/wd/hub/session")) + var rawResp rawResponse + data := map[string]interface{}{"capabilities": capabilities} + if rawResp, err = ud.httpPOST(data, "/session"); err != nil { + return SessionInfo{SessionId: ""}, err + } + reply := new(struct{ Value struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return SessionInfo{SessionId: ""}, err + } + sessionID := reply.Value.SessionId + // d.sessionIdCache[sessionID] = true + return SessionInfo{SessionId: sessionID}, nil +} + +func (ud *uiaDriver) ActiveSession() (sessionInfo SessionInfo, err error) { + // [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)] + return SessionInfo{SessionId: ud.sessionId}, nil +} + +func (ud *uiaDriver) SessionIDs() (sessionIDs []string, err error) { + // register(getHandler, new GetSessions("/wd/hub/sessions")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/sessions"); err != nil { + return nil, err + } + reply := new(struct{ Value []struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + sessionIDs = make([]string, len(reply.Value)) + for i := range reply.Value { + sessionIDs[i] = reply.Value[i].SessionId + } + return +} + +func (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { + // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + scrollData = reply.Value + return +} + +func (ud *uiaDriver) DeleteSession() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Status() (deviceStatus DeviceStatus, err error) { + // register(getHandler, new Status("/wd/hub/status")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/status"); err != nil { + return DeviceStatus{Ready: false}, err + } + reply := new(struct { + Value struct { + // Message string + Ready bool + } + }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceStatus{Ready: false}, err + } + return DeviceStatus{Ready: true}, nil +} + +func (ud *uiaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { + // register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/info"); err != nil { + return DeviceInfo{}, err + } + reply := new(struct{ Value struct{ DeviceInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceInfo{}, err + } + deviceInfo = reply.Value.DeviceInfo + return +} + +func (ud *uiaDriver) Location() (location Location, err error) { + // TODO + return location, errDriverNotImplemented +} + +func (ud *uiaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { + // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/battery_info"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value struct{ BatteryInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + if reply.Value.Level == -1 || reply.Value.Status == -1 { + return reply.Value.BatteryInfo, errors.New("cannot be retrieved from the system") + } + batteryInfo = reply.Value.BatteryInfo + return +} + +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 + } + reply := new(struct{ Value struct{ Size } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + size = reply.Value.Size + return +} + +func (ud *uiaDriver) Screen() (screen Screen, err error) { + // TODO + return screen, errDriverNotImplemented +} + +func (ud *uiaDriver) Scale() (scale float64, err error) { + return 1, nil +} + +// PressBack simulates a short press on the BACK button. +func (ud *uiaDriver) PressBack() (err error) { + // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) + _, err = ud.httpPOST(nil, "/session", ud.sessionId, "back") + return +} + +func (ud *uiaDriver) StartCamera() (err error) { + if _, err = ud.adbDevice.RunShellCommand("rm", "-r", "/sdcard/DCIM/Camera"); err != nil { + return err + } + time.Sleep(5 * time.Second) + var version string + if version, err = ud.adbDevice.RunShellCommand("getprop", "ro.build.version.release"); err != nil { + return err + } + if version == "11" || version == "12" { + if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.STILL_IMAGE_CAMERA"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "swipe", "750", "1000", "250", "1000"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + return + } else { + if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + return + } +} + +func (ud *uiaDriver) StopCamera() (err error) { + err = ud.PressBack() + if err != nil { + return err + } + err = ud.Homescreen() + if err != nil { + return err + } + + // kill samsung shell command + if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.sec.android.app.camera"); err != nil { + return err + } + + // kill other camera (huawei mi) + if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.android.camera2"); err != nil { + return err + } + return +} + +func (ud *uiaDriver) ActiveAppInfo() (info AppInfo, err error) { + // TODO + return info, errDriverNotImplemented +} + +func (ud *uiaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { + // TODO + return appsList, errDriverNotImplemented +} + +func (ud *uiaDriver) AppState(bundleId string) (runState AppState, err error) { + // TODO + return runState, errDriverNotImplemented +} + +func (ud *uiaDriver) IsLocked() (locked bool, err error) { + // TODO + return locked, errDriverNotImplemented +} + +func (ud *uiaDriver) Unlock() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Lock() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Homescreen() (err error) { + return ud.PressKeyCode(KCHome, KMEmpty) +} + +func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + return ud._pressKeyCode(keyCode, metaState, KFFromSystem) +} + +func (ud *uiaDriver) _pressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + // register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode")) + data := map[string]interface{}{ + "keycode": keyCode, + } + if metaState != KMEmpty { + data["metastate"] = metaState + } + if len(flags) != 0 { + data["flags"] = flags[0] + } + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/press_keycode") + return +} + +func (ud *uiaDriver) AlertText() (text string, err error) { + // register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "alert/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + text = reply.Value + return +} + +func (ud *uiaDriver) AlertButtons() (btnLabels []string, err error) { + // TODO + return btnLabels, errDriverNotImplemented +} + +func (ud *uiaDriver) AlertAccept(label ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(label) != 0 { + data["buttonLabel"] = label[0] + } + // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/accept") + return +} + +func (ud *uiaDriver) AlertDismiss(label ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(label) != 0 { + data["buttonLabel"] = label[0] + } + // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/dismiss") + return +} + +func (ud *uiaDriver) AlertSendKeys(text string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) check() error { + if ud.adbDevice.Serial() == "" { + return errors.New("adb daemon: the device is not ready") + } + return nil +} + +func (ud *uiaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { + if err = ud.check(); err != nil { + return err + } + + var sOutput string + if sOutput, err = ud.adbDevice.RunShellCommand("monkey -p", bundleId, "-c android.intent.category.LAUNCHER 1"); err != nil { + return err + } + if strings.Contains(sOutput, "monkey aborted") { + return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) + } + + if len(launchOpt) != 0 { + var ce error + exists := func(ud WebDriver) (bool, error) { + for _, opt := range launchOpt { + if waitForComplete, ok := opt["androidBySelector"]; ok { + for _, e := range waitForComplete.([]BySelector) { + _, ce = ud.FindElement(e) + if ce == nil { + return true, nil + } + } + } + } + return false, nil + } + if err = ud.WaitWithTimeoutAndInterval(exists, 45, 1); err != nil { + return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + } + } + return +} + +func (ud *uiaDriver) AppLaunchUnattached(bundleId string) (err error) { + // TODO + return errDriverNotImplemented +} + +// Dispose corresponds to the command: +// adb -s $serial forward --remove $localPort +func (ud *uiaDriver) Dispose() (err error) { + if err = ud.check(); err != nil { + return err + } + if ud.localPort == 0 { + return nil + } + return ud.adbDevice.ForwardKill(ud.localPort) +} + +func (ud *uiaDriver) AppTerminate(bundleId string) (successful bool, err error) { + if err = ud.check(); err != nil { + return false, err + } + + _, err = ud.adbDevice.RunShellCommand("am force-stop", bundleId) + return err == nil, err +} + +func (ud *uiaDriver) AppActivate(bundleId string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) AppDeactivate(second float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) AppAuthReset(resource ProtectedResource) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error { + return ud.TapFloat(float64(x), float64(y), options...) +} + +func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (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 + d := NewData(data, options...) + + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "appium/tap") + return +} + +func (ud *uiaDriver) DoubleTap(x, y int) error { + return ud.DoubleTapFloat(float64(x), float64(y)) +} + +func (ud *uiaDriver) DoubleTapFloat(x, y float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) { + return ud.TouchAndHoldFloat(float64(x), float64(y), second...) +} + +func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) { + if len(second) == 0 { + second = []float64{1.0} + } + // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + "duration": int(second[0] * 1000), + }, + } + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/longclick") + return +} + +func (ud *uiaDriver) _drag(data map[string]interface{}) (err error) { + // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/drag") + return +} + +// Drag performs a swipe from one coordinate to another coordinate. You can control +// 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 { + 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) { + data := map[string]interface{}{ + "startX": fromX, + "startY": fromY, + "endX": toX, + "endY": toY, + } + + // new data options in post data for extra uiautomator configurations + d := NewData(data, options...) + + return ud._drag(d.Data) +} + +func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...DataOption) (err error) { + // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) + data := map[string]interface{}{ + "startX": startX, + "startY": startY, + "endX": endX, + "endY": endY, + } + + // new data options in post data for extra uiautomator configurations + d := NewData(data, options...) + + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "touch/perform") + return +} + +// Swipe performs a swipe from one coordinate to another using the number of steps +// to determine smoothness and speed. Each step execution is throttled to 5ms +// per step. So for a 100 steps, the swipe will take about 1/2 second to complete. +// `steps` is the number of move steps sent to the system +func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { + options = append(options, WithDataSteps(12)) + return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { + return ud._swipe(fromX, fromY, toX, toY, options...) +} + +func (ud *uiaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error { + return ud.ForceTouchFloat(float64(x), float64(y), pressure, second...) +} + +func (ud *uiaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) PerformW3CActions(actions *W3CActions) (err error) { + data := map[string]interface{}{ + "actions": actions, + } + // register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions") + return +} + +func (ud *uiaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { + lbl := content + + const defaultLabelLen = 10 + if len(lbl) > defaultLabelLen { + lbl = lbl[:defaultLabelLen] + } + + data := map[string]interface{}{ + "contentType": contentType, + "label": lbl, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + } + // register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/set_clipboard") + return +} + +func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { + if len(contentType) == 0 { + contentType = PasteboardTypePlaintext + } + // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) + data := map[string]interface{}{ + "contentType": contentType[0], + } + var rawResp rawResponse + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/get_clipboard"); err != nil { + return + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return + } + + if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil { + raw.Write([]byte(reply.Value)) + } else { + raw.Write(data) + } + return +} + +func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (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 + d := NewData(data, options...) + + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "keys") + return +} + +func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { + data := map[string]interface{}{ + "view": text, + } + // new data options in post data for extra uiautomator configurations + d := NewData(data, options...) + + var element WebElement + if valuetext, ok := d.Data["textview"]; ok { + element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().TextContains(fmt.Sprintf("%v", valuetext)).String()}) + } else if valueid, ok := d.Data["id"]; ok { + element, err = ud.FindElement(BySelector{ResourceIdID: fmt.Sprintf("%v", valueid)}) + } else if valuedesc, ok := d.Data["description"]; ok { + element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().Description(fmt.Sprintf("%v", valuedesc)).String()}) + } else { + element, err = ud.FindElement(BySelector{ClassName: ElementType{EditText: true}}) + } + if err != nil { + return err + } + return element.SendKeys(text, options...) +} + +func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) PressButton(devBtn DeviceButton) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { + // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) + _, err = ud.httpPOST(nil, "/session", ud.sessionId, "appium/device/open_notifications") + return +} + +func (ud *uiaDriver) SiriActivate(text string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) SiriOpenUrl(url string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Orientation() (orientation Orientation, err error) { + // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "orientation"); err != nil { + return "", err + } + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + orientation = reply.Value + return +} + +func (ud *uiaDriver) SetOrientation(orientation Orientation) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Rotation() (rotation Rotation, err error) { + // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "rotation"); err != nil { + return Rotation{}, err + } + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + + rotation = reply.Value + return +} + +func (ud *uiaDriver) SetRotation(rotation Rotation) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) MatchTouchID(isMatch bool) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []WebElement, err error) { + // register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { + return nil, err + } + reply := new(struct{ Value []map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + elements = make([]WebElement, len(reply.Value)) + for i, elem := range reply.Value { + var id string + if id = elementIDFromValue(elem); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + uie := WebElement(uiaElement{parent: ud, id: id}) + elements[i] = uie + } + return +} + +func (ud *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { + // register(postHandler, new FindElement("/wd/hub/session/:sessionId/element")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/element"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + var id string + if id = elementIDFromValue(reply.Value); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elem = &uiaElement{parent: ud, id: id} + return +} + +func (ud *uiaDriver) ActiveElement() (element WebElement, err error) { + // TODO + return element, errDriverNotImplemented +} + +func (ud *uiaDriver) FindElement(by BySelector) (element WebElement, err error) { + return ud._findElement(by.getUsingAndValue()) +} + +func (ud *uiaDriver) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = WebElement(uiaElement{parent: ud, id: elementIDs[i]}) + } + return +} + +func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { + // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "screenshot"); err != nil { + return nil, errors.Wrap(code.AndroidScreenShotError, + fmt.Sprintf("get UIA screenshot data failed: %v", err)) + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, errors.Wrap(code.AndroidScreenShotError, + fmt.Sprintf("decode UIA screenshot data failed: %v", err)) + } + + raw = bytes.NewBuffer(decodeStr) + return +} + +func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) { + // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "source"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + source = reply.Value + return +} + +func (ud *uiaDriver) AccessibleSource() (source string, err error) { + // TODO + return source, errDriverNotImplemented +} + +func (ud *uiaDriver) HealthCheck() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { + // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + settings = reply.Value + return +} + +func (ud *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { + data := map[string]interface{}{ + "settings": settings, + } + // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/settings") + return +} + +func (ud *uiaDriver) IsHealthy() (healthy bool, err error) { + // TODO + return healthy, errDriverNotImplemented +} + +func (ud *uiaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { + startTime := time.Now() + for { + done, err := condition(ud) + if err != nil { + return err + } + if done { + return nil + } + + if elapsed := time.Since(startTime); elapsed > timeout { + return fmt.Errorf("timeout after %v", elapsed) + } + time.Sleep(interval) + } +} + +func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error { + return ud.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) +} + +func (ud *uiaDriver) Wait(condition Condition) error { + return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +} + +func (ud *uiaDriver) StartCaptureLog(identifier ...string) (err error) { + log.Info().Msg("start adb log recording") + err = ud.logcat.CatchLogcat() + if err != nil { + err = errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("start adb log recording failed: %v", err)) + return err + } + return nil +} + +func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) { + log.Info().Msg("stop adb log recording") + err = ud.logcat.Stop() + if err != nil { + log.Error().Err(err).Msg("failed to get adb log recording") + err = errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("get adb log recording failed: %v", err)) + return "", err + } + content := ud.logcat.logBuffer.String() + return ConvertPoints(content), nil +} diff --git a/hrp/pkg/uixt/android_elment.go b/hrp/pkg/uixt/android_elment.go new file mode 100644 index 00000000..9d45cd39 --- /dev/null +++ b/hrp/pkg/uixt/android_elment.go @@ -0,0 +1,306 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +var errElementNotImplemented = errors.New("element method not implemented") + +type uiaElement struct { + parent *uiaDriver + id string +} + +func (ue uiaElement) Click() (err error) { + // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) + _, err = ue.parent.httpPOST(nil, "/session", ue.parent.sessionId, "/element", ue.id, "/click") + return +} + +func (ue uiaElement) SendKeys(text string, options ...DataOption) (err error) { + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) + // https://github.com/appium/appium-uiutomator2-server/blob/master/app/src/main/java/io/appium/uiutomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + } + + // new data options in post data for extra uiautomator configurations + d := NewData(data, options...) + + _, err = ue.parent.httpPOST(d.Data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") + return +} + +func (ue uiaElement) Clear() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Tap(x, y int) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TapFloat(x, y float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) DoubleTap() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TouchAndHold(second ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TwoFingerTap() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { + // Todo: implement + log.Fatal().Msg("not support") + return +} + +func (ue uiaElement) ForceTouch(pressure float64, second ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Drag(fromX, fromY, toX, toY int, steps ...float64) (err error) { + return ue.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), steps...) +} + +func (ue uiaElement) DragFloat(fromX, fromY, toX, toY float64, steps ...float64) (err error) { + if len(steps) == 0 { + steps = []float64{12 * 10} + } else { + steps[0] = 12 * 10 + } + data := map[string]interface{}{ + "elementId": ue.id, + "endX": toX, + "endY": toY, + "steps": steps[0], + } + return ue.parent._drag(data) +} + +func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error { + return ue.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY)) +} + +func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { + options := []DataOption{ + WithDataSteps(12), + WithCustomOption("elementId", ue.id), + } + return ue.parent._swipe(fromX, fromY, toX, toY, options...) +} + +func (ue uiaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Pinch(scale, velocity float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Rotate(rotation float64, velocity ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) scroll(data interface{}) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollElementByName(name string) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollElementByPredicate(predicate string) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollToVisible() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollDirection(direction Direction, distance ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) FindElement(by BySelector) (element WebElement, err error) { + method, selector := by.getMethodAndSelector() + return ue.parent._findElement(method, selector, ue.id) +} + +func (ue uiaElement) FindElements(by BySelector) (elements []WebElement, err error) { + method, selector := by.getMethodAndSelector() + return ue.parent._findElements(method, selector, ue.id) +} + +func (ue uiaElement) FindVisibleCells() (elements []WebElement, err error) { + // TODO + return elements, errElementNotImplemented +} + +func (ue uiaElement) Rect() (rect Rect, err error) { + // register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/rect"); err != nil { + return Rect{}, err + } + reply := new(struct{ Value Rect }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rect{}, err + } + rect = reply.Value + return +} + +func (ue uiaElement) Location() (point Point, err error) { + // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/location"); err != nil { + return Point{-1, -1}, err + } + reply := new(struct{ Value Point }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Point{-1, -1}, err + } + point = reply.Value + return +} + +func (ue uiaElement) Size() (size Size, err error) { + // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/size"); err != nil { + return Size{-1, -1}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{-1, -1}, err + } + size = reply.Value + return +} + +func (ue uiaElement) Text() (text string, err error) { + // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + text = reply.Value + return +} + +func (ue uiaElement) Type() (elemType string, err error) { + // TODO + return elemType, errElementNotImplemented +} + +func (ue uiaElement) IsEnabled() (enabled bool, err error) { + // TODO + return enabled, errElementNotImplemented +} + +func (ue uiaElement) IsDisplayed() (displayed bool, err error) { + // TODO + return displayed, errElementNotImplemented +} + +func (ue uiaElement) IsSelected() (selected bool, err error) { + // TODO + return selected, errElementNotImplemented +} + +func (ue uiaElement) IsAccessible() (accessible bool, err error) { + // TODO + return accessible, errElementNotImplemented +} + +func (ue uiaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { + // TODO + return isAccessibilityContainer, errElementNotImplemented +} + +func (ue uiaElement) GetAttribute(attr ElementAttribute) (value string, err error) { + // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/attribute", attr.getAttributeName()); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + value = reply.Value + return +} + +func (ue uiaElement) UID() (uid string) { + return ue.id +} + +func (ue uiaElement) Screenshot() (raw *bytes.Buffer, err error) { + // W3C endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/element/:id/screenshot")) + // JSONWP endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/screenshot/:id")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err + } + + raw = bytes.NewBuffer(decodeStr) + return +} diff --git a/hrp/pkg/uixt/android_key.go b/hrp/pkg/uixt/android_key.go new file mode 100644 index 00000000..07f0d5b7 --- /dev/null +++ b/hrp/pkg/uixt/android_key.go @@ -0,0 +1,879 @@ +package uixt + +type KeyMeta int + +const ( + KMEmpty KeyMeta = 0 // As a `null` + KMCapLocked KeyMeta = 0x100 // SHIFT key locked in CAPS mode. + KMAltLocked KeyMeta = 0x200 // ALT key locked. + KMSymLocked KeyMeta = 0x400 // SYM key locked. + KMSelecting KeyMeta = 0x800 // Text is in selection mode. + // KMAltOn KeyMeta = 0x02 // This mask is used to check whether one of the ALT meta keys is pressed. + // KMAltLeftOn KeyMeta = 0x10 // This mask is used to check whether the left ALT meta key is pressed. + // KMAltRightOn KeyMeta = 0x20 // This mask is used to check whether the right the ALT meta key is pressed. + // KMShiftOn KeyMeta = 0x1 // This mask is used to check whether one of the SHIFT meta keys is pressed. + // KMShiftLeftOn KeyMeta = 0x40 // This mask is used to check whether the left SHIFT meta key is pressed. + // KMShiftRightOn KeyMeta = 0x80 // This mask is used to check whether the right SHIFT meta key is pressed. + // KMSymOn KeyMeta = 0x4 // This mask is used to check whether the SYM meta key is pressed. + // KMFunctionOn KeyMeta = 0x8 // This mask is used to check whether the FUNCTION meta key is pressed. + // KMCtrlOn KeyMeta = 0x1000 // This mask is used to check whether one of the CTRL meta keys is pressed. + // KMCtrlLeftOn KeyMeta = 0x2000 // This mask is used to check whether the left CTRL meta key is pressed. + // KMCtrlRightOn KeyMeta = 0x4000 // This mask is used to check whether the right CTRL meta key is pressed. + // KMMetaOn KeyMeta = 0x10000 // This mask is used to check whether one of the META meta keys is pressed. + // KMMetaLeftOn KeyMeta = 0x20000 // This mask is used to check whether the left META meta key is pressed. + // KMMetaRightOn KeyMeta = 0x40000 // This mask is used to check whether the right META meta key is pressed. + // KMCapsLockOn KeyMeta = 0x100000 // This mask is used to check whether the CAPS LOCK meta key is on. + // KMNumLockOn KeyMeta = 0x200000 // This mask is used to check whether the NUM LOCK meta key is on. + // KMScrollLockOn KeyMeta = 0x400000 // This mask is used to check whether the SCROLL LOCK meta key is on. + // KMShiftMask = KMShiftOn | KMShiftLeftOn | KMShiftRightOn + // KMAltMask = KMAltOn | KMAltLeftOn | KMAltRightOn + // KMCtrlMask = KMCtrlOn | KMCtrlLeftOn | KMCtrlRightOn + // KMMetaMask = KMMetaOn | KMMetaLeftOn | KMMetaRightOn +) + +type KeyFlag int + +const ( + // KFWokeHere This mask is set if the device woke because of this key event. + // Deprecated + KFWokeHere KeyFlag = 0x1 + + // KFSoftKeyboard This mask is set if the key event was generated by a software keyboard. + KFSoftKeyboard KeyFlag = 0x2 + + // KFKeepTouchMode This mask is set if we don't want the key event to cause us to leave touch mode. + KFKeepTouchMode KeyFlag = 0x4 + + // KFFromSystem This mask is set if an event was known to come from a trusted part + // of the system. That is, the event is known to come from the user, + // and could not have been spoofed by a third party component. + KFFromSystem KeyFlag = 0x8 + + // KFEditorAction This mask is used for compatibility, to identify enter keys that are + // coming from an IME whose enter key has been auto-labelled "next" or + // "done". This allows TextView to dispatch these as normal enter keys + // for old applications, but still do the appropriate action when receiving them. + KFEditorAction KeyFlag = 0x10 + + // KFCanceled When associated with up key events, this indicates that the key press + // has been canceled. Typically this is used with virtual touch screen + // keys, where the user can slide from the virtual key area on to the + // display: in that case, the application will receive a canceled up + // event and should not perform the action normally associated with the + // key. Note that for this to work, the application can not perform an + // action for a key until it receives an up or the long press timeout has expired. + KFCanceled KeyFlag = 0x20 + + // KFVirtualHardKey This key event was generated by a virtual (on-screen) hard key area. + // Typically this is an area of the touchscreen, outside of the regular + // display, dedicated to "hardware" buttons. + KFVirtualHardKey KeyFlag = 0x40 + + // KFLongPress This flag is set for the first key repeat that occurs after the long press timeout. + KFLongPress KeyFlag = 0x80 + + // KFCanceledLongPress Set when a key event has `KFCanceled` set because a long + // press action was executed while it was down. + KFCanceledLongPress KeyFlag = 0x100 + + // KFTracking Set for `ACTION_UP` when this event's key code is still being + // tracked from its initial down. That is, somebody requested that tracking + // started on the key down and a long press has not caused + // the tracking to be canceled. + KFTracking KeyFlag = 0x200 + + // KFFallback Set when a key event has been synthesized to implement default behavior + // for an event that the application did not handle. + // Fallback key events are generated by unhandled trackball motions + // (to emulate a directional keypad) and by certain unhandled key presses + // that are declared in the key map (such as special function numeric keypad + // keys when numlock is off). + KFFallback KeyFlag = 0x400 + + // KFPredispatch Signifies that the key is being predispatched. + // KFPredispatch KeyFlag = 0x20000000 + + // KFStartTracking Private control to determine when an app is tracking a key sequence. + // KFStartTracking KeyFlag = 0x40000000 + + // KFTainted Private flag that indicates when the system has detected that this key event + // may be inconsistent with respect to the sequence of previously delivered key events, + // such as when a key up event is sent but the key was not down. + // KFTainted KeyFlag = 0x80000000 +) + +type KeyCode int + +const ( + _ KeyCode = 0 // Unknown key code. + + // KCSoftLeft Soft Left key + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom left + // of the display. + KCSoftLeft KeyCode = 1 + + // KCSoftRight Soft Right key. + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom right + // of the display. + KCSoftRight KeyCode = 2 + + // KCHome Home key. + // This key is handled by the framework and is never delivered to applications. + KCHome KeyCode = 3 + + KCBack KeyCode = 4 // Back key + KCCall KeyCode = 5 // Call key + KCEndCall KeyCode = 6 // End Call key + KC0 KeyCode = 7 // '0' key + KC1 KeyCode = 8 // '1' key + KC2 KeyCode = 9 // '2' key + KC3 KeyCode = 10 // '3' key + KC4 KeyCode = 11 // '4' key + KC5 KeyCode = 12 // '5' key + KC6 KeyCode = 13 // '6' key + KC7 KeyCode = 14 // '7' key + KC8 KeyCode = 15 // '8' key + KC9 KeyCode = 16 // '9' key + KCStar KeyCode = 17 // '*' key + KCPound KeyCode = 18 // '#' key + + // KCDPadUp KeycodeDPadUp Directional Pad Up key. + // May also be synthesized from trackball motions. + KCDPadUp KeyCode = 19 + + // KCDPadDown Directional Pad Down key. + // May also be synthesized from trackball motions. + KCDPadDown KeyCode = 20 + + // KCDPadLeft Directional Pad Left key. + // May also be synthesized from trackball motions. + KCDPadLeft KeyCode = 21 + + // KCDPadRight Directional Pad Right key. + // May also be synthesized from trackball motions. + KCDPadRight KeyCode = 22 + + // KCDPadCenter Directional Pad Center key. + // May also be synthesized from trackball motions. + KCDPadCenter KeyCode = 23 + + // KCVolumeUp Volume Up key. + // Adjusts the speaker volume up. + KCVolumeUp KeyCode = 24 + + // KCVolumeDown Volume Down key. + // Adjusts the speaker volume down. + KCVolumeDown KeyCode = 25 + + // KCPower Power key. + KCPower KeyCode = 26 + + // KCCamera Camera key. + // Used to launch a camera application or take pictures. + KCCamera KeyCode = 27 + + KCClear KeyCode = 28 // Clear key + KCa KeyCode = 29 // 'a' key + KCb KeyCode = 30 // 'b' key + KCc KeyCode = 31 // 'c' key + KCd KeyCode = 32 // 'd' key + KCe KeyCode = 33 // 'e' key + KCf KeyCode = 34 // 'f' key + KCg KeyCode = 35 // 'g' key + KCh KeyCode = 36 // 'h' key + KCi KeyCode = 37 // 'i' key + KCj KeyCode = 38 // 'j' key + KCk KeyCode = 39 // 'k' key + KCl KeyCode = 40 // 'l' key + KCm KeyCode = 41 // 'm' key + KCn KeyCode = 42 // 'n' key + KCo KeyCode = 43 // 'o' key + KCp KeyCode = 44 // 'p' key + KCq KeyCode = 45 // 'q' key + KCr KeyCode = 46 // 'r' key + KCs KeyCode = 47 // 's' key + KCt KeyCode = 48 // 't' key + KCu KeyCode = 49 // 'u' key + KCv KeyCode = 50 // 'v' key + KCw KeyCode = 51 // 'w' key + KCx KeyCode = 52 // 'x' key + KCy KeyCode = 53 // 'y' key + KCz KeyCode = 54 // 'z' key + KCComma KeyCode = 55 // ',' key + KCPeriod KeyCode = 56 // '.' key + KCAltLeft KeyCode = 57 // Left Alt modifier key + KCAltRight KeyCode = 58 // Right Alt modifier key + KCShiftLeft KeyCode = 59 // Left Shift modifier key + KCShiftRight KeyCode = 60 // Right Shift modifier key + KCTab KeyCode = 61 // Tab key + KCSpace KeyCode = 62 // Space key + + // KCSym Symbol modifier key. + // Used to enter alternate symbols. + KCSym KeyCode = 63 + + // KCExplorer Explorer special function key. + // Used to launch a browser application. + KCExplorer KeyCode = 64 + + // KCEnvelope Envelope special function key. + // Used to launch a mail application. + KCEnvelope KeyCode = 65 + + // KCEnter Enter key. + KCEnter KeyCode = 66 + + // KCDel Backspace key. + // Deletes characters before the insertion point, unlike `KCForwardDel`. + KCDel KeyCode = 67 + + KCGrave KeyCode = 68 // '`' (backtick) key + KCMinus KeyCode = 69 // '-' + KCEquals KeyCode = 70 // '=' key + KCLeftBracket KeyCode = 71 // '[' key + KCRightBracket KeyCode = 72 // ']' key + KCBackslash KeyCode = 73 // '\' key + KCSemicolon KeyCode = 74 // '' key + KCApostrophe KeyCode = 75 // ''' (apostrophe) key + KCSlash KeyCode = 76 // '/' key + KCAt KeyCode = 77 // '@' key + + // KCNum Number modifier key. + // Used to enter numeric symbols. + // This key is not Num Lock; it is more like `KCAltLeft` and is + // interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. + KCNum KeyCode = 78 + + // KCHeadsetHook Headset Hook key. + // Used to hang up calls and stop media. + KCHeadsetHook KeyCode = 79 + + // KCFocus Camera Focus key. + // Used to focus the camera. + // *Camera* focus + KCFocus KeyCode = 80 + + KCPlus KeyCode = 81 // '+' key. + KCMenu KeyCode = 82 // Menu key. + KCNotification KeyCode = 83 // Notification key. + KCSearch KeyCode = 84 // Search key. + KCMediaPlayPause KeyCode = 85 // Play/Pause media key. + KCMediaStop KeyCode = 86 // Stop media key. + KCMediaNext KeyCode = 87 // Play Next media key. + KCMediaPrevious KeyCode = 88 // Play Previous media key. + KCMediaRewind KeyCode = 89 // Rewind media key. + KCMediaFastForward KeyCode = 90 // Fast Forward media key. + + // KCMute Mute key. + // Mutes the microphone, unlike `KCVolumeMute` + KCMute KeyCode = 91 + + // KCPageUp Page Up key. + KCPageUp KeyCode = 92 + + // KCPageDown Page Down key. + KCPageDown KeyCode = 93 + + // KCPictSymbols Picture Symbols modifier key. + // Used to switch symbol sets (Emoji, Kao-moji). + // switch symbol-sets (Emoji,Kao-moji) + KCPictSymbols KeyCode = 94 + + // KCSwitchCharset Switch Charset modifier key. + // Used to switch character sets (Kanji, Katakana). + // switch char-sets (Kanji,Katakana) + KCSwitchCharset KeyCode = 95 + + // KCButtonA A Button key. + // On a game controller, the A button should be either the button labeled A + // or the first button on the bottom row of controller buttons. + KCButtonA KeyCode = 96 + + // KCButtonB B Button key. + // On a game controller, the B button should be either the button labeled B + // or the second button on the bottom row of controller buttons. + KCButtonB KeyCode = 97 + + // KCButtonC C Button key. + // On a game controller, the C button should be either the button labeled C + // or the third button on the bottom row of controller buttons. + KCButtonC KeyCode = 98 + + // KCButtonX X Button key. + // On a game controller, the X button should be either the button labeled X + // or the first button on the upper row of controller buttons. + KCButtonX KeyCode = 99 + + // KCButtonY Y Button key. + // On a game controller, the Y button should be either the button labeled Y + // or the second button on the upper row of controller buttons. + KCButtonY KeyCode = 100 + + // KCButtonZ Z Button key. + // On a game controller, the Z button should be either the button labeled Z + // or the third button on the upper row of controller buttons. + KCButtonZ KeyCode = 101 + + // KCButtonL1 L1 Button key. + // On a game controller, the L1 button should be either the button labeled L1 (or L) + // or the top left trigger button. + KCButtonL1 KeyCode = 102 + + // KCButtonR1 R1 Button key. + // On a game controller, the R1 button should be either the button labeled R1 (or R) + // or the top right trigger button. + KCButtonR1 KeyCode = 103 + + // KCButtonL2 L2 Button key. + // On a game controller, the L2 button should be either the button labeled L2 + // or the bottom left trigger button. + KCButtonL2 KeyCode = 104 + + // KCButtonR2 R2 Button key. + // On a game controller, the R2 button should be either the button labeled R2 + // or the bottom right trigger button. + KCButtonR2 KeyCode = 105 + + // KCButtonTHUMBL Left Thumb Button key. + // On a game controller, the left thumb button indicates that the left (or only) + // joystick is pressed. + KCButtonTHUMBL KeyCode = 106 + + // KCButtonTHUMBR Right Thumb Button key. + // On a game controller, the right thumb button indicates that the right + // joystick is pressed. + KCButtonTHUMBR KeyCode = 107 + + // KCButtonStart Start Button key. + // On a game controller, the button labeled Start. + KCButtonStart KeyCode = 108 + + // KCButtonSelect Select Button key. + // On a game controller, the button labeled Select. + KCButtonSelect KeyCode = 109 + + // KCButtonMode Mode Button key. + // On a game controller, the button labeled Mode. + KCButtonMode KeyCode = 110 + + // KCEscape Escape key. + KCEscape KeyCode = 111 + + // KCForwardDel Forward Delete key. + // Deletes characters ahead of the insertion point, unlike `KCDel`. + KCForwardDel KeyCode = 112 + + KCCtrlLeft KeyCode = 113 // Left Control modifier key + KCCtrlRight KeyCode = 114 // Right Control modifier key + KCCapsLock KeyCode = 115 // Caps Lock key + KCScrollLock KeyCode = 116 // Scroll Lock key + KCMetaLeft KeyCode = 117 // Left Meta modifier key + KCMetaRight KeyCode = 118 // Right Meta modifier key + KCFunction KeyCode = 119 // Function modifier key + KCSysRq KeyCode = 120 // System Request / Print Screen key + KCBreak KeyCode = 121 // Break / Pause key + + // KCMoveHome Home Movement key. + // Used for scrolling or moving the cursor around to the start of a line + // or to the top of a list. + KCMoveHome KeyCode = 122 + + // KCMoveEnd End Movement key. + // Used for scrolling or moving the cursor around to the end of a line + // or to the bottom of a list. + KCMoveEnd KeyCode = 123 + + // KCInsert Insert key. + // Toggles insert / overwrite edit mode. + KCInsert KeyCode = 124 + + // KCForward Forward key. + // Navigates forward in the history stack. Complement of `KCBack`. + KCForward KeyCode = 125 + + // KCMediaPlay Play media key. + KCMediaPlay KeyCode = 126 + + // KCMediaPause Pause media key. + KCMediaPause KeyCode = 127 + + // KCMediaClose Close media key. + // May be used to close a CD tray, for example. + KCMediaClose KeyCode = 128 + + // KCMediaEject Eject media key. + // May be used to eject a CD tray, for example. + KCMediaEject KeyCode = 129 + + // KCMediaRecord Record media key. + KCMediaRecord KeyCode = 130 + + KCF1 KeyCode = 131 // F1 key. + KCF2 KeyCode = 132 // F2 key. + KCF3 KeyCode = 133 // F3 key. + KCF4 KeyCode = 134 // F4 key. + KCF5 KeyCode = 135 // F5 key. + KCF6 KeyCode = 136 // F6 key. + KCF7 KeyCode = 137 // F7 key. + KCF8 KeyCode = 138 // F8 key. + KCF9 KeyCode = 139 // F9 key. + KCF10 KeyCode = 140 // F10 key. + KCF11 KeyCode = 141 // F11 key. + KCF12 KeyCode = 142 // F12 key. + + // KCNumLock Num Lock key. + // This is the Num Lock key; it is different from `KCNum`. + // This key alters the behavior of other keys on the numeric keypad. + KCNumLock KeyCode = 143 + + KCNumpad0 KeyCode = 144 // Numeric keypad '0' key + KCNumpad1 KeyCode = 145 // Numeric keypad '1' key + KCNumpad2 KeyCode = 146 // Numeric keypad '2' key + KCNumpad3 KeyCode = 147 // Numeric keypad '3' key + KCNumpad4 KeyCode = 148 // Numeric keypad '4' key + KCNumpad5 KeyCode = 149 // Numeric keypad '5' key + KCNumpad6 KeyCode = 150 // Numeric keypad '6' key + KCNumpad7 KeyCode = 151 // Numeric keypad '7' key + KCNumpad8 KeyCode = 152 // Numeric keypad '8' key + KCNumpad9 KeyCode = 153 // Numeric keypad '9' key + KCNumpadDivide KeyCode = 154 // Numeric keypad '/' key (for division) + KCNumpadMultiply KeyCode = 155 // Numeric keypad '*' key (for multiplication) + KCNumpadSubtract KeyCode = 156 // Numeric keypad '-' key (for subtraction) + KCNumpadAdd KeyCode = 157 // Numeric keypad '+' key (for addition) + KCNumpadDot KeyCode = 158 // Numeric keypad '.' key (for decimals or digit grouping) + KCNumpadComma KeyCode = 159 // Numeric keypad ',' key (for decimals or digit grouping) + KCNumpadEnter KeyCode = 160 // Numeric keypad Enter key + KCNumpadEquals KeyCode = 161 // Numeric keypad 'KeyCode =' key + KCNumpadLeftParen KeyCode = 162 // Numeric keypad '(' key + KCNumpadRightParen KeyCode = 163 // Numeric keypad ')' key + + // KCVolumeMute Volume Mute key. + // Mutes the speaker, unlike `KCMute`. + // This key should normally be implemented as a toggle such that the first press + // mutes the speaker and the second press restores the original volume. + KCVolumeMute KeyCode = 164 + + // KCInfo Info key. + // Common on TV remotes to show additional information related to what is + // currently being viewed. + KCInfo KeyCode = 165 + + // KCChannelUp Channel up key. + // On TV remotes, increments the television channel. + KCChannelUp KeyCode = 166 + + // KCChannelDown Channel down key. + // On TV remotes, decrements the television channel. + KCChannelDown KeyCode = 167 + + // KCZoomIn Zoom in key. + KCZoomIn KeyCode = 168 + + // KCZoomOut Zoom out key. + KCZoomOut KeyCode = 169 + + // KCTv TV key. + // On TV remotes, switches to viewing live TV. + KCTv KeyCode = 170 + + // KCWindow Window key. + // On TV remotes, toggles picture-in-picture mode or other windowing functions. + // On Android Wear devices, triggers a display offset. + KCWindow KeyCode = 171 + + // KCGuide Guide key. + // On TV remotes, shows a programming guide. + KCGuide KeyCode = 172 + + // KCDvr DVR key. + // On some TV remotes, switches to a DVR mode for recorded shows. + KCDvr KeyCode = 173 + + // KCBookmark Bookmark key. + // On some TV remotes, bookmarks content or web pages. + KCBookmark KeyCode = 174 + + // KCCaptions Toggle captions key. + // Switches the mode for closed-captioning text, for example during television shows. + KCCaptions KeyCode = 175 + + // KCSettings Settings key. + // Starts the system settings activity. + KCSettings KeyCode = 176 + + // KCTvPower TV power key. + // On TV remotes, toggles the power on a television screen. + KCTvPower KeyCode = 177 + + // KCTvInput TV input key. + // On TV remotes, switches the input on a television screen. + KCTvInput KeyCode = 178 + + // KCStbPower Set-top-box power key. + // On TV remotes, toggles the power on an external Set-top-box. + KCStbPower KeyCode = 179 + + // KCStbInput Set-top-box input key. + // On TV remotes, switches the input mode on an external Set-top-box. + KCStbInput KeyCode = 180 + + // KCAvrPower A/V Receiver power key. + // On TV remotes, toggles the power on an external A/V Receiver. + KCAvrPower KeyCode = 181 + + // KCAvrInput A/V Receiver input key. + // On TV remotes, switches the input mode on an external A/V Receiver. + KCAvrInput KeyCode = 182 + + // KCProgRed Red "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgRed KeyCode = 183 + + // KCProgGreen Green "programmable" key. + // On TV remotes, actsas a contextual/programmable key. + KCProgGreen KeyCode = 184 + + // KCProgYellow Yellow "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgYellow KeyCode = 185 + + // KCProgBlue Blue "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgBlue KeyCode = 186 + + // KCAppSwitch App switch key. + // Should bring up the application switcher dialog. + KCAppSwitch KeyCode = 187 + + KCButton1 KeyCode = 188 // Generic Game Pad Button #1 + KCButton2 KeyCode = 189 // Generic Game Pad Button #2 + KCButton3 KeyCode = 190 // Generic Game Pad Button #3 + KCButton4 KeyCode = 191 // Generic Game Pad Button #4 + KCButton5 KeyCode = 192 // Generic Game Pad Button #5 + KCButton6 KeyCode = 193 // Generic Game Pad Button #6 + KCButton7 KeyCode = 194 // Generic Game Pad Button #7 + KCButton8 KeyCode = 195 // Generic Game Pad Button #8 + KCButton9 KeyCode = 196 // Generic Game Pad Button #9 + KCButton10 KeyCode = 197 // Generic Game Pad Button #10 + KCButton11 KeyCode = 198 // Generic Game Pad Button #11 + KCButton12 KeyCode = 199 // Generic Game Pad Button #12 + KCButton13 KeyCode = 200 // Generic Game Pad Button #13 + KCButton14 KeyCode = 201 // Generic Game Pad Button #14 + KCButton15 KeyCode = 202 // Generic Game Pad Button #15 + KCButton16 KeyCode = 203 // Generic Game Pad Button #16 + + // KCLanguageSwitch Language Switch key. + // Toggles the current input language such as switching between English and Japanese on + // a QWERTY keyboard. On some devices, the same function may be performed by + // pressing Shift+Spacebar. + KCLanguageSwitch KeyCode = 204 + + // Manner Mode key. + // Toggles silent or vibrate mode on and off to make the device behave more politely + // in certain settings such as on a crowded train. On some devices, the key may only + // operate when long-pressed. + KCMannerMode KeyCode = 205 + + // 3D Mode key. + // Toggles the display between 2D and 3D mode. + KC3dMode KeyCode = 206 + + // Contacts special function key. + // Used to launch an address book application. + KCContacts KeyCode = 207 + + // Calendar special function key. + // Used to launch a calendar application. + KCCalendar KeyCode = 208 + + // Music special function key. + // Used to launch a music player application. + KCMusic KeyCode = 209 + + // Calculator special function key. + // Used to launch a calculator application. + KCCalculator KeyCode = 210 + + // Japanese full-width / half-width key. + KCZenkakuHankaku KeyCode = 211 + + // Japanese alphanumeric key. + KCEisu KeyCode = 212 + + // Japanese non-conversion key. + KCMuhenkan KeyCode = 213 + + // Japanese conversion key. + KCHenkan KeyCode = 214 + + // Japanese katakana / hiragana key. + KCKatakanaHiragana KeyCode = 215 + + // Japanese Yen key. + KCYen KeyCode = 216 + + // Japanese Ro key. + KCRo KeyCode = 217 + + // Japanese kana key. + KCKana KeyCode = 218 + + // Assist key. + // Launches the global assist activity. Not delivered to applications. + KCAssist KeyCode = 219 + + // Brightness Down key. + // Adjusts the screen brightness down. + KCBrightnessDown KeyCode = 220 + + // Brightness Up key. + // Adjusts the screen brightness up. + KCBrightnessUp KeyCode = 221 + + // Audio Track key. + // Switches the audio tracks. + KCMediaAudioTrack KeyCode = 222 + + // Sleep key. + // Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already asleep. + KCSleep KeyCode = 223 + + // Wakeup key. + // Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already awake. + KCWakeup KeyCode = 224 + + // Pairing key. + // Initiates peripheral pairing mode. Useful for pairing remote control + // devices or game controllers, especially if no other input mode is + // available. + KCPairing KeyCode = 225 + + // Media Top Menu key. + // Goes to the top of media menu. + KCMediaTopMenu KeyCode = 226 + + // '11' key. + KC11 KeyCode = 227 + + // '12' key. + KC12 KeyCode = 228 + + // Last Channel key. + // Goes to the last viewed channel. + KCLastChannel KeyCode = 229 + + // TV data service key. + // Displays data services like weather, sports. + KCTvDataService KeyCode = 230 + + // Voice Assist key. + // Launches the global voice assist activity. Not delivered to applications. + KCVoiceAssist KeyCode = 231 + + // Radio key. + // Toggles TV service / Radio service. + KCTvRadioService KeyCode = 232 + + // Teletext key. + // Displays Teletext service. + KCTvTeletext KeyCode = 233 + + // Number entry key. + // Initiates to enter multi-digit channel nubmber when each digit key is assigned + // for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + // User Control Code. + KCTvNumberEntry KeyCode = 234 + + // Analog Terrestrial key. + // Switches to analog terrestrial broadcast service. + KCTvTerrestrialAnalog KeyCode = 235 + + // Digital Terrestrial key. + // Switches to digital terrestrial broadcast service. + KCTvTerrestrialDigital KeyCode = 236 + + // Satellite key. + // Switches to digital satellite broadcast service. + KCTvSatellite KeyCode = 237 + + // BS key. + // Switches to BS digital satellite broadcasting service available in Japan. + KCTvSatelliteBs KeyCode = 238 + + // CS key. + // Switches to CS digital satellite broadcasting service available in Japan. + KCTvSatelliteCs KeyCode = 239 + + // BS/CS key. + // Toggles between BS and CS digital satellite services. + KCTvSatelliteService KeyCode = 240 + + // Toggle Network key. + // Toggles selecting broacast services. + KCTvNetwork KeyCode = 241 + + // Antenna/Cable key. + // Toggles broadcast input source between antenna and cable. + KCTvAntennaCable KeyCode = 242 + + // HDMI #1 key. + // Switches to HDMI input #1. + KCTvInputHdmi1 KeyCode = 243 + + // HDMI #2 key. + // Switches to HDMI input #2. + KCTvInputHdmi2 KeyCode = 244 + + // HDMI #3 key. + // Switches to HDMI input #3. + KCTvInputHdmi3 KeyCode = 245 + + // HDMI #4 key. + // Switches to HDMI input #4. + KCTvInputHdmi4 KeyCode = 246 + + // Composite #1 key. + // Switches to composite video input #1. + KCTvInputComposite1 KeyCode = 247 + + // Composite #2 key. + // Switches to composite video input #2. + KCTvInputComposite2 KeyCode = 248 + + // Component #1 key. + // Switches to component video input #1. + KCTvInputComponent1 KeyCode = 249 + + // Component #2 key. + // Switches to component video input #2. + KCTvInputComponent2 KeyCode = 250 + + // VGA #1 key. + // Switches to VGA (analog RGB) input #1. + KCTvInputVga1 KeyCode = 251 + + // Audio description key. + // Toggles audio description off / on. + KCTvAudioDescription KeyCode = 252 + + // Audio description mixing volume up key. + // Louden audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixUp KeyCode = 253 + + // Audio description mixing volume down key. + // Lessen audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixDown KeyCode = 254 + + // Zoom mode key. + // Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) + KCTvZoomMode KeyCode = 255 + + // Contents menu key. + // Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + // Code + KCTvContentsMenu KeyCode = 256 + + // Media context menu key. + // Goes to the context menu of media contents. Corresponds to Media Context-sensitive + // Menu (0x11) of CEC User Control Code. + KCTvMediaContextMenu KeyCode = 257 + + // Timer programming key. + // Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + // CEC User Control Code. + KCTvTimerProgramming KeyCode = 258 + + // Help key. + KCHelp KeyCode = 259 + + // Navigate to previous key. + // Goes backward by one item in an ordered collection of items. + KCNavigatePrevious KeyCode = 260 + + // Navigate to next key. + // Advances to the next item in an ordered collection of items. + KCNavigateNext KeyCode = 261 + + // Navigate in key. + // Activates the item that currently has focus or expands to the next level of a navigation + // hierarchy. + KCNavigateIn KeyCode = 262 + + // Navigate out key. + // Backs out one level of a navigation hierarchy or collapses the item that currently has + // focus. + KCNavigateOut KeyCode = 263 + + // Primary stem key for Wear + // Main power/reset button on watch. + KCStemPrimary KeyCode = 264 + + // Generic stem key 1 for Wear + KCStem1 KeyCode = 265 + + // Generic stem key 2 for Wear + KCStem2 KeyCode = 266 + + // Generic stem key 3 for Wear + KCStem3 KeyCode = 267 + + // Directional Pad Up-Left + KCDPadUpLeft KeyCode = 268 + + // Directional Pad Down-Left + KCDPadDownLeft KeyCode = 269 + + // Directional Pad Up-Right + KCDPadUpRight KeyCode = 270 + + // Directional Pad Down-Right + KCDPadDownRight KeyCode = 271 + + // Skip forward media key. + KCMediaSkipForward KeyCode = 272 + + // Skip backward media key. + KCMediaSkipBackward KeyCode = 273 + + // Step forward media key. + // Steps media forward, one frame at a time. + KCMediaStepForward KeyCode = 274 + + // Step backward media key. + // Steps media backward, one frame at a time. + KCMediaStepBackward KeyCode = 275 + + // put device to sleep unless a wakelock is held. + KCSoftSleep KeyCode = 276 + + // Cut key. + KCCut KeyCode = 277 + + // Copy key. + KCCopy KeyCode = 278 + + // Paste key. + KCPaste KeyCode = 279 + + // Consumed by the system for navigation up + KCSystemNavigationUp KeyCode = 280 + + // Consumed by the system for navigation down + KCSystemNavigationDown KeyCode = 281 + + // Consumed by the system for navigation left*/ + KCSystemNavigationLeft KeyCode = 282 + + // Consumed by the system for navigation right + KCSystemNavigationRight KeyCode = 283 + + // Show all apps + KCAllApps KeyCode = 284 + + // Refresh key. + KCRefresh KeyCode = 285 +) diff --git a/hrp/pkg/uixt/android_test.go b/hrp/pkg/uixt/android_test.go new file mode 100644 index 00000000..eecf7b91 --- /dev/null +++ b/hrp/pkg/uixt/android_test.go @@ -0,0 +1,1307 @@ +//go:build localtest + +package uixt + +import ( + "io/ioutil" + "testing" + "time" +) + +var uiaServerURL = "http://localhost:6790/wd/hub" + +func TestDriver_NewSession(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + firstMatchEntry := make(map[string]interface{}) + firstMatchEntry["package"] = "com.android.settings" + firstMatchEntry["activity"] = "com.android.settings/.Settings" + caps := Capabilities{ + "firstMatch": []interface{}{firstMatchEntry}, + "alwaysMatch": struct{}{}, + } + session, err := driver.NewSession(caps) + if err != nil { + t.Fatal(err) + } + if len(session.SessionId) == 0 { + t.Fatal("should not be empty") + } +} + +func TestNewDriver(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + t.Log(driver.sessionId) +} + +func TestDriver_Quit(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + if err = driver.Close(); err != nil { + t.Fatal(err) + } +} + +func TestDriver_Status(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + _, err = driver.Status() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SessionIDs(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + sessions, err := driver.SessionIDs() + if err != nil { + t.Fatal(err) + } + if len(sessions) == 0 { + t.Fatal("should have at least one") + } + t.Log(len(sessions), sessions) +} + +func TestDriver_SessionDetails(t *testing.T) { + // firstMatchEntry := make(map[string]interface{}) + // firstMatchEntry["package"] = "com.android.settings" + // firstMatchEntry["activity"] = "com.android.settings/.Settings" + // caps = Capabilities{ + // "firstMatch": []interface{}{firstMatchEntry}, + // "alwaysMatch": struct{}{}, + // } + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + scrollData, err := driver.SessionDetails() + if err != nil { + t.Fatal(err) + } + + t.Log(scrollData) +} + +func TestDriver_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0o600)) +} + +func TestDriver_Orientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + + t.Log(orientation) +} + +func TestDriver_Rotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal(err) + } + + t.Logf("x = %d\ty = %d\tz = %d", rotation.X, rotation.Y, rotation.Z) +} + +func TestDriver_DeviceSize(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + deviceSize, err := driver.WindowSize() + if err != nil { + t.Fatal(err) + } + + t.Logf("width = %d\theight = %d", deviceSize.Width, deviceSize.Height) +} + +func TestDriver_Source(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + source, err := driver.Source() + if err != nil { + t.Fatal(err) + } + + t.Log(source) +} + +func TestDriver_BatteryInfo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal(err) + } + + t.Log(batteryInfo) +} + +func TestDriver_GetAppiumSettings(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + + for k := range appiumSettings { + t.Logf("key: %s\tvalue: %v", k, appiumSettings[k]) + } + // 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 { + t.Fatal(err) + } + + devInfo, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + + t.Logf("api version: %s", devInfo.APIVersion) + t.Logf("platform version: %s", devInfo.PlatformVersion) + t.Logf("bluetooth state: %s", devInfo.Bluetooth.State) +} + +func TestDriver_AlertText(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + alertText, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + + t.Log(alertText) +} + +func TestDriver_Tap(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Tap(150, 340) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapFloat(60.5, 125.5) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) +} + +func TestDriver_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Swipe(400, 1000, 400, 500) + if err != nil { + t.Fatal(err) + } + + err = driver.SwipeFloat(400, 555.5, 400, 1255.5) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Drag(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Drag(400, 260, 400, 500) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + err = driver.DragFloat(400, 501.5, 400, 261.5) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) +} + +func TestDriver_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SendKeys("abc") + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("def") + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("\\n") + // err = driver.SendKeys(`\n`, false) + if err != nil { + t.Fatal(err) + } +} + +//func TestDriver_PressBack(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressBack() +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestDriver_PressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressKeyCodeAsync(KCx) +// if err != nil { +// t.Fatal(err) +// } +// err = driver.PressKeyCodeAsync(KCx, KMCapLocked) +// if err != nil { +// t.Fatal(err) +// } +// // err = driver.PressKeyCodeAsync(KCExplorer) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// err = driver.PressKeyCode(KCExplorer, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestDriver_LongPressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.LongPressKeyCode(KCAt, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchDown(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp := func() { +// err = driver.TouchUp(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // _ = driver.TapPoint(Point{400, 500}) +// doTouchUp() +// +// err = driver.TouchDownPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp() +//} +// +//func TestDriver_TouchUp(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // err = driver.TouchUp(400, 260) +// err = driver.TouchUpPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchMove(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchDown := func(x, y int) { +// err = driver.TouchDown(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchUp := func(x, y int) { +// err = driver.TouchUp(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchDown(400, 260) +// +// err = driver.TouchMove(400, 500) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 500) +// +// doTouchDown(400, 500) +// +// err = driver.TouchMove(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 260) +//} +// +//func TestDriver_OpenNotification(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.OpenNotification() +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.Flick(50, -100) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.ScrollTo(BySelector{ClassName: "android.widget.SeekBar"}) +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestDriver_MultiPointerGesture(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) +// gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) +// gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) +// _ = gesture3 +// +// // err = driver.MultiPointerGesture(gesture1, gesture2) +// err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_PerformW3CActions(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) +// // err = driver.PerformW3CActions(actionKey) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// // var queryField map[string]string +// // queryField = make(map[string]string) +// // { +// // queryField = map[string]string{ +// // "a": "", +// // } +// // } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) +// // actionPointer := NewW3CAction(ATPointer, +// // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). +// // PointerMove(0, 0, elem.id).Pause(2). +// // PointerMove(20, 0, "pointer").Pause(2). +// // PointerUp(), +// // ) +// actionPointer := NewW3CAction(ATPointer, +// NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). +// PointerMouseOver(0, 0, elem). +// PointerMoveRelative(20, 0).PointerUp()) +// err = driver.PerformW3CActions(actionPointer) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_GetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// t.Log(text) +//} +// +//func TestDriver_SetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// content := "test123" +// err = driver.SetClipboard(ClipDataTypePlaintext, content) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// if text != content { +// t.Fatal("should be the same") +// } +//} + +func TestDriver_AlertAccept(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.AlertAccept() + // err = driver.AlertAccept("是") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AlertDismiss(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.AlertDismiss() + err = driver.AlertDismiss("否") + if err != nil { + t.Fatal(err) + } +} + +//func TestDriver_SetAppiumSettings(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err := driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// sdopd := appiumSettings["shutdownOnPowerDisconnect"] +// t.Log("shutdownOnPowerDisconnect:", sdopd) +// +// err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err = driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { +// t.Fatal("should not be equal") +// } +// t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) +//} + +func TestDriver_SetOrientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SetOrientation(OrientationLandscapeLeft) + // err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SetRotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.SetRotation(Rotation{Z: 0}) + err = driver.SetRotation(Rotation{Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +//func TestDriver_NetworkConnection(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.NetworkConnection(NetworkTypeWifi) +// if err != nil { +// t.Fatal(err) +// } +//} + +func TestDriver_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/content"}) + if err != nil { + t.Fatal(err) + } + e := ElementAttribute{}.WithLabel("class") + t.Log(elem.GetAttribute(e)) +} + +func TestDriver_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // elements, err := driver.FindElements(BySelector{ResourceIdID: "com.android.settings:id/title"}) + elements, err := driver.FindElements(BySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) + if err != nil { + t.Fatal(err) + } + + elem, err := element.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + x := rect.X + int(float64(rect.Width)*2) + y := rect.Y + rect.Height/2 + err = driver.Tap(x, y) + if err != nil { + t.Fatal(err) + } + + by := BySelector{UiAutomator: "new UiSelector().text(\"科技\");"} + exists := func(d WebDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + + err = driver.WaitWithTimeoutAndInterval(exists, 1, 1) + if err != nil { + t.Fatal(err) + } + + // element, err = driver.FindElement(by) + // if err != nil { + // t.Fatal(err) + // } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +//func TestDriver_ActiveElement(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer func() { +// _ = driver.Dispose() +// }() +// +// element, err := driver.ActiveElement() +// if err != nil { +// t.Fatal(err) +// } +// +// if err = element.SendKeys("test"); err != nil { +// t.Fatal(err) +// } +//} + +func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { + uiSelector := NewUiSelectorHelper().Text("a").String() + if uiSelector != `new UiSelector().text("a");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Text("a").TextStartsWith("b").String() + if uiSelector != `new UiSelector().text("a").textStartsWith("b");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ClassName("android.widget.LinearLayout").Index(6).String() + if uiSelector != `new UiSelector().className("android.widget.LinearLayout").index(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Focused(false).Instance(6).String() + if uiSelector != `new UiSelector().focused(false).instance(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ChildSelector(NewUiSelectorHelper().Enabled(true)).String() + if uiSelector != `new UiSelector().childSelector(new UiSelector().enabled(true));` { + t.Fatal("[ERROR]", uiSelector) + } +} + +func Test_getFreePort(t *testing.T) { + freePort, err := getFreePort() + if err != nil { + t.Fatal(err) + } + t.Log(freePort) +} + +func TestDeviceList(t *testing.T) { + devices, err := DeviceList() + if err != nil { + t.Fatal(err) + } + for i := range devices { + t.Log(devices[i].Serial()) + } +} + +//func TestAndroidNewUSBDriver(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// ready, err := driver.Status() +// if err != nil { +// t.Fatal(err) +// } +// if !ready { +// t.Fatal("should be 'true'") +// } +//} + +//func TestDriver_ActiveAppPackageName(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// appPackageName, err := driver.ActiveAppPackageName() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(appPackageName) +//} + +func TestDriver_AppLaunch(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + + // err = driver.AppLaunch("tv.danmaku.bili", BySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) + err = driver.AppLaunch("com.android.settings", AppLaunchOption{}.WithAndroidBySelector(AndroidBySelector{ResourceIdID: "android:id/list"})) + if err != nil { + t.Fatal(err) + } + + // screenshot, err := driver.Screenshot() + // if err != nil { + // t.Fatal(err) + // } + // t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0600)) +} + +func TestDriver_AppTerminate(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + _, err = driver.AppTerminate("tv.danmaku.bili") + if err != nil { + t.Fatal(err) + } +} + +//func TestNewWiFiDriver(t *testing.T) { +// device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) +// driver, err := device.NewHTTPDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// +// // SetDebug(false, true) +// _, err = driver.ActiveAppActivity() +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestDriver_AppInstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestDriver_AppUninstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppUninstall("cn.xuexi.android") +// if err != nil { +// t.Fatal(err) +// } +//} + +func TestBySelector_getMethodAndSelector(t *testing.T) { + testVal := "test id" + bySelector := BySelector{ResourceIdID: testVal} + method, selector := bySelector.getMethodAndSelector() + if method != "id" || selector != testVal { + t.Fatal(method, "=", selector) + } + + bySelector = BySelector{ContentDescription: testVal} + method, selector = bySelector.getMethodAndSelector() + if method != "accessibility id" || selector != testVal { + t.Fatal(method, "=", selector) + } +} + +func TestElement_Text(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + text, err := elem.Text() + if err != nil { + t.Fatal(err) + } + + t.Log(text) +} + +func TestElement_GetAttribute(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + e := ElementAttribute{}.WithName("class") + attribute, err := elem.GetAttribute(e) + if err != nil { + t.Fatal(err) + } + + t.Log(attribute) +} + +//func TestElement_ContentDescription(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// +// name, err := elem.ContentDescription() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(name) +//} + +func TestElement_Size(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + + size, err := elem.Size() + if err != nil { + t.Fatal(err) + } + + t.Log(size) +} + +func TestElement_Rect(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) +} + +func TestElement_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + screenshot, err := elem.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/e1.png", screenshot.Bytes(), 0o600)) +} + +func TestElement_Location(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + location, err := elem.Location() + if err != nil { + t.Fatal(err) + } + + t.Log(location) +} + +func TestElement_Click(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/title"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Click() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_Clear(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Clear() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + // return + + // err = elem.SendKeys("abc") + err = elem.SendKeys("456") + if err != nil { + t.Fatal(err) + } +} + +func TestElement_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elements, err := parentElem.FindElements(BySelector{ResourceIdID: "com.android.settings:id/category"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestElement_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elem, err := parentElem.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + t.Log(elem.Text()) +} + +func TestElement_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) + + var startX, startY, endX, endY int + startX = rect.X + rect.Width/20 + startY = rect.Y + rect.Height/2 + endX = startX + endY = startY - startY/2 + err = elem.Swipe(startX, startY, endX, endY) + if err != nil { + t.Fatal(err) + } +} + +//func TestElement_Drag(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elements, err := driver.FindElements(BySelector{ClassName: "android.widget.TextView"}) +// if err != nil { +// t.Fatal(err) +// } +// +// for i, elem := range elements { +// text, _ := elem.Text() +// t.Log(i, text) +// } +// +// rect, err := elements[0].Rect() +// if err != nil { +// t.Fatal(err) +// } +// +// // err = elements[0].Drag(300, 450, 256) +// err = elements[0].Drag(300, 450, 256) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elements[0].DragTo(elements[1], 256) +// if err != nil { +// t.Fatal(err) +// } +// +// endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} +// err = elements[0].DragPointF(endPoint, 256) +// if err != nil { +// t.Fatal() +// } +//} + +//func TestElement_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elem.Flick(36, 20, 100) +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestElement_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// // parentElem, err := driver.FindElement(BySelector{ClassName: "android.widget.ScrollView"}) +// // parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) +// parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/dashboard"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollTo(BySelector{ContentDescription: "电池"}) +// if err != nil { +// t.Fatal(err) +// } +//} + +//func TestElement_ScrollToElement(t *testing.T) { +// // android.widget.HorizontalScrollView +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// parentElem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollToElement(element) +// if err != nil { +// t.Fatal(err) +// } +//} diff --git a/hrp/pkg/uixt/client.go b/hrp/pkg/uixt/client.go new file mode 100644 index 00000000..5ebd9309 --- /dev/null +++ b/hrp/pkg/uixt/client.go @@ -0,0 +1,104 @@ +package uixt + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +type Driver struct { + urlPrefix *url.URL + sessionId string + client *http.Client +} + +func (wd *Driver) concatURL(u *url.URL, elem ...string) string { + var tmp *url.URL + if u == nil { + u = wd.urlPrefix + } + tmp, _ = url.Parse(u.String()) + tmp.Path = path.Join(append([]string{u.Path}, elem...)...) + return tmp.String() +} + +func (wd *Driver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return wd.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) +} + +func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { + log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request driver agent") + + var req *http.Request + if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + req.Header.Set("Accept", "application/json") + + start := time.Now() + var resp *http.Response + if resp, err = wd.client.Do(req); err != nil { + return nil, err + } + defer func() { + // https://github.com/etcd-io/etcd/blob/v3.3.25/pkg/httputil/httputil.go#L16-L22 + _, _ = io.Copy(ioutil.Discard, resp.Body) + _ = resp.Body.Close() + }() + + rawResp, err = ioutil.ReadAll(resp.Body) + logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String()) + if !strings.HasSuffix(rawURL, "screenshot") { + // avoid printing screenshot data + logger.Str("response", string(rawResp)) + } + logger.Msg("get driver agent response") + if err != nil { + return nil, err + } + + if err = rawResp.checkErr(); err != nil { + if resp.StatusCode == http.StatusOK { + return rawResp, nil + } + return nil, err + } + + return +} + +func convertToHTTPClient(conn net.Conn) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return conn, nil + }, + }, + Timeout: 0, + } +} diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go new file mode 100644 index 00000000..1ff036ce --- /dev/null +++ b/hrp/pkg/uixt/demo/main_test.go @@ -0,0 +1,46 @@ +//go:build localtest + +package demo + +import ( + "testing" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func TestIOSDemo(t *testing.T) { + device, err := uixt.NewIOSDevice( + uixt.WithWDAPort(8700), uixt.WithWDAMjpegPort(8800), + uixt.WithResetHomeOnStartup(false), // not reset home on startup + ) + if err != nil { + t.Fatal(err) + } + + capabilities := uixt.NewCapabilities() + capabilities.WithDefaultAlertAction(uixt.AlertActionAccept) // or uixt.AlertActionDismiss + driverExt, err := device.NewDriver(capabilities) + if err != nil { + t.Fatal(err) + } + + // release session + defer func() { + driverExt.Driver.DeleteSession() + }() + + // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 + for { + points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}) + if err != nil { + time.Sleep(1 * time.Second) + continue + } + + err = driverExt.TapAbsXY(points[1].X, points[1].Y) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/hrp/pkg/uixt/drag.go b/hrp/pkg/uixt/drag.go new file mode 100644 index 00000000..31d33b1d --- /dev/null +++ b/hrp/pkg/uixt/drag.go @@ -0,0 +1,30 @@ +package uixt + +func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) { + return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...) +} + +func (dExt *DriverExt) DragFloat(pathname string, toX, toY float64, pressForDuration ...float64) (err error) { + return dExt.DragOffsetFloat(pathname, toX, toY, 0.5, 0.5, pressForDuration...) +} + +func (dExt *DriverExt) DragOffset(pathname string, toX, toY int, xOffset, yOffset float64, pressForDuration ...float64) (err error) { + return dExt.DragOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset, pressForDuration...) +} + +func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64, pressForDuration ...float64) (err error) { + if len(pressForDuration) == 0 { + pressForDuration = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + return dExt.Driver.DragFloat(fromX, fromY, toX, toY, + WithDataPressDuration(pressForDuration[0])) +} diff --git a/hrp/pkg/uixt/drag_test.go b/hrp/pkg/uixt/drag_test.go new file mode 100644 index 00000000..59a8fbfb --- /dev/null +++ b/hrp/pkg/uixt/drag_test.go @@ -0,0 +1,20 @@ +//go:build localtest + +package uixt + +import ( + "testing" +) + +func TestDriverExt_Drag(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" + + // err = driverExt.Drag(pathSearch, 300, 500, 2) + // checkErr(t, err) + + err = driverExt.DragOffset(pathSearch, 300, 500, 2.1, 0.5, 2) + checkErr(t, err) +} diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go new file mode 100644 index 00000000..f7b353bf --- /dev/null +++ b/hrp/pkg/uixt/ext.go @@ -0,0 +1,683 @@ +package uixt + +import ( + "bytes" + "encoding/json" + "fmt" + "image" + "image/jpeg" + "image/png" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "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 首屏加载完成,可以传入 app 的启动参数、环境变量 + AppLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app,不堵塞等待,不可传入启动参数 + AppTerminate MobileMethod = "app_terminate" + AppStop MobileMethod = "app_stop" + CtlScreenShot MobileMethod = "screenshot" + CtlSleep MobileMethod = "sleep" + 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 + SelectorName string = "ui_name" + SelectorLabel string = "ui_label" + SelectorOCR string = "ui_ocr" + SelectorImage string = "ui_image" + 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" + + // 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 + 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 + 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 + } +} + +// 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 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 + +type CVArgs struct { + matchMode TemplateMatchMode + threshold float64 +} + +type CVOption func(*CVArgs) + +func WithTemplateMatchMode(mode TemplateMatchMode) CVOption { + return func(args *CVArgs) { + args.matchMode = mode + } +} + +func WithThreshold(threshold float64) CVOption { + return func(args *CVArgs) { + args.threshold = threshold + } +} + +type DriverExt struct { + UUID string // ios udid or android serial + Driver WebDriver + windowSize Size + frame *bytes.Buffer + doneMjpegStream chan bool + scale float64 + StartTime time.Time // used to associate screenshots name + ScreenShots []string // save screenshots path + perfStop chan struct{} // stop performance monitor + perfData []string // save perf data + + CVArgs +} + +func extend(driver WebDriver) (dExt *DriverExt, err error) { + dExt = &DriverExt{Driver: driver} + 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 + } + + return dExt, nil +} + +func (dExt *DriverExt) GetPerfData() []string { + if dExt.perfStop == nil { + return nil + } + close(dExt.perfStop) + return dExt.perfData +} + +func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { + // wait for action done + time.Sleep(500 * time.Millisecond) + + // iOS 优先使用 MJPEG 流进行截图,性能最优 + // 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口 + if dExt.frame != nil { + return dExt.frame, nil + } + if raw, err = dExt.Driver.Screenshot(); err != nil { + log.Error().Err(err).Msg("takeScreenShot failed") + return nil, err + } + return raw, nil +} + +// saveScreenShot saves image file to $CWD/screenshots/ folder +func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { + img, format, err := image.Decode(raw) + if err != nil { + return "", errors.Wrap(err, "decode screenshot image failed") + } + + dir, _ := os.Getwd() + screenshotsDir := filepath.Join(dir, "screenshots") + if err = os.MkdirAll(screenshotsDir, os.ModePerm); err != nil { + return "", errors.Wrap(err, "create screenshots directory failed") + } + screenshotPath := filepath.Join(screenshotsDir, + fmt.Sprintf("%s.%s", fileName, format)) + + file, err := os.Create(screenshotPath) + if err != nil { + return "", errors.Wrap(err, "create screenshot image file failed") + } + defer func() { + _ = file.Close() + }() + + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + default: + return "", fmt.Errorf("unsupported image format: %s", format) + } + if err != nil { + return "", errors.Wrap(err, "encode screenshot image failed") + } + + return screenshotPath, nil +} + +// ScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder +func (dExt *DriverExt) ScreenShot(fileName string) (string, error) { + raw, err := dExt.takeScreenShot() + if err != nil { + return "", errors.Wrap(err, "screenshot failed") + } + + path, err := dExt.saveScreenShot(raw, fileName) + if err != nil { + return "", errors.Wrap(err, "save screenshot failed") + } + return path, nil +} + +// isPathExists returns true if path exists, whether path is file or dir +func isPathExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + +func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { + var selector BySelector + if strings.HasPrefix(param, "/") { + // xpath + selector = BySelector{ + XPath: param, + } + } else if strings.HasPrefix(param, "com.") { + // name + selector = BySelector{ + ResourceIdID: param, + } + } else { + // name + selector = BySelector{ + LinkText: NewElementAttribute().WithName(param), + } + } + + return dExt.Driver.FindElement(selector) +} + +func (dExt *DriverExt) FindUIRectInUIKit(search string, options ...DataOption) (x, y, width, height float64, err error) { + // click on text, using OCR + if !isPathExists(search) { + return dExt.FindTextByOCR(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) PerformTouchActions(touchActions *TouchActions) error { + return dExt.Driver.PerformAppiumTouchActions(touchActions) +} + +func (dExt *DriverExt) PerformActions(actions *W3CActions) error { + return dExt.Driver.PerformW3CActions(actions) +} + +func (dExt *DriverExt) IsNameExist(name string) bool { + selector := BySelector{ + LinkText: NewElementAttribute().WithName(name), + } + _, err := dExt.Driver.FindElement(selector) + return err == nil +} + +func (dExt *DriverExt) IsLabelExist(label string) bool { + selector := BySelector{ + LinkText: NewElementAttribute().WithLabel(label), + } + _, err := dExt.Driver.FindElement(selector) + return err == nil +} + +func (dExt *DriverExt) IsOCRExist(text string) bool { + _, _, _, _, err := dExt.FindTextByOCR(text) + return err == nil +} + +func (dExt *DriverExt) IsImageExist(text string) bool { + _, _, _, _, err := dExt.FindImageRectInUIKit(text) + return err == nil +} + +var errActionNotImplemented = errors.New("UI action not implemented") + +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 AppLaunchUnattached: + if bundleId, ok := action.Params.(string); ok { + return dExt.Driver.AppLaunchUnattached(bundleId) + } + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + AppLaunchUnattached, 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} + } + + identifierOption := WithDataIdentifier(action.Identifier) + 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) + } + + 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} + } + + 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, WithDataIdentifier(action.Identifier)) + } + + // 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) + return dExt.TapAbsXY(x, y, WithDataIdentifier(action.Identifier)) + } + 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} + } + + indexOption := WithDataIndex(action.Index) + 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) + } + 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) + 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) + } + if direction, ok := action.Params.(string); ok { + return dExt.SwipeTo(direction, identifierOption) + } + 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 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 CtlScreenShot: + // take snapshot + log.Info().Msg("take snapshot for current screen") + screenshotPath, err := dExt.ScreenShot(fmt.Sprintf("%d_screenshot_%d", + dExt.StartTime.Unix(), time.Now().Unix())) + if err != nil { + return errors.Wrap(err, "take screenshot failed") + } + log.Info().Str("path", screenshotPath).Msg("take screenshot") + dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath) + 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 exists bool + if assert == AssertionExists { + exists = true + } else { + exists = false + } + var result bool + switch check { + case SelectorName: + result = (dExt.IsNameExist(expected) == exists) + case SelectorLabel: + result = (dExt.IsLabelExist(expected) == exists) + case SelectorOCR: + result = (dExt.IsOCRExist(expected) == exists) + case SelectorImage: + result = (dExt.IsImageExist(expected) == exists) + } + + if !result { + if message == nil { + message = []string{""} + } + log.Error(). + Str("assert", assert). + Str("expect", expected). + Str("msg", message[0]). + Msg("validate UI failed") + return false + } + + log.Info(). + Str("assert", assert). + Str("expect", expected). + Msg("validate UI success") + return true +} + +func checkErr(t *testing.T, err error, msg ...string) { + if err != nil { + if len(msg) == 0 { + t.Fatal(err) + } else { + t.Fatal(msg, err) + } + } +} diff --git a/hrp/pkg/uixt/gesture.go b/hrp/pkg/uixt/gesture.go new file mode 100644 index 00000000..7642425c --- /dev/null +++ b/hrp/pkg/uixt/gesture.go @@ -0,0 +1,44 @@ +//go:build opencv + +package uixt + +import ( + "image" + "sort" +) + +func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) { + var rects []image.Rectangle + if rects, err = dExt.FindAllImageRect(pathname); err != nil { + return err + } + + sort.Slice(rects, func(i, j int) bool { + if rects[i].Min.Y < rects[j].Min.Y { + return true + } else if rects[i].Min.Y == rects[j].Min.Y { + if rects[i].Min.X < rects[j].Min.X { + return true + } + } + return false + }) + + touchActions := NewTouchActions(len(password)*2 + 1) + for i := range password { + x, y, width, height := dExt.MappingToRectInUIKit(rects[password[i]]) + x = x + width*0.5 + y = y + height*0.5 + + if i == 0 { + touchActions.Press(NewTouchActionPress().WithXYFloat(x, y)). + Wait(0.2) + } else { + touchActions.MoveTo(NewTouchActionMoveTo().WithXYFloat(x, y)). + Wait(0.2) + } + } + touchActions.Release() + + return dExt.PerformTouchActions(touchActions) +} diff --git a/hrp/pkg/uixt/gesture_test.go b/hrp/pkg/uixt/gesture_test.go new file mode 100644 index 00000000..c3b8dcad --- /dev/null +++ b/hrp/pkg/uixt/gesture_test.go @@ -0,0 +1,25 @@ +//go:build opencv + +package uixt + +import ( + "strconv" + "strings" + "testing" +) + +func TestDriverExt_GesturePassword(t *testing.T) { + split := strings.Split("6304258", "") + password := make([]int, len(split)) + for i := range split { + password[i], _ = strconv.Atoi(split[i]) + } + + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" + + err = driverExt.GesturePassword(pathSearch, password...) + checkErr(t, err) +} diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go new file mode 100644 index 00000000..547197a4 --- /dev/null +++ b/hrp/pkg/uixt/interface.go @@ -0,0 +1,1180 @@ +package uixt + +import ( + "bytes" + "fmt" + "math" + "reflect" + "strconv" + "strings" + "time" +) + +var ( + DefaultWaitTimeout = 60 * time.Second + DefaultWaitInterval = 400 * time.Millisecond +) + +type AlertAction string + +const ( + AlertActionAccept AlertAction = "accept" + AlertActionDismiss AlertAction = "dismiss" +) + +type Capabilities map[string]interface{} + +func NewCapabilities() Capabilities { + return make(Capabilities) +} + +func (caps Capabilities) WithAppLaunchOption(launchOpt AppLaunchOption) Capabilities { + for k, v := range launchOpt { + caps[k] = v + } + return caps +} + +// WithDefaultAlertAction +func (caps Capabilities) WithDefaultAlertAction(alertAction AlertAction) Capabilities { + caps["defaultAlertAction"] = alertAction + return caps +} + +// WithMaxTypingFrequency +// Defaults to `60`. +func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities { + if n <= 0 { + n = 60 + } + caps["maxTypingFrequency"] = n + return caps +} + +// WithWaitForIdleTimeout +// Defaults to `10` +func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities { + caps["waitForIdleTimeout"] = second + return caps +} + +// WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility +// Defaults to `false` +func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities { + caps["shouldUseTestManagerForVisibilityDetection"] = b + return caps +} + +// WithShouldUseCompactResponses If set to YES will use compact (standards-compliant) & faster responses +// Defaults to `true` +func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities { + caps["shouldUseCompactResponses"] = b + return caps +} + +// WithElementResponseAttributes If shouldUseCompactResponses == NO, +// is the comma-separated list of fields to return with each element. +// Defaults to `type,label`. +func (caps Capabilities) WithElementResponseAttributes(s string) Capabilities { + caps["elementResponseAttributes"] = s + return caps +} + +// WithShouldUseSingletonTestManager +// Defaults to `true` +func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities { + caps["shouldUseSingletonTestManager"] = b + return caps +} + +// WithDisableAutomaticScreenshots +// Defaults to `true` +func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities { + caps["disableAutomaticScreenshots"] = b + return caps +} + +// WithShouldTerminateApp +// Defaults to `true` +func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities { + caps["shouldTerminateApp"] = b + return caps +} + +// WithEventloopIdleDelaySec +// Delays the invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]' by the timer interval passed. +// which is skipped on setting it to zero. +func (caps Capabilities) WithEventloopIdleDelaySec(second float64) Capabilities { + caps["eventloopIdleDelaySec"] = second + return caps +} + +type SessionInfo struct { + SessionId string `json:"sessionId"` + Capabilities struct { + Device string `json:"device"` + BrowserName string `json:"browserName"` + SdkVersion string `json:"sdkVersion"` + CFBundleIdentifier string `json:"CFBundleIdentifier"` + } `json:"capabilities"` +} + +type DeviceStatus struct { + Message string `json:"message"` + State string `json:"state"` + OS struct { + TestmanagerdVersion int `json:"testmanagerdVersion"` + Name string `json:"name"` + SdkVersion string `json:"sdkVersion"` + Version string `json:"version"` + } `json:"os"` + IOS struct { + IP string `json:"ip"` + SimulatorVersion string `json:"simulatorVersion"` + } `json:"ios"` + Ready bool `json:"ready"` + Build struct { + Time string `json:"time"` + ProductBundleIdentifier string `json:"productBundleIdentifier"` + } `json:"build"` +} + +type DeviceInfo struct { + TimeZone string `json:"timeZone"` + CurrentLocale string `json:"currentLocale"` + Model string `json:"model"` + UUID string `json:"uuid"` + UserInterfaceIdiom int `json:"userInterfaceIdiom"` + UserInterfaceStyle string `json:"userInterfaceStyle"` + Name string `json:"name"` + IsSimulator bool `json:"isSimulator"` + ThermalState int `json:"thermalState"` + // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user + // first sets up the device and should remain constant for the lifetime of the user's device. The value + // may change if a factory reset is performed on the device. + AndroidID string `json:"androidId"` + // Build.MANUFACTURER value + Manufacturer string `json:"manufacturer"` + // Build.BRAND value + Brand string `json:"brand"` + // Current running OS's API VERSION + APIVersion string `json:"apiVersion"` + // The current version string, for example "1.0" or "3.4b5" + PlatformVersion string `json:"platformVersion"` + // the name of the current celluar network carrier + CarrierName string `json:"carrierName"` + // the real size of the default display + RealDisplaySize string `json:"realDisplaySize"` + // The logical density of the display in Density Independent Pixel units. + DisplayDensity int `json:"displayDensity"` + // available networks + Networks []networkInfo `json:"networks"` + // current system locale + Locale string `json:"locale"` + Bluetooth struct { + State string `json:"state"` + } `json:"bluetooth"` +} + +type networkCapabilities struct { + TransportTypes string `json:"transportTypes"` + NetworkCapabilities string `json:"networkCapabilities"` + LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` + LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` + SignalStrength int `json:"signalStrength"` + SSID string `json:"SSID"` +} + +type networkInfo struct { + Type int `json:"type"` + TypeName string `json:"typeName"` + Subtype int `json:"subtype"` + SubtypeName string `json:"subtypeName"` + IsConnected bool `json:"isConnected"` + DetailedState string `json:"detailedState"` + State string `json:"state"` + ExtraInfo string `json:"extraInfo"` + IsAvailable bool `json:"isAvailable"` + IsRoaming bool `json:"isRoaming"` + IsFailover bool `json:"isFailover"` + Capabilities networkCapabilities `json:"capabilities"` +} + +type Location struct { + AuthorizationStatus int `json:"authorizationStatus"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Altitude float64 `json:"altitude"` +} + +type BatteryInfo struct { + // Battery level in range [0.0, 1.0], where 1.0 means 100% charge. + Level float64 `json:"level"` + + // Battery state ( 1: on battery, discharging; 2: plugged in, less than 100%, 3: plugged in, at 100% ) + State BatteryState `json:"state"` + + Status BatteryStatus `json:"status"` +} + +type BatteryState int + +const ( + _ = iota + BatteryStateUnplugged BatteryState = iota // on battery, discharging + BatteryStateCharging // plugged in, less than 100% + BatteryStateFull // plugged in, at 100% +) + +func (v BatteryState) String() string { + switch v { + case BatteryStateUnplugged: + return "On battery, discharging" + case BatteryStateCharging: + return "Plugged in, less than 100%" + case BatteryStateFull: + return "Plugged in, at 100%" + default: + return "UNKNOWN" + } +} + +type Size struct { + Width int `json:"width"` + Height int `json:"height"` +} + +type Screen struct { + StatusBarSize Size `json:"statusBarSize"` + Scale float64 `json:"scale"` +} + +type AppInfo struct { + ProcessArguments struct { + Env interface{} `json:"env"` + Args []interface{} `json:"args"` + } `json:"processArguments"` + Name string `json:"name"` + AppBaseInfo +} + +type AppBaseInfo struct { + Pid int `json:"pid"` + BundleId string `json:"bundleId"` +} + +type AppState int + +const ( + AppStateNotRunning AppState = 1 << iota + AppStateRunningBack + AppStateRunningFront +) + +func (v AppState) String() string { + switch v { + case AppStateNotRunning: + return "Not Running" + case AppStateRunningBack: + return "Running (Back)" + case AppStateRunningFront: + return "Running (Front)" + default: + return "UNKNOWN" + } +} + +// AppLaunchOption Configure app launch parameters +type AppLaunchOption map[string]interface{} + +func NewAppLaunchOption() AppLaunchOption { + return make(AppLaunchOption) +} + +func (opt AppLaunchOption) WithBundleId(bundleId string) AppLaunchOption { + opt["bundleId"] = bundleId + return opt +} + +// WithShouldWaitForQuiescence whether to wait for quiescence on application startup +// Defaults to `true` +func (opt AppLaunchOption) WithShouldWaitForQuiescence(b bool) AppLaunchOption { + opt["shouldWaitForQuiescence"] = b + return opt +} + +// WithArguments The optional array of application command line arguments. +// The arguments are going to be applied if the application was not running before. +func (opt AppLaunchOption) WithArguments(args []string) AppLaunchOption { + opt["arguments"] = args + return opt +} + +// WithEnvironment The optional dictionary of environment variables for the application, which is going to be executed. +// The environment variables are going to be applied if the application was not running before. +func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOption { + opt["environment"] = env + return opt +} + +func (opt AppLaunchOption) WithAndroidBySelector(waitForComplete ...AndroidBySelector) AppLaunchOption { + opt["androidBySelector"] = waitForComplete + return opt +} + +// PasteboardType The type of the item on the pasteboard. +type PasteboardType string + +const ( + PasteboardTypePlaintext PasteboardType = "plaintext" + PasteboardTypeImage PasteboardType = "image" + PasteboardTypeUrl PasteboardType = "url" +) + +const ( + TextBackspace string = "\u0008" + TextDelete string = "\u007F" +) + +// type KeyboardKeyLabel string +// +// const ( +// KeyboardKeyReturn = "return" +// ) + +// DeviceButton A physical button on an iOS device. +type DeviceButton string + +const ( + DeviceButtonHome DeviceButton = "home" + DeviceButtonVolumeUp DeviceButton = "volumeUp" + DeviceButtonVolumeDown DeviceButton = "volumeDown" +) + +type NotificationType string + +const ( + NotificationTypePlain NotificationType = "plain" + NotificationTypeDarwin NotificationType = "darwin" +) + +// EventPageID The event page identifier +type EventPageID int + +const EventPageIDConsumer EventPageID = 0x0C + +// EventUsageID The event usage identifier (usages are defined per-page) +type EventUsageID int + +const ( + EventUsageIDCsmrVolumeUp EventUsageID = 0xE9 + EventUsageIDCsmrVolumeDown EventUsageID = 0xEA + EventUsageIDCsmrHome EventUsageID = 0x40 + EventUsageIDCsmrPower EventUsageID = 0x30 + EventUsageIDCsmrSnapshot EventUsageID = 0x65 // Power + Home +) + +type Orientation string + +const ( + // OrientationPortrait Device oriented vertically, home button on the bottom + OrientationPortrait Orientation = "PORTRAIT" + + // OrientationPortraitUpsideDown Device oriented vertically, home button on the top + OrientationPortraitUpsideDown Orientation = "UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN" + + // OrientationLandscapeLeft Device oriented horizontally, home button on the right + OrientationLandscapeLeft Orientation = "LANDSCAPE" + + // OrientationLandscapeRight Device oriented horizontally, home button on the left + OrientationLandscapeRight Orientation = "UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT" +) + +type Rotation struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` +} + +// SourceOption Configure the format or attribute of the Source +type SourceOption map[string]interface{} + +func NewSourceOption() SourceOption { + return make(SourceOption) +} + +// WithFormatAsJson Application elements tree in form of json string +func (opt SourceOption) WithFormatAsJson() SourceOption { + opt["format"] = "json" + return opt +} + +// WithFormatAsXml Application elements tree in form of xml string +func (opt SourceOption) WithFormatAsXml() SourceOption { + opt["format"] = "xml" + return opt +} + +// WithFormatAsDescription Application elements tree in form of internal XCTest debugDescription string +func (opt SourceOption) WithFormatAsDescription() SourceOption { + opt["format"] = "description" + return opt +} + +// WithScope Allows to provide XML scope. +// only `xml` is supported. +func (opt SourceOption) WithScope(scope string) SourceOption { + if vFormat, ok := opt["format"]; ok && vFormat != "xml" { + return opt + } + opt["scope"] = scope + return opt +} + +// WithExcludedAttributes Excludes the given attribute names. +// only `xml` is supported. +func (opt SourceOption) WithExcludedAttributes(attributes []string) SourceOption { + if vFormat, ok := opt["format"]; ok && vFormat != "xml" { + return opt + } + opt["excluded_attributes"] = strings.Join(attributes, ",") + return opt +} + +const ( + // legacyWebElementIdentifier is the string constant used in the old + // WebDriver JSON protocol that is the key for the map that contains an + // unique element identifier. + legacyWebElementIdentifier = "ELEMENT" + + // webElementIdentifier is the string constant defined by the W3C + // specification that is the key for the map that contains a unique element identifier. + webElementIdentifier = "element-6066-11e4-a52e-4f735466cecf" +) + +func elementIDFromValue(val map[string]string) string { + for _, key := range []string{webElementIdentifier, legacyWebElementIdentifier} { + if v, ok := val[key]; ok && v != "" { + return v + } + } + return "" +} + +// performance ranking: class name > accessibility id > link text > predicate > class chain > xpath +type BySelector struct { + ClassName ElementType `json:"class name"` + + // isSearchByIdentifier + Name string `json:"name"` + Id string `json:"id"` + AccessibilityId string `json:"accessibility id"` + + // partialSearch + LinkText ElementAttribute `json:"link text"` + PartialLinkText ElementAttribute `json:"partial link text"` + // partialSearch + + Predicate string `json:"predicate string"` + + ClassChain string `json:"class chain"` + + XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively + + // Set the search criteria to match the given resource ResourceIdID. + ResourceIdID string `json:"id"` + // Set the search criteria to match the content-description property for a widget. + ContentDescription string `json:"accessibility id"` + + UiAutomator string `json:"-android uiautomator"` +} + +func (wl BySelector) getUsingAndValue() (using, value string) { + vBy := reflect.ValueOf(wl) + tBy := reflect.TypeOf(wl) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + switch vi := vi.(type) { + case ElementType: + value = vi.String() + case string: + value = vi + case ElementAttribute: + value = vi.String() + } + if value != "" && value != "UNKNOWN" { + using = tBy.Field(i).Tag.Get("json") + return + } + } + return +} + +func (by BySelector) getMethodAndSelector() (method, selector string) { + vBy := reflect.ValueOf(by) + tBy := reflect.TypeOf(by) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + // switch vi := vi.(type) { + // case string: + // selector = vi + // } + selector = vi.(string) + if selector != "" && selector != "UNKNOWN" { + method = tBy.Field(i).Tag.Get("json") + return + } + } + return +} + +type ElementAttribute map[string]interface{} + +func (ea ElementAttribute) String() string { + for k, v := range ea { + switch v := v.(type) { + case bool: + return k + "=" + strconv.FormatBool(v) + case string: + return k + "=" + v + default: + return k + "=" + fmt.Sprintf("%v", v) + } + } + return "UNKNOWN" +} + +func (ea ElementAttribute) getAttributeName() string { + for k := range ea { + return k + } + return "UNKNOWN" +} + +func NewElementAttribute() ElementAttribute { + return make(ElementAttribute) +} + +// WithUID Element's unique identifier +func (ea ElementAttribute) WithUID(uid string) ElementAttribute { + ea["UID"] = uid + return ea +} + +// WithAccessibilityContainer Whether element is an accessibility container +// (contains children of any depth that are accessible) +func (ea ElementAttribute) WithAccessibilityContainer(b bool) ElementAttribute { + ea["accessibilityContainer"] = b + return ea +} + +// WithAccessible Whether element is accessible +func (ea ElementAttribute) WithAccessible(b bool) ElementAttribute { + ea["accessible"] = b + return ea +} + +// WithEnabled Whether element is enabled +func (ea ElementAttribute) WithEnabled(b bool) ElementAttribute { + ea["enabled"] = b + return ea +} + +// WithLabel Element's label +func (ea ElementAttribute) WithLabel(s string) ElementAttribute { + ea["label"] = s + return ea +} + +// WithName Element's name +func (ea ElementAttribute) WithName(s string) ElementAttribute { + ea["name"] = s + return ea +} + +// WithSelected Element's selected state +func (ea ElementAttribute) WithSelected(b bool) ElementAttribute { + ea["selected"] = b + return ea +} + +// WithType Element's type +func (ea ElementAttribute) WithType(elemType ElementType) ElementAttribute { + ea["type"] = elemType + return ea +} + +// WithValue Element's value +func (ea ElementAttribute) WithValue(s string) ElementAttribute { + ea["value"] = s + return ea +} + +// WithVisible +// +// Whether element is visible +func (ea ElementAttribute) WithVisible(b bool) ElementAttribute { + ea["visible"] = b + return ea +} + +func (et ElementType) String() string { + vBy := reflect.ValueOf(et) + tBy := reflect.TypeOf(et) + for i := 0; i < vBy.NumField(); i++ { + if vBy.Field(i).Bool() { + return tBy.Field(i).Tag.Get("json") + } + } + return "UNKNOWN" +} + +// ElementType +// !!! This mapping should be updated if there are changes after each new XCTest release"` +type ElementType struct { + Any bool `json:"XCUIElementTypeAny"` + Other bool `json:"XCUIElementTypeOther"` + Application bool `json:"XCUIElementTypeApplication"` + Group bool `json:"XCUIElementTypeGroup"` + Window bool `json:"XCUIElementTypeWindow"` + Sheet bool `json:"XCUIElementTypeSheet"` + Drawer bool `json:"XCUIElementTypeDrawer"` + Alert bool `json:"XCUIElementTypeAlert"` + Dialog bool `json:"XCUIElementTypeDialog"` + Button bool `json:"XCUIElementTypeButton"` + RadioButton bool `json:"XCUIElementTypeRadioButton"` + RadioGroup bool `json:"XCUIElementTypeRadioGroup"` + CheckBox bool `json:"XCUIElementTypeCheckBox"` + DisclosureTriangle bool `json:"XCUIElementTypeDisclosureTriangle"` + PopUpButton bool `json:"XCUIElementTypePopUpButton"` + ComboBox bool `json:"XCUIElementTypeComboBox"` + MenuButton bool `json:"XCUIElementTypeMenuButton"` + ToolbarButton bool `json:"XCUIElementTypeToolbarButton"` + Popover bool `json:"XCUIElementTypePopover"` + Keyboard bool `json:"XCUIElementTypeKeyboard"` + Key bool `json:"XCUIElementTypeKey"` + NavigationBar bool `json:"XCUIElementTypeNavigationBar"` + TabBar bool `json:"XCUIElementTypeTabBar"` + TabGroup bool `json:"XCUIElementTypeTabGroup"` + Toolbar bool `json:"XCUIElementTypeToolbar"` + StatusBar bool `json:"XCUIElementTypeStatusBar"` + Table bool `json:"XCUIElementTypeTable"` + TableRow bool `json:"XCUIElementTypeTableRow"` + TableColumn bool `json:"XCUIElementTypeTableColumn"` + Outline bool `json:"XCUIElementTypeOutline"` + OutlineRow bool `json:"XCUIElementTypeOutlineRow"` + Browser bool `json:"XCUIElementTypeBrowser"` + CollectionView bool `json:"XCUIElementTypeCollectionView"` + Slider bool `json:"XCUIElementTypeSlider"` + PageIndicator bool `json:"XCUIElementTypePageIndicator"` + ProgressIndicator bool `json:"XCUIElementTypeProgressIndicator"` + ActivityIndicator bool `json:"XCUIElementTypeActivityIndicator"` + SegmentedControl bool `json:"XCUIElementTypeSegmentedControl"` + Picker bool `json:"XCUIElementTypePicker"` + PickerWheel bool `json:"XCUIElementTypePickerWheel"` + Switch bool `json:"XCUIElementTypeSwitch"` + Toggle bool `json:"XCUIElementTypeToggle"` + Link bool `json:"XCUIElementTypeLink"` + Image bool `json:"XCUIElementTypeImage"` + Icon bool `json:"XCUIElementTypeIcon"` + SearchField bool `json:"XCUIElementTypeSearchField"` + ScrollView bool `json:"XCUIElementTypeScrollView"` + ScrollBar bool `json:"XCUIElementTypeScrollBar"` + StaticText bool `json:"XCUIElementTypeStaticText"` + TextField bool `json:"XCUIElementTypeTextField"` + SecureTextField bool `json:"XCUIElementTypeSecureTextField"` + DatePicker bool `json:"XCUIElementTypeDatePicker"` + TextView bool `json:"XCUIElementTypeTextView"` + Menu bool `json:"XCUIElementTypeMenu"` + MenuItem bool `json:"XCUIElementTypeMenuItem"` + MenuBar bool `json:"XCUIElementTypeMenuBar"` + MenuBarItem bool `json:"XCUIElementTypeMenuBarItem"` + Map bool `json:"XCUIElementTypeMap"` + WebView bool `json:"XCUIElementTypeWebView"` + IncrementArrow bool `json:"XCUIElementTypeIncrementArrow"` + DecrementArrow bool `json:"XCUIElementTypeDecrementArrow"` + Timeline bool `json:"XCUIElementTypeTimeline"` + RatingIndicator bool `json:"XCUIElementTypeRatingIndicator"` + ValueIndicator bool `json:"XCUIElementTypeValueIndicator"` + SplitGroup bool `json:"XCUIElementTypeSplitGroup"` + Splitter bool `json:"XCUIElementTypeSplitter"` + RelevanceIndicator bool `json:"XCUIElementTypeRelevanceIndicator"` + ColorWell bool `json:"XCUIElementTypeColorWell"` + HelpTag bool `json:"XCUIElementTypeHelpTag"` + Matte bool `json:"XCUIElementTypeMatte"` + DockItem bool `json:"XCUIElementTypeDockItem"` + Ruler bool `json:"XCUIElementTypeRuler"` + RulerMarker bool `json:"XCUIElementTypeRulerMarker"` + Grid bool `json:"XCUIElementTypeGrid"` + LevelIndicator bool `json:"XCUIElementTypeLevelIndicator"` + Cell bool `json:"XCUIElementTypeCell"` + LayoutArea bool `json:"XCUIElementTypeLayoutArea"` + LayoutItem bool `json:"XCUIElementTypeLayoutItem"` + Handle bool `json:"XCUIElementTypeHandle"` + Stepper bool `json:"XCUIElementTypeStepper"` + Tab bool `json:"XCUIElementTypeTab"` + TouchBar bool `json:"XCUIElementTypeTouchBar"` + StatusItem bool `json:"XCUIElementTypeStatusItem"` + EditText bool `json:"android.widget.EditText"` +} + +// ProtectedResource A system resource that requires user authorization to access. +type ProtectedResource int + +// https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc +const ( + ProtectedResourceContacts ProtectedResource = 1 + ProtectedResourceCalendar ProtectedResource = 2 + ProtectedResourceReminders ProtectedResource = 3 + ProtectedResourcePhotos ProtectedResource = 4 + ProtectedResourceMicrophone ProtectedResource = 5 + ProtectedResourceCamera ProtectedResource = 6 + ProtectedResourceMediaLibrary ProtectedResource = 7 + ProtectedResourceHomeKit ProtectedResource = 8 + ProtectedResourceSystemRootDirectory ProtectedResource = 0x40000000 + ProtectedResourceUserDesktopDirectory ProtectedResource = 0x40000001 + ProtectedResourceUserDownloadsDirectory ProtectedResource = 0x40000002 + ProtectedResourceUserDocumentsDirectory ProtectedResource = 0x40000003 + ProtectedResourceBluetooth ProtectedResource = -0x40000000 + ProtectedResourceKeyboardNetwork ProtectedResource = -0x40000001 + ProtectedResourceLocation ProtectedResource = -0x40000002 + ProtectedResourceHealth ProtectedResource = -0x40000003 +) + +type Condition func(wd WebDriver) (bool, error) + +type Direction string + +const ( + DirectionUp Direction = "up" + DirectionDown Direction = "down" + DirectionLeft Direction = "left" + DirectionRight Direction = "right" +) + +type PickerWheelOrder string + +const ( + PickerWheelOrderNext PickerWheelOrder = "next" + PickerWheelOrderPrevious PickerWheelOrder = "previous" +) + +type Point struct { + X int `json:"x"` // upper left X coordinate of selected element + Y int `json:"y"` // upper left Y coordinate of selected element +} + +type PointF struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + +type Rect struct { + Point + 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 + 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 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 NewData(data map[string]interface{}, options ...DataOption) *DataOptions { + if data == nil { + data = make(map[string]interface{}) + } + dataOptions := &DataOptions{ + Data: data, + } + for _, option := range options { + option(dataOptions) + } + + if len(dataOptions.Scope) == 0 { + dataOptions.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope + } + + if _, ok := dataOptions.Data["steps"]; !ok { + dataOptions.Data["steps"] = 12 // default steps + } + + if _, ok := dataOptions.Data["duration"]; !ok { + dataOptions.Data["duration"] = 1.0 // default duration + } + + if _, ok := dataOptions.Data["frequency"]; !ok { + dataOptions.Data["frequency"] = 60 // default frequency + } + + if _, ok := dataOptions.Data["isReplace"]; !ok { + dataOptions.Data["isReplace"] = true // default true + } + + return dataOptions +} + +// current implemeted device: IOSDevice, AndroidDevice +type Device interface { + UUID() string + NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) +} + +// WebDriver defines methods supported by WebDriver drivers. +type WebDriver interface { + // NewSession starts a new session and returns the SessionInfo. + NewSession(capabilities Capabilities) (SessionInfo, error) + + ActiveSession() (SessionInfo, error) + // DeleteSession Kills application associated with that session and removes session + // 1) alertsMonitor disable + // 2) testedApplicationBundleId terminate + DeleteSession() error + + Status() (DeviceStatus, error) + + DeviceInfo() (DeviceInfo, error) + + // Location Returns device location data. + // + // It requires to configure location access permission by manual. + // The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization. + // 'authorizationStatus' indicates current authorization status. '3' is 'Always'. + // https://developer.apple.com/documentation/corelocation/clauthorizationstatus + // + // Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always + // + // The return value could be zero even if the permission is set to 'Always' + // since the location service needs some time to update the location data. + Location() (Location, error) + BatteryInfo() (BatteryInfo, error) + WindowSize() (Size, error) + Screen() (Screen, error) + Scale() (float64, error) + ActiveAppInfo() (AppInfo, error) + // ActiveAppsList Retrieves the information about the currently active apps + ActiveAppsList() ([]AppBaseInfo, error) + // AppState Get the state of the particular application in scope of the current session. + // !This method is only returning reliable results since Xcode9 SDK + AppState(bundleId string) (AppState, error) + + // IsLocked Checks if the screen is locked or not. + IsLocked() (bool, error) + // Unlock Forces the device under test to unlock. + // An immediate return will happen if the device is already unlocked + // and an error is going to be thrown if the screen has not been unlocked after the timeout. + Unlock() error + // Lock Forces the device under test to switch to the lock screen. + // An immediate return will happen if the device is already locked + // and an error is going to be thrown if the screen has not been locked after the timeout. + Lock() error + + // Homescreen Forces the device under test to switch to the home screen + Homescreen() error + + // AlertText Returns alert's title and description separated by new lines + AlertText() (string, error) + // AlertButtons Gets the labels of the buttons visible in the alert + AlertButtons() ([]string, error) + // AlertAccept Accepts alert, if present + AlertAccept(label ...string) error + // AlertDismiss Dismisses alert, if present + AlertDismiss(label ...string) error + // AlertSendKeys Types a text into an input inside the alert container, if it is present + AlertSendKeys(text string) error + + // AppLaunch Launch an application with given bundle identifier in scope of current session. + // !This method is only available since Xcode9 SDK + AppLaunch(bundleId string, launchOpt ...AppLaunchOption) error + // AppLaunchUnattached Launch the app with the specified bundle ID. + AppLaunchUnattached(bundleId string) error + // AppTerminate Terminate an application with the given bundle id. + // Either `true` if the app has been successfully terminated or `false` if it was not running + AppTerminate(bundleId string) (bool, error) + // AppActivate Activate an application with given bundle identifier in scope of current session. + // !This method is only available since Xcode9 SDK + AppActivate(bundleId string) error + // AppDeactivate Deactivates application for given time and then activate it again + // The minimum application switch wait is 3 seconds + AppDeactivate(second float64) error + + // AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4 + AppAuthReset(ProtectedResource) error + + // StartCamera Starts a new camera for recording + StartCamera() error + // StopCamera Stops the camera for recording + StopCamera() error + + // Tap Sends a tap event at the coordinate. + Tap(x, y int, options ...DataOption) error + TapFloat(x, y float64, options ...DataOption) error + + // DoubleTap Sends a double tap event at the coordinate. + DoubleTap(x, y int) error + DoubleTapFloat(x, y float64) error + + // TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration. + // second: The default value is 1 + TouchAndHold(x, y int, second ...float64) error + TouchAndHoldFloat(x, y float64, second ...float64) error + + // 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 + + // 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 + + ForceTouch(x, y int, pressure float64, second ...float64) error + ForceTouchFloat(x, y, pressure float64, second ...float64) error + + // PerformW3CActions Perform complex touch action in scope of the current application. + PerformW3CActions(actions *W3CActions) error + PerformAppiumTouchActions(touchActs *TouchActions) error + + // SetPasteboard Sets data to the general pasteboard + SetPasteboard(contentType PasteboardType, content string) error + // GetPasteboard Gets the data contained in the general pasteboard. + // It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330 + GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) + + // 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 + + // Input works like SendKeys + Input(text string, options ...DataOption) error + + // KeyboardDismiss Tries to dismiss the on-screen keyboard + KeyboardDismiss(keyNames ...string) error + + // PressButton Presses the corresponding hardware button on the device + PressButton(devBtn DeviceButton) error + + // IOHIDEvent Emulated triggering of the given low-level IOHID device event. + // duration: The event duration in float seconds (XCTest uses 0.005 for a single press event) + IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) error + + // ExpectNotification Creates an expectation that is fulfilled when an expected Notification is received + ExpectNotification(notifyName string, notifyType NotificationType, second ...int) error + + // SiriActivate Activates Siri service voice recognition with the given text to parse + SiriActivate(text string) error + // SiriOpenUrl Opens the particular url scheme using Siri voice recognition helpers. + // !This will only work since XCode 8.3/iOS 10.3 + // It doesn't actually work, right? + SiriOpenUrl(url string) error + + Orientation() (Orientation, error) + // SetOrientation Sets requested device interface orientation. + SetOrientation(Orientation) error + + Rotation() (Rotation, error) + // SetRotation Sets the devices orientation to the rotation passed. + SetRotation(Rotation) error + + // MatchTouchID Matches or mismatches TouchID request + MatchTouchID(isMatch bool) error + + // ActiveElement Returns the element, which currently holds the keyboard input focus or nil if there are no such elements. + ActiveElement() (WebElement, error) + FindElement(by BySelector) (WebElement, error) + FindElements(by BySelector) ([]WebElement, error) + + Screenshot() (*bytes.Buffer, error) + + // Source Return application elements tree + Source(srcOpt ...SourceOption) (string, error) + // AccessibleSource Return application elements accessibility tree + AccessibleSource() (string, error) + + // HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions + // Checks health of XCTest by: + // 1) Querying application for some elements, + // 2) Triggering some device events. + HealthCheck() error + GetAppiumSettings() (map[string]interface{}, error) + SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) + + IsHealthy() (bool, error) + + // WaitWithTimeoutAndInterval waits for the condition to evaluate to true. + WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error + // WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. + WaitWithTimeout(condition Condition, timeout time.Duration) error + // Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. + Wait(condition Condition) error + + // Close inner connections properly + Close() error + + // triggers the log capture and returns the log entries + StartCaptureLog(identifier ...string) (err error) + StopCaptureLog() (result interface{}, err error) +} + +// WebElement defines method supported by web elements. +type WebElement interface { + // Click Waits for element to become stable (not move) and performs sync tap on element. + Click() error + // SendKeys Types a text into element. It will try to activate keyboard on element, + // if element has no keyboard focus. + // frequency: Frequency of typing (letters per sec). The default value is 60 + SendKeys(text string, options ...DataOption) error + // Clear Clears text on element. It will try to activate keyboard on element, + // if element has no keyboard focus. + Clear() error + + // Tap Waits for element to become stable (not move) and performs sync tap on element, + // relative to the current element position + Tap(x, y int) error + TapFloat(x, y float64) error + + // DoubleTap Sends a double tap event to a hittable point computed for the element. + DoubleTap() error + + // TouchAndHold Sends a long-press gesture to a hittable point computed for the element, + // holding for the specified duration. + // second: The default value is 1 + TouchAndHold(second ...float64) error + // TwoFingerTap Sends a two finger tap event to a hittable point computed for the element. + TwoFingerTap() error + // TapWithNumberOfTaps Sends one or more taps with one or more touch points. + TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) error + // ForceTouch Waits for element to become stable (not move) and performs sync force touch on element. + // second: The default value is 1 + ForceTouch(pressure float64, second ...float64) error + // ForceTouchFloat works like ForceTouch, but relative to the current element position + ForceTouchFloat(x, y, pressure float64, second ...float64) error + + // Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate. + // relative to the current element position + // pressForDuration: The default value is 1 second. + Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error + DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) error + + // Swipe works like Drag, but `pressForDuration` value is 0. + // relative to the current element position + Swipe(fromX, fromY, toX, toY int) error + SwipeFloat(fromX, fromY, toX, toY float64) error + // SwipeDirection Performs swipe gesture on the element. + // velocity: swipe speed in pixels per second. Custom velocity values are only supported since Xcode SDK 11.4. + SwipeDirection(direction Direction, velocity ...float64) error + + // Pinch Sends a pinching gesture with two touches. + // scale: The scale of the pinch gesture. Use a scale between 0 and 1 to "pinch close" or zoom out + // and a scale greater than 1 to "pinch open" or zoom in. + // velocity: The velocity of the pinch in scale factor per second. + Pinch(scale, velocity float64) error + PinchToZoomOutByW3CAction(scale ...float64) error + + // Rotate Sends a rotation gesture with two touches. + // rotation: The rotation of the gesture in radians. + // velocity: The velocity of the rotation gesture in radians per second. + Rotate(rotation float64, velocity ...float64) error + + // PickerWheelSelect + // offset: The default value is 2 + PickerWheelSelect(order PickerWheelOrder, offset ...int) error + + ScrollElementByName(name string) error + ScrollElementByPredicate(predicate string) error + ScrollToVisible() error + // ScrollDirection + // distance: The default value is 0.5 + ScrollDirection(direction Direction, distance ...float64) error + + FindElement(by BySelector) (element WebElement, err error) + FindElements(by BySelector) (elements []WebElement, err error) + FindVisibleCells() (elements []WebElement, err error) + + Rect() (rect Rect, err error) + Location() (Point, error) + Size() (Size, error) + Text() (text string, err error) + Type() (elemType string, err error) + IsEnabled() (enabled bool, err error) + IsDisplayed() (displayed bool, err error) + IsSelected() (selected bool, err error) + IsAccessible() (accessible bool, err error) + IsAccessibilityContainer() (isAccessibilityContainer bool, err error) + GetAttribute(attr ElementAttribute) (value string, err error) + UID() (uid string) + + Screenshot() (raw *bytes.Buffer, err error) +} diff --git a/hrp/pkg/uixt/ios_action.go b/hrp/pkg/uixt/ios_action.go new file mode 100644 index 00000000..0541f827 --- /dev/null +++ b/hrp/pkg/uixt/ios_action.go @@ -0,0 +1,373 @@ +package uixt + +import ( + "strconv" + "strings" +) + +type W3CActions []map[string]interface{} + +func NewW3CActions(capacity ...int) *W3CActions { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(W3CActions, 0, capacity[0]) + return &tmp +} + +func (act *W3CActions) SendKeys(text string) *W3CActions { + keyboard := make(map[string]interface{}) + keyboard["type"] = "key" + keyboard["id"] = "keyboard" + strconv.FormatInt(int64(len(*act)+1), 10) + + ss := strings.Split(text, "") + type KeyEvent struct { + Type string `json:"type"` + Value string `json:"value"` + } + actOptKey := make([]KeyEvent, 0, len(ss)+1) + for i := range ss { + actOptKey = append( + actOptKey, + KeyEvent{Type: "keyDown", Value: ss[i]}, + KeyEvent{Type: "keyUp", Value: ss[i]}, + ) + } + keyboard["actions"] = actOptKey + *act = append(*act, keyboard) + return act +} + +func (act *W3CActions) _newFinger() map[string]interface{} { + pointer := make(map[string]interface{}) + pointer["type"] = "pointer" + pointer["id"] = "finger" + strconv.FormatInt(int64(len(*act)+1), 10) + pointer["parameters"] = map[string]string{"pointerType": "touch"} + return pointer +} + +func (act *W3CActions) FingerAction(fingerAct *FingerAction, fActs ...*FingerAction) *W3CActions { + fActs = append([]*FingerAction{fingerAct}, fActs...) + for i := range fActs { + pointer := act._newFinger() + pointer["actions"] = *fActs[i] + *act = append(*act, pointer) + } + return act +} + +type FingerAction []map[string]interface{} + +func NewFingerAction(capacity ...int) *FingerAction { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(FingerAction, 0, capacity[0]) + return &tmp +} + +type FingerMove map[string]interface{} + +func NewFingerMove() FingerMove { + return map[string]interface{}{"type": "pointerMove"} +} + +func (fm FingerMove) WithXY(x, y int) FingerMove { + fm["x"] = x + fm["y"] = y + return fm +} + +func (fm FingerMove) WithXYFloat(x, y float64) FingerMove { + fm["x"] = x + fm["y"] = y + return fm +} + +func (fm FingerMove) WithOrigin(element WebElement) FingerMove { + fm["origin"] = element.UID() + return fm +} + +func (fm FingerMove) WithDuration(second float64) FingerMove { + fm["duration"] = second + return fm +} + +func (fa *FingerAction) Move(fm FingerMove) *FingerAction { + *fa = append(*fa, fm) + return fa +} + +func (fa *FingerAction) Down() *FingerAction { + *fa = append(*fa, map[string]interface{}{"type": "pointerDown"}) + return fa +} + +func (fa *FingerAction) Up() *FingerAction { + *fa = append(*fa, map[string]interface{}{"type": "pointerUp"}) + return fa +} + +func (fa *FingerAction) Pause(second ...float64) *FingerAction { + if len(second) == 0 || second[0] < 0 { + second = []float64{0.5} + } + tmp := map[string]interface{}{ + "type": "pause", + "duration": second[0] * 1000, + } + *fa = append(*fa, tmp) + return fa +} + +func (act *W3CActions) Tap(x, y int, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(0.1). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) DoubleTap(x, y int, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(0.1). + Up(). + Pause(0.04). + Down(). + Pause(0.1). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) Press(x, y int, second float64, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(second). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) Swipe(fromX, fromY, toX, toY int, element ...WebElement) *W3CActions { + fmFrom := NewFingerMove().WithXY(fromX, fromY) + fmTo := NewFingerMove().WithXY(toX, toY) + if len(element) != 0 { + fmFrom.WithOrigin(element[0]) + fmTo.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fmFrom). + Down(). + Pause(0.25). + Move(fmTo). + Pause(0.25). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) SwipeFloat(fromX, fromY, toX, toY float64, element ...WebElement) *W3CActions { + fmFrom := NewFingerMove().WithXYFloat(fromX, fromY) + fmTo := NewFingerMove().WithXYFloat(toX, toY) + if len(element) != 0 { + fmFrom.WithOrigin(element[0]) + fmTo.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fmFrom). + Down(). + Pause(0.25). + Move(fmTo). + Pause(0.25). + Up() + return act.FingerAction(fingerAction) +} + +/* ---------------------------------------------------------------------------------------------------------------- */ + +type TouchActions []map[string]interface{} + +func NewTouchActions(capacity ...int) *TouchActions { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(TouchActions, 0, capacity[0]) + return &tmp +} + +func (act *TouchActions) MoveTo(opt TouchActionMoveTo) *TouchActions { + tmp := map[string]interface{}{ + "action": "moveTo", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Tap(opt TouchActionTap) *TouchActions { + tmp := map[string]interface{}{ + "action": "tap", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Press(opt TouchActionPress) *TouchActions { + tmp := map[string]interface{}{ + "action": "press", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) LongPress(opt TouchActionLongPress) *TouchActions { + tmp := map[string]interface{}{ + "action": "longPress", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Wait(second ...float64) *TouchActions { + if len(second) == 0 || second[0] < 0 { + second = []float64{0.5} + } + tmp := map[string]interface{}{ + "action": "wait", + "options": map[string]interface{}{"ms": second[0] * 1000}, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Release() *TouchActions { + tmp := map[string]interface{}{"action": "release"} + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Cancel() *TouchActions { + tmp := map[string]interface{}{"action": "cancel"} + *act = append(*act, tmp) + return act +} + +type TouchActionMoveTo map[string]interface{} + +func NewTouchActionMoveTo() TouchActionMoveTo { + return make(map[string]interface{}) +} + +func (opt TouchActionMoveTo) WithXY(x, y int) TouchActionMoveTo { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionMoveTo) WithXYFloat(x, y float64) TouchActionMoveTo { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionMoveTo) WithElement(element WebElement) TouchActionMoveTo { + opt["element"] = element.UID() + return opt +} + +type TouchActionTap map[string]interface{} + +func NewTouchActionTap() TouchActionTap { + return make(map[string]interface{}) +} + +func (opt TouchActionTap) WithXY(x, y int) TouchActionTap { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionTap) WithXYFloat(x, y float64) TouchActionTap { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionTap) WithElement(element WebElement) TouchActionTap { + opt["element"] = element.UID() + return opt +} + +func (opt TouchActionTap) WithCount(count int) TouchActionTap { + opt["count"] = count + return opt +} + +type TouchActionPress map[string]interface{} + +func NewTouchActionPress() TouchActionPress { + return make(map[string]interface{}) +} + +func (opt TouchActionPress) WithXY(x, y int) TouchActionPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionPress) WithXYFloat(x, y float64) TouchActionPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionPress) WithElement(element WebElement) TouchActionPress { + opt["element"] = element.UID() + return opt +} + +func (opt TouchActionPress) WithPressure(pressure float64) TouchActionPress { + opt["pressure"] = pressure + return opt +} + +type TouchActionLongPress map[string]interface{} + +func NewTouchActionLongPress() TouchActionLongPress { + return make(map[string]interface{}) +} + +func (opt TouchActionLongPress) WithXY(x, y int) TouchActionLongPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionLongPress) WithXYFloat(x, y float64) TouchActionLongPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionLongPress) WithElement(element WebElement) TouchActionLongPress { + opt["element"] = element.UID() + return opt +} diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go new file mode 100644 index 00000000..6b2cadc1 --- /dev/null +++ b/hrp/pkg/uixt/ios_device.go @@ -0,0 +1,625 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + builtinJSON "encoding/json" + "fmt" + "io" + "mime" + "mime/multipart" + "net" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/env" + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +const ( + defaultWDAPort = 8100 + defaultMjpegPort = 9100 +) + +const ( + // Changes the value of maximum depth for traversing elements source tree. + // It may help to prevent out of memory or timeout errors while getting the elements source tree, + // but it might restrict the depth of source tree. + // A part of elements source tree might be lost if the value was too small. Defaults to 50 + snapshotMaxDepth = 10 + // Allows to customize accept/dismiss alert button selector. + // It helps you to handle an arbitrary element as accept button in accept alert command. + // The selector should be a valid class chain expression, where the search root is the alert element itself. + // The default button location algorithm is used if the provided selector is wrong or does not match any element. + // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] + acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" + dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" +) + +type IOSDeviceOption func(*IOSDevice) + +func WithUDID(udid string) IOSDeviceOption { + return func(device *IOSDevice) { + device.UDID = udid + } +} + +func WithWDAPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.Port = port + } +} + +func WithWDAMjpegPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.MjpegPort = port + } +} + +func WithLogOn(logOn bool) IOSDeviceOption { + return func(device *IOSDevice) { + device.LogOn = logOn + } +} + +func WithResetHomeOnStartup(reset bool) IOSDeviceOption { + return func(device *IOSDevice) { + device.ResetHomeOnStartup = reset + } +} + +func WithSnapshotMaxDepth(depth int) IOSDeviceOption { + return func(device *IOSDevice) { + device.SnapshotMaxDepth = depth + } +} + +func WithAcceptAlertButtonSelector(selector string) IOSDeviceOption { + return func(device *IOSDevice) { + device.AcceptAlertButtonSelector = selector + } +} + +func WithDismissAlertButtonSelector(selector string) IOSDeviceOption { + return func(device *IOSDevice) { + device.DismissAlertButtonSelector = selector + } +} + +func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption { + return func(device *IOSDevice) { + device.PerfOptions = &giDevice.PerfOptions{} + for _, option := range options { + option(device.PerfOptions) + } + } +} + +func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { + var usbmux giDevice.Usbmux + if usbmux, err = giDevice.NewUsbmux(); err != nil { + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("init usbmux failed: %v", err)) + } + + if devices, err = usbmux.Devices(); err != nil { + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("list ios devices failed: %v", err)) + } + + // filter by udid + var deviceList []giDevice.Device + for _, d := range devices { + for _, u := range udid { + if u != "" && u != d.Properties().SerialNumber { + continue + } + deviceList = append(deviceList, d) + } + } + + return deviceList, nil +} + +func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) { + if dev.UDID != "" { + deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) + } + if dev.MjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) + } + if dev.LogOn { + deviceOptions = append(deviceOptions, WithLogOn(true)) + } + if dev.PerfOptions != nil { + deviceOptions = append(deviceOptions, WithPerfOptions(dev.perfOpitons()...)) + } + if dev.ResetHomeOnStartup { + deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true)) + } + if dev.SnapshotMaxDepth != 0 { + deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth)) + } + if dev.AcceptAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector)) + } + if dev.DismissAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithDismissAlertButtonSelector(dev.DismissAlertButtonSelector)) + } + return +} + +func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { + device = &IOSDevice{ + Port: defaultWDAPort, + MjpegPort: defaultMjpegPort, + SnapshotMaxDepth: snapshotMaxDepth, + AcceptAlertButtonSelector: acceptAlertButtonSelector, + DismissAlertButtonSelector: dismissAlertButtonSelector, + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is active such as douyin or wexin + ResetHomeOnStartup: true, + } + for _, option := range options { + option(device) + } + + deviceList, err := IOSDevices(device.UDID) + if err != nil { + return nil, err + } + + if len(deviceList) > 0 { + device.UDID = deviceList[0].Properties().SerialNumber + log.Info().Str("udid", device.UDID).Msg("select device") + device.d = deviceList[0] + return device, nil + } + + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("device %s not found", device.UDID)) +} + +type IOSDevice struct { + d giDevice.Device + PerfOptions *giDevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"` + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + + // switch to iOS springboard before init WDA session + ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` + + // config appium settings + SnapshotMaxDepth int `json:"snapshot_max_depth,omitempty" yaml:"snapshot_max_depth,omitempty"` + AcceptAlertButtonSelector string `json:"accept_alert_button_selector,omitempty" yaml:"accept_alert_button_selector,omitempty"` + DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"` +} + +func (dev *IOSDevice) UUID() string { + return dev.UDID +} + +func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { + // init WDA driver + if capabilities == nil { + capabilities = NewCapabilities() + capabilities.WithDefaultAlertAction(AlertActionAccept) + } + + var driver WebDriver + if env.WDA_USB_DRIVER == "" { + // default use http driver + driver, err = dev.NewHTTPDriver(capabilities) + } else { + driver, err = dev.NewUSBDriver(capabilities) + } + if err != nil { + return nil, errors.Wrap(err, "failed to init WDA driver") + } + + if dev.ResetHomeOnStartup { + log.Info().Msg("go back to home screen") + if err = driver.Homescreen(); err != nil { + return nil, errors.Wrap(code.MobileUIDriverError, + fmt.Sprintf("go back to home screen failed: %v", err)) + } + } + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(code.MobileUIDriverError, + fmt.Sprintf("extend WebDriver failed: %v", err)) + } + settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ + "snapshotMaxDepth": dev.SnapshotMaxDepth, + "acceptAlertButtonSelector": dev.AcceptAlertButtonSelector, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set appium WDA settings") + } + log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + + if dev.LogOn { + err = driverExt.Driver.StartCaptureLog("hrp_wda_log") + if err != nil { + return nil, err + } + } + + if dev.PerfOptions != nil { + data, err := dev.d.PerfStart(dev.perfOpitons()...) + if err != nil { + return nil, err + } + + driverExt.perfStop = make(chan struct{}) + // start performance monitor + go func() { + for { + select { + case <-driverExt.perfStop: + dev.d.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + driverExt.perfData = append(driverExt.perfData, string(d)) + } + } + }() + } + + driverExt.UUID = dev.UUID() + return driverExt, nil +} + +func (dev *IOSDevice) forward(localPort, remotePort int) error { + log.Info().Int("localPort", localPort).Int("remotePort", remotePort). + Str("udid", dev.UDID).Msg("forward tcp port") + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + log.Error().Err(err).Msg("listen tcp error") + return err + } + + go func(listener net.Listener, device giDevice.Device) { + for { + accept, err := listener.Accept() + if err != nil { + log.Error().Err(err).Msg("accept error") + continue + } + + rInnerConn, err := device.NewConnect(remotePort) + if err != nil { + log.Error().Err(err).Msg("connect to ios device failed") + os.Exit(code.GetErrorCode(code.IOSDeviceConnectionError)) + } + + rConn := rInnerConn.RawConn() + _ = rConn.SetDeadline(time.Time{}) + + go func(lConn net.Conn) { + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(lConn, rConn); err != nil { + log.Error().Err(err).Msg("copy local -> remote") + } + }(lConn, rConn) + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(rConn, lConn); err != nil { + log.Error().Err(err).Msg("copy local <- remote") + } + }(lConn, rConn) + }(accept) + } + }(listener, dev.d) + + return nil +} + +func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) { + if dev.PerfOptions == nil { + return + } + + // system + if dev.PerfOptions.SysCPU { + perfOptions = append(perfOptions, giDevice.WithPerfSystemCPU(true)) + } + if dev.PerfOptions.SysMem { + perfOptions = append(perfOptions, giDevice.WithPerfSystemMem(true)) + } + if dev.PerfOptions.SysDisk { + perfOptions = append(perfOptions, giDevice.WithPerfSystemDisk(true)) + } + if dev.PerfOptions.SysNetwork { + perfOptions = append(perfOptions, giDevice.WithPerfSystemNetwork(true)) + } + if dev.PerfOptions.FPS { + perfOptions = append(perfOptions, giDevice.WithPerfFPS(true)) + } + if dev.PerfOptions.Network { + perfOptions = append(perfOptions, giDevice.WithPerfNetwork(true)) + } + + // process + if dev.PerfOptions.BundleID != "" { + perfOptions = append(perfOptions, + giDevice.WithPerfBundleID(dev.PerfOptions.BundleID)) + } + if dev.PerfOptions.Pid != 0 { + perfOptions = append(perfOptions, + giDevice.WithPerfPID(dev.PerfOptions.Pid)) + } + + // config + if dev.PerfOptions.OutputInterval != 0 { + perfOptions = append(perfOptions, + giDevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval)) + } + if dev.PerfOptions.SystemAttributes != nil { + perfOptions = append(perfOptions, + giDevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...)) + } + if dev.PerfOptions.ProcessAttributes != nil { + perfOptions = append(perfOptions, + giDevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...)) + } + return +} + +// NewHTTPDriver creates new remote HTTP client, this will also start a new session. +func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { + localPort, err := getFreePort() + if err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = dev.forward(localPort, dev.Port); err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + localMjpegPort, err := getFreePort() + if err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + + log.Info().Interface("capabilities", capabilities). + Int("localPort", localPort).Int("localMjpegPort", localMjpegPort). + Msg("init WDA HTTP driver") + + wd := new(wdaDriver) + wd.client = http.DefaultClient + + host := "127.0.0.1" + if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) + } + var sessionInfo SessionInfo + if sessionInfo, err = wd.NewSession(capabilities); err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) + } + wd.sessionId = sessionInfo.SessionId + + if wd.mjpegHTTPConn, err = net.Dial( + "tcp", + fmt.Sprintf("%s:%d", host, localMjpegPort), + ); err != nil { + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) + } + wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn) + + return wd, nil +} + +// NewUSBDriver creates new client via USB connected device, this will also start a new session. +func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { + log.Info().Interface("capabilities", capabilities). + Str("udid", dev.UDID).Msg("init WDA USB driver") + + wd := new(wdaDriver) + if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, + fmt.Sprintf("connect port %d failed: %v", dev.Port, err)) + } + wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) + + if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, + fmt.Sprintf("connect MJPEG port %d failed: %v", dev.MjpegPort, err)) + } + wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) + + if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil { + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, err.Error()) + } + if _, err = wd.NewSession(capabilities); err != nil { + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, err.Error()) + } + + return wd, nil +} + +func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { + if httpClient == nil { + return errors.New(`'httpClient' can't be nil`) + } + + var req *http.Request + if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil { + return err + } + + var resp *http.Response + if resp, err = httpClient.Do(req); err != nil { + return err + } + // defer func() { _ = resp.Body.Close() }() + + var boundary string + if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { + return err + } else { + boundary = strings.Trim(param["boundary"], "-") + } + + mjpegReader := multipart.NewReader(resp.Body, boundary) + + go func() { + for { + select { + case <-dExt.doneMjpegStream: + _ = resp.Body.Close() + return + default: + var part *multipart.Part + if part, err = mjpegReader.NextPart(); err != nil { + dExt.frame = nil + continue + } + + raw := new(bytes.Buffer) + if _, err = raw.ReadFrom(part); err != nil { + dExt.frame = nil + continue + } + dExt.frame = raw + } + } + }() + + return +} + +func (dExt *DriverExt) CloseMjpegStream() { + dExt.doneMjpegStream <- true +} + +type rawResponse []byte + +func (r rawResponse) checkErr() (err error) { + reply := new(struct { + Value struct { + Err string `json:"error"` + Message string `json:"message"` + Traceback string `json:"traceback"` // wda + Stacktrace string `json:"stacktrace"` // uia + } + }) + if err = json.Unmarshal(r, reply); err != nil { + return err + } + if reply.Value.Err != "" { + errText := reply.Value.Message + re := regexp.MustCompile(`{.+?=(.+?)}`) + if re.MatchString(reply.Value.Message) { + subMatch := re.FindStringSubmatch(reply.Value.Message) + errText = subMatch[len(subMatch)-1] + } + return fmt.Errorf("%s: %s", reply.Value.Err, errText) + } + return +} + +func (r rawResponse) valueConvertToString() (s string, err error) { + reply := new(struct{ Value string }) + if err = json.Unmarshal(r, reply); err != nil { + return "", errors.Wrapf(err, "json.Unmarshal failed, rawResponse: %s", string(r)) + } + s = reply.Value + return +} + +func (r rawResponse) valueConvertToBool() (b bool, err error) { + reply := new(struct{ Value bool }) + if err = json.Unmarshal(r, reply); err != nil { + return false, err + } + b = reply.Value + return +} + +func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) { + reply := new(struct{ Value struct{ SessionInfo } }) + if err = json.Unmarshal(r, reply); err != nil { + return SessionInfo{}, err + } + sessionInfo = reply.Value.SessionInfo + return +} + +func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) { + reply := new(struct{ Value builtinJSON.RawMessage }) + if err = json.Unmarshal(r, reply); err != nil { + return nil, err + } + raw = reply.Value + return +} + +func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) { + str, err := r.valueConvertToString() + if err != nil { + return nil, errors.Wrap(err, "failed to convert value to string") + } + decodeString, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, errors.Wrap(err, "failed to decode base64 string") + } + raw = bytes.NewBuffer(decodeString) + return +} + +var errNoSuchElement = errors.New("no such element") + +func (r rawResponse) valueConvertToElementID() (id string, err error) { + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(r, reply); err != nil { + return "", err + } + if len(reply.Value) == 0 { + return "", errNoSuchElement + } + if id = elementIDFromValue(reply.Value); id == "" { + return "", fmt.Errorf("invalid element returned: %+v", reply) + } + return +} + +func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) { + reply := new(struct{ Value []map[string]string }) + if err = json.Unmarshal(r, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, errNoSuchElement + } + IDs = make([]string, len(reply.Value)) + for i, elem := range reply.Value { + var id string + if id = elementIDFromValue(elem); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + IDs[i] = id + } + return +} diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go new file mode 100644 index 00000000..72da3928 --- /dev/null +++ b/hrp/pkg/uixt/ios_driver.go @@ -0,0 +1,890 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + builtinJSON "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +type wdaDriver struct { + Driver + + // default port + defaultConn giDevice.InnerConn + + // mjpeg port + mjpegUSBConn giDevice.InnerConn // via USB + mjpegHTTPConn net.Conn // via HTTP + mjpegClient *http.Client +} + +func (wd *wdaDriver) GetMjpegClient() *http.Client { + return wd.mjpegClient +} + +func (wd *wdaDriver) Close() error { + if wd.defaultConn != nil { + wd.defaultConn.Close() + } + if wd.mjpegUSBConn != nil { + wd.mjpegUSBConn.Close() + } + + if wd.mjpegClient != nil { + wd.mjpegClient.CloseIdleConnections() + } + return wd.mjpegHTTPConn.Close() +} + +func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { + // [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)] + data := make(map[string]interface{}) + if len(capabilities) == 0 { + data["capabilities"] = make(map[string]interface{}) + } else { + data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities} + } + + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session"); err != nil { + return SessionInfo{}, err + } + if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { + return SessionInfo{}, err + } + wd.sessionId = sessionInfo.SessionId + return +} + +func (wd *wdaDriver) ActiveSession() (sessionInfo SessionInfo, err error) { + // [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId); err != nil { + return SessionInfo{}, err + } + if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { + return SessionInfo{}, err + } + return +} + +func (wd *wdaDriver) DeleteSession() (err error) { + // [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)] + _, err = wd.httpDELETE("/session", wd.sessionId) + return +} + +func (wd *wdaDriver) Status() (deviceStatus DeviceStatus, err error) { + // [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/status"); err != nil { + return DeviceStatus{}, err + } + reply := new(struct{ Value struct{ DeviceStatus } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceStatus{}, err + } + deviceStatus = reply.Value.DeviceStatus + return +} + +func (wd *wdaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { + // [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)] + // [[FBRoute GET:@"/wda/device/info"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/info"); err != nil { + return DeviceInfo{}, err + } + reply := new(struct{ Value struct{ DeviceInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceInfo{}, err + } + deviceInfo = reply.Value.DeviceInfo + return +} + +func (wd *wdaDriver) Location() (location Location, err error) { + // [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)] + // [[FBRoute GET:@"/wda/device/location"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/location"); err != nil { + return Location{}, err + } + reply := new(struct{ Value struct{ Location } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Location{}, err + } + location = reply.Value.Location + return +} + +func (wd *wdaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { + // [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/batteryInfo"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value struct{ BatteryInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + batteryInfo = reply.Value.BatteryInfo + return +} + +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 + } + reply := new(struct{ Value struct{ Size } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + size = reply.Value.Size + return +} + +func (wd *wdaDriver) Screen() (screen Screen, err error) { + // [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/screen"); err != nil { + return Screen{}, err + } + reply := new(struct{ Value struct{ Screen } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Screen{}, err + } + screen = reply.Value.Screen + return +} + +func (wd *wdaDriver) Scale() (float64, error) { + screen, err := wd.Screen() + if err != nil { + return 0, err + } + return screen.Scale, nil +} + +func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) { + // [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)] + // [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil { + return AppInfo{}, err + } + reply := new(struct{ Value struct{ AppInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return AppInfo{}, err + } + info = reply.Value.AppInfo + return +} + +func (wd *wdaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { + // [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/apps/list"); err != nil { + return nil, err + } + reply := new(struct{ Value []AppBaseInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + appsList = reply.Value + return +} + +func (wd *wdaDriver) AppState(bundleId string) (runState AppState, err error) { + // [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)] + data := map[string]interface{}{"bundleId": bundleId} + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil { + return 0, err + } + reply := new(struct{ Value AppState }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + runState = reply.Value + _ = rawResp + return +} + +func (wd *wdaDriver) IsLocked() (locked bool, err error) { + // [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)] + // [[FBRoute GET:@"/wda/locked"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/locked"); err != nil { + return false, err + } + if locked, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (wd *wdaDriver) Unlock() (err error) { + // [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)] + // [[FBRoute POST:@"/wda/unlock"].withoutSession + _, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/unlock") + return +} + +func (wd *wdaDriver) Lock() (err error) { + // [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)] + // [[FBRoute POST:@"/wda/lock"].withoutSession + _, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/lock") + return +} + +func (wd *wdaDriver) Homescreen() (err error) { + // [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)] + _, err = wd.httpPOST(nil, "/wda/homescreen") + return +} + +func (wd *wdaDriver) AlertText() (text string, err error) { + // [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)] + // [[FBRoute GET:@"/alert/text"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/alert/text"); err != nil { + return "", err + } + if text, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (wd *wdaDriver) AlertButtons() (btnLabels []string, err error) { + // [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/alert/buttons"); err != nil { + return nil, err + } + reply := new(struct{ Value []string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + btnLabels = reply.Value + return +} + +func (wd *wdaDriver) AlertAccept(label ...string) (err error) { + // [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)] + // [[FBRoute POST:@"/alert/accept"].withoutSession + data := make(map[string]interface{}) + if len(label) != 0 && label[0] != "" { + data["name"] = label[0] + } + _, err = wd.httpPOST(data, "/alert/accept") + return +} + +func (wd *wdaDriver) AlertDismiss(label ...string) (err error) { + // [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)] + // [[FBRoute POST:@"/alert/dismiss"].withoutSession + data := make(map[string]interface{}) + if len(label) != 0 && label[0] != "" { + data["name"] = label[0] + } + _, err = wd.httpPOST(data, "/alert/dismiss") + return +} + +func (wd *wdaDriver) AlertSendKeys(text string) (err error) { + // [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)] + data := map[string]interface{}{"value": strings.Split(text, "")} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/alert/text") + return +} + +func (wd *wdaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { + // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] + data := make(map[string]interface{}) + if len(launchOpt) != 0 { + data = launchOpt[0] + } + data["bundleId"] = bundleId + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch") + return +} + +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 +} + +func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) { + // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] + data := map[string]interface{}{"bundleId": bundleId} + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil { + return false, err + } + if successful, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (wd *wdaDriver) AppActivate(bundleId string) (err error) { + // [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)] + data := map[string]interface{}{"bundleId": bundleId} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/activate") + return +} + +func (wd *wdaDriver) AppDeactivate(second float64) (err error) { + // [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)] + if second < 3 { + second = 3.0 + } + data := map[string]interface{}{"duration": second} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/deactivateApp") + return +} + +func (wd *wdaDriver) AppAuthReset(resource ProtectedResource) (err error) { + // [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)] + data := map[string]interface{}{"resource": resource} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/resetAppAuth") + return +} + +func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error { + return wd.TapFloat(float64(x), float64(y), options...) +} + +func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { + // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + // new data options in post data for extra WDA configurations + d := NewData(data, options...) + + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/tap/0") + return +} + +func (wd *wdaDriver) DoubleTap(x, y int) error { + return wd.DoubleTapFloat(float64(x), float64(y)) +} + +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, + } + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/doubleTap") + return +} + +func (wd *wdaDriver) TouchAndHold(x, y int, second ...float64) error { + return wd.TouchAndHoldFloat(float64(x), float64(y), second...) +} + +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, + } + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["duration"] = second[0] + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touchAndHold") + return +} + +func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) 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) { + // [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)] + data := map[string]interface{}{ + "fromX": fromX, + "fromY": fromY, + "toX": toX, + "toY": toY, + } + + // new data options in post data for extra WDA configurations + d := NewData(data, options...) + + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/dragfromtoforduration") + return +} + +func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { + options = append(options, WithDataPressDuration(0)) + return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { + options = append(options, WithDataPressDuration(0)) + return wd.DragFloat(fromX, fromY, toX, toY, options...) +} + +func (wd *wdaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error { + return wd.ForceTouchFloat(float64(x), float64(y), pressure, second...) +} + +func (wd *wdaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) error { + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + actions := NewTouchActions(). + Press( + NewTouchActionPress().WithXYFloat(x, y).WithPressure(pressure)). + Wait(second[0]). + Release() + return wd.PerformAppiumTouchActions(actions) +} + +func (wd *wdaDriver) PerformW3CActions(actions *W3CActions) (err error) { + // [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)] + data := map[string]interface{}{"actions": actions} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/actions") + return +} + +func (wd *wdaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { + // [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)] + // [[FBRoute POST:@"/wda/touch/multi/perform"] + data := map[string]interface{}{"actions": touchActs} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch/multi/perform") + return +} + +func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { + // [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)] + data := map[string]interface{}{ + "contentType": contentType, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + } + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/setPasteboard") + return +} + +func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { + // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] + data := map[string]interface{}{"contentType": contentType} + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil { + return nil, err + } + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, err + } + return +} + +func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (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 + d := NewData(data, options...) + + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/keys") + return +} + +func (wd *wdaDriver) Input(text string, options ...DataOption) (err error) { + return wd.SendKeys(text, options...) +} + +func (wd *wdaDriver) KeyboardDismiss(keyNames ...string) (err error) { + // [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)] + if len(keyNames) == 0 { + keyNames = []string{"return"} + } + data := map[string]interface{}{"keyNames": keyNames} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keyboard/dismiss") + return +} + +func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) { + // [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)] + data := map[string]interface{}{"name": devBtn} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/pressButton") + return +} + +func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { + // [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)] + if len(duration) == 0 || duration[0] <= 0 { + duration = []float64{0.005} + } + data := map[string]interface{}{ + "page": pageID, + "usage": usageID, + "duration": duration[0], + } + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/performIoHidEvent") + return +} + +func (wd *wdaDriver) StartCamera() (err error) { + // start camera, alias for app_launch com.apple.camera + return wd.AppLaunch("com.apple.camera") +} + +func (wd *wdaDriver) StopCamera() (err error) { + // stop camera, alias for app_terminate com.apple.camera + success, err := wd.AppTerminate("com.apple.camera") + if err != nil { + return errors.Wrap(err, "failed to terminate camera") + } + if !success { + log.Warn().Msg("camera was not running") + } + return nil +} + +func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { + // [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)] + if len(second) == 0 { + second = []int{60} + } + data := map[string]interface{}{ + "name": notifyName, + "type": notifyType, + "timeout": second[0], + } + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/expectNotification") + return +} + +func (wd *wdaDriver) SiriActivate(text string) (err error) { + // [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)] + data := map[string]interface{}{"text": text} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/siri/activate") + return +} + +func (wd *wdaDriver) SiriOpenUrl(url string) (err error) { + // [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)] + data := map[string]interface{}{"url": url} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/url") + return +} + +func (wd *wdaDriver) Orientation() (orientation Orientation, err error) { + // [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/orientation"); err != nil { + return "", err + } + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + orientation = reply.Value + return +} + +func (wd *wdaDriver) SetOrientation(orientation Orientation) (err error) { + // [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)] + data := map[string]interface{}{"orientation": orientation} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/orientation") + return +} + +func (wd *wdaDriver) Rotation() (rotation Rotation, err error) { + // [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/rotation"); err != nil { + return Rotation{}, err + } + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + rotation = reply.Value + return +} + +func (wd *wdaDriver) SetRotation(rotation Rotation) (err error) { + // [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)] + _, err = wd.httpPOST(rotation, "/session", wd.sessionId, "/rotation") + return +} + +func (wd *wdaDriver) MatchTouchID(isMatch bool) (err error) { + // [FBRoute POST:@"/wda/touch_id"] + data := map[string]interface{}{"match": isMatch} + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch_id") + return +} + +func (wd *wdaDriver) ActiveElement() (element WebElement, err error) { + // [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/element/active"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + return nil, err + } + element = &wdaElement{parent: wd, id: elementID} + return +} + +func (wd *wdaDriver) FindElement(by BySelector) (element WebElement, err error) { + // [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/element"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + element = &wdaElement{parent: wd, id: elementID} + return +} + +func (wd *wdaDriver) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/elements"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &wdaElement{parent: wd, id: elementIDs[i]} + } + return +} + +func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) { + // [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)] + // [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/screenshot"); err != nil { + return nil, errors.Wrap(code.IOSScreenShotError, + fmt.Sprintf("get WDA screenshot data failed: %v", err)) + } + + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, errors.Wrap(code.IOSScreenShotError, + fmt.Sprintf("decode WDA screenshot data failed: %v", err)) + } + return +} + +func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) { + // [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)] + // [[FBRoute GET:@"/source"].withoutSession + tmp, _ := url.Parse(wd.concatURL(nil, "/session", wd.sessionId)) + toJsonRaw := false + if len(srcOpt) != 0 { + q := tmp.Query() + for k, val := range srcOpt[0] { + v := val.(string) + q.Set(k, v) + if k == "format" && v == "json" { + toJsonRaw = true + } + } + tmp.RawQuery = q.Encode() + } + + var rawResp rawResponse + if rawResp, err = wd.httpRequest(http.MethodGet, wd.concatURL(tmp, "/source"), nil); err != nil { + return "", nil + } + if toJsonRaw { + var jr builtinJSON.RawMessage + if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil { + return "", err + } + return string(jr), nil + } + if source, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (wd *wdaDriver) AccessibleSource() (source string, err error) { + // [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)] + // [[FBRoute GET:@"/wda/accessibleSource"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/accessibleSource"); err != nil { + return "", err + } + var jr builtinJSON.RawMessage + if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil { + return "", err + } + source = string(jr) + return +} + +func (wd *wdaDriver) HealthCheck() (err error) { + // [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)] + _, err = wd.httpGET("/wda/healthcheck") + return +} + +func (wd *wdaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { + // [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)] + var rawResp rawResponse + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + settings = reply.Value + return +} + +func (wd *wdaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { + // [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)] + data := map[string]interface{}{"settings": settings} + var rawResp rawResponse + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + ret = reply.Value + return +} + +func (wd *wdaDriver) IsHealthy() (healthy bool, err error) { + var rawResp rawResponse + if rawResp, err = wd.httpGET("/health"); err != nil { + return false, err + } + if string(rawResp) != "I-AM-ALIVE" { + return false, nil + } + return true, nil +} + +func (wd *wdaDriver) WdaShutdown() (err error) { + _, err = wd.httpGET("/wda/shutdown") + return +} + +func (wd *wdaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { + startTime := time.Now() + for { + done, err := condition(wd) + if err != nil { + return err + } + if done { + return nil + } + + if elapsed := time.Since(startTime); elapsed > timeout { + return fmt.Errorf("timeout after %v", elapsed) + } + time.Sleep(interval) + } +} + +func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error { + return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) +} + +func (wd *wdaDriver) Wait(condition Condition) error { + return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +} + +func (wd *wdaDriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, err error) { + // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] + return wd.httpPOST(data, "/gtf/automation/log") +} + +func (wd *wdaDriver) StartCaptureLog(identifier ...string) error { + log.Info().Msg("start WDA log recording") + if identifier == nil { + identifier = []string{""} + } + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier[0]} + _, err := wd.triggerWDALog(data) + if err != nil { + return errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("start WDA log recording failed: %v", err)) + } + + return nil +} + +type wdaResponse struct { + Value interface{} `json:"value"` + SessionID string `json:"sessionId"` +} + +func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { + log.Info().Msg("stop log recording") + data := map[string]interface{}{"action": "stop"} + rawResp, err := wd.triggerWDALog(data) + if err != nil { + log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to get WDA logs") + return "", errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("get WDA logs failed: %v", err)) + } + reply := new(wdaResponse) + if err = json.Unmarshal(rawResp, reply); err != nil { + log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to json.Unmarshal WDA logs") + return reply, errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("json.Unmarshal WDA logs failed: %v", err)) + } + log.Info().Interface("value", reply.Value).Msg("get WDA log response") + return reply.Value, nil +} diff --git a/hrp/pkg/uixt/ios_element.go b/hrp/pkg/uixt/ios_element.go new file mode 100644 index 00000000..9e2208b5 --- /dev/null +++ b/hrp/pkg/uixt/ios_element.go @@ -0,0 +1,478 @@ +package uixt + +import ( + "bytes" + "fmt" + "math" + "strings" + + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +// All elements returned by search endpoints have assigned element_id. +// Given element_id you can query properties like: +// enabled, rect, size, location, text, displayed, accessible, name +type wdaElement struct { + parent *wdaDriver + id string // element_id +} + +func (we wdaElement) Click() (err error) { + // [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)] + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/click") + return +} + +func (we wdaElement) SendKeys(text string, options ...DataOption) (err error) { + // [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)] + data := map[string]interface{}{ + "value": strings.Split(text, ""), + } + // new data options in post data for extra uiautomator configurations + d := NewData(data, options...) + + _, err = we.parent.httpPOST(d.Data, "/session", we.parent.sessionId, "/element", we.id, "/value") + return +} + +func (we wdaElement) Clear() (err error) { + // [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)] + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear") + return +} + +func (we wdaElement) Tap(x, y int) error { + return we.TapFloat(float64(x), float64(y)) +} + +func (we wdaElement) TapFloat(x, y float64) (err error) { + // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/tap/", we.id) + return +} + +func (we wdaElement) DoubleTap() (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)] + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap") + return +} + +func (we wdaElement) TouchAndHold(second ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)] + data := make(map[string]interface{}) + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["duration"] = second[0] + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold") + return +} + +func (we wdaElement) TwoFingerTap() (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)] + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap") + return +} + +func (we wdaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)] + if numberOfTouches <= 0 { + return errors.New("'numberOfTouches' must be greater than zero") + } + if numberOfTouches > 5 { + return errors.New("'numberOfTouches' cannot be greater than 5") + } + if numberOfTaps <= 0 { + return errors.New("'numberOfTaps' must be greater than zero") + } + if numberOfTaps > 10 { + return errors.New("'numberOfTaps' cannot be greater than 10") + } + data := map[string]interface{}{ + "numberOfTaps": numberOfTaps, + "numberOfTouches": numberOfTouches, + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps") + return +} + +func (we wdaElement) ForceTouch(pressure float64, second ...float64) (err error) { + return we.ForceTouchFloat(-1, -1, pressure, second...) +} + +func (we wdaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)] + data := make(map[string]interface{}) + if x != -1 && y != -1 { + data["x"] = x + data["y"] = y + } + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["pressure"] = pressure + data["duration"] = second[0] + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch") + return +} + +func (we wdaElement) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error { + return we.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), pressForDuration...) +} + +func (we wdaElement) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)] + data := map[string]interface{}{ + "fromX": fromX, + "fromY": fromY, + "toX": toX, + "toY": toY, + } + if len(pressForDuration) == 0 || pressForDuration[0] < 0 { + pressForDuration = []float64{1.0} + } + data["duration"] = pressForDuration[0] + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration") + return +} + +func (we wdaElement) Swipe(fromX, fromY, toX, toY int) error { + return we.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY)) +} + +func (we wdaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { + return we.DragFloat(fromX, fromY, toX, toY, 0) +} + +func (we wdaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)] + data := map[string]interface{}{"direction": direction} + if len(velocity) != 0 && velocity[0] > 0 { + data["velocity"] = velocity[0] + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe") + return +} + +func (we wdaElement) Pinch(scale, velocity float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)] + if scale <= 0 { + return errors.New("'scale' must be greater than zero") + } + if scale == 1 { + return errors.New("'scale' must be greater or less than 1") + } + if scale < 1 && velocity > 0 { + return errors.New("'velocity' must be less than zero when 'scale' is less than 1") + } + if scale > 1 && velocity <= 0 { + return errors.New("'velocity' must be greater than zero when 'scale' is greater than 1") + } + data := map[string]interface{}{ + "scale": scale, + "velocity": velocity, + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch") + return +} + +func (we wdaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) { + if len(scale) == 0 { + scale = []float64{1.0} + } else if scale[0] > 23 { + scale[0] = 23 + } + var size Size + if size, err = we.Size(); err != nil { + return err + } + r := scale[0] * 2 / 100.0 + offsetX, offsetY := float64(size.Width)*r, float64(size.Height)*r + + actions := NewW3CActions().SwipeFloat(0-offsetX, 0-offsetY, 0, 0, we).SwipeFloat(offsetX, offsetY, 0, 0, we) + return we.parent.PerformW3CActions(actions) +} + +func (we wdaElement) Rotate(rotation float64, velocity ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)] + if rotation > math.Pi*2 || rotation < math.Pi*-2 { + return errors.New("'rotation' must not be more than 2π or less than -2π") + } + if len(velocity) == 0 || velocity[0] == 0 { + velocity = []float64{rotation} + } + if rotation > 0 && velocity[0] < 0 || rotation < 0 && velocity[0] > 0 { + return errors.New("'rotation' and 'velocity' must have the same sign") + } + data := map[string]interface{}{ + "rotation": rotation, + "velocity": velocity[0], + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate") + return +} + +func (we wdaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { + // [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)] + if len(offset) == 0 { + offset = []int{2} + } else if offset[0] <= 0 || offset[0] > 5 { + return fmt.Errorf("'offset' value is expected to be in range (0, 5]. '%d' was given instead", offset[0]) + } + data := map[string]interface{}{ + "order": order, + "offset": float64(offset[0]) * 0.1, + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select") + return +} + +func (we wdaElement) scroll(data interface{}) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)] + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll") + return +} + +func (we wdaElement) ScrollElementByName(name string) error { + data := map[string]interface{}{"name": name} + return we.scroll(data) +} + +func (we wdaElement) ScrollElementByPredicate(predicate string) error { + data := map[string]interface{}{"predicateString": predicate} + return we.scroll(data) +} + +func (we wdaElement) ScrollToVisible() error { + data := map[string]interface{}{"toVisible": true} + return we.scroll(data) +} + +func (we wdaElement) ScrollDirection(direction Direction, distance ...float64) error { + if len(distance) == 0 || distance[0] <= 0 { + distance = []float64{0.5} + } + data := map[string]interface{}{ + "direction": direction, + "distance": distance[0], + } + return we.scroll(data) +} + +func (we wdaElement) FindElement(by BySelector) (element WebElement, err error) { + // [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + element = &wdaElement{parent: we.parent, id: elementID} + return +} + +func (we wdaElement) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]} + } + return +} + +func (we wdaElement) FindVisibleCells() (elements []WebElement, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find a cell element in this element", err) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]} + } + return +} + +func (we wdaElement) Rect() (rect Rect, err error) { + // [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil { + return Rect{}, err + } + reply := new(struct{ Value struct{ Rect } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rect{}, err + } + rect = reply.Value.Rect + return +} + +func (we wdaElement) Location() (Point, error) { + rect, err := we.Rect() + if err != nil { + return Point{}, err + } + return rect.Point, nil +} + +func (we wdaElement) Size() (Size, error) { + rect, err := we.Rect() + if err != nil { + return Size{}, err + } + return rect.Size, nil +} + +func (we wdaElement) Text() (text string, err error) { + // [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil { + return "", err + } + if text, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we wdaElement) Type() (elemType string, err error) { + // [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil { + return "", err + } + if elemType, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we wdaElement) IsEnabled() (enabled bool, err error) { + // [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil { + return false, err + } + if enabled, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we wdaElement) IsDisplayed() (displayed bool, err error) { + // [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil { + return false, err + } + if displayed, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we wdaElement) IsSelected() (selected bool, err error) { + // [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil { + return false, err + } + if selected, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we wdaElement) IsAccessible() (accessible bool, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil { + return false, err + } + if accessible, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we wdaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil { + return false, err + } + if isAccessibilityContainer, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we wdaElement) GetAttribute(attr ElementAttribute) (value string, err error) { + // [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil { + return "", err + } + if value, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we wdaElement) UID() (uid string) { + return we.id +} + +func (we wdaElement) Screenshot() (raw *bytes.Buffer, err error) { + // W3C element screenshot + // [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)] + // JSONWP element screenshot + // [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)] + var rawResp rawResponse + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil { + return nil, err + } + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, err + } + return +} diff --git a/hrp/pkg/uixt/ios_test.go b/hrp/pkg/uixt/ios_test.go new file mode 100644 index 00000000..07377317 --- /dev/null +++ b/hrp/pkg/uixt/ios_test.go @@ -0,0 +1,1174 @@ +//go:build localtest + +package uixt + +import ( + "bytes" + "fmt" + "math" + "testing" + "time" +) + +var ( + bundleId = "com.apple.Preferences" + driver WebDriver +) + +func setup(t *testing.T) { + device, err := NewIOSDevice() + if err != nil { + t.Fatal(err) + } + + driver, err = device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } +} + +func TestViaUSB(t *testing.T) { + setup(t) + t.Log(driver.Status()) +} + +func TestNewIOSDevice(t *testing.T) { + device, _ := NewIOSDevice() + if device != nil { + t.Log(device) + } + + device, _ = NewIOSDevice(WithUDID("xxxx")) + if device != nil { + t.Log(device) + } + + device, _ = NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800)) + if device != nil { + t.Log(device) + } + + device, _ = NewIOSDevice(WithUDID("xxxx"), WithWDAPort(8700), WithWDAMjpegPort(8800)) + if device != nil { + t.Log(device) + } +} + +func TestNewWDAHTTPDriver(t *testing.T) { + device, _ := NewIOSDevice() + var err error + _, err = device.NewHTTPDriver(nil) + if err != nil { + t.Fatal(err) + } +} + +func TestNewUSBDriver(t *testing.T) { + setup(t) + + // t.Log(driver.IsWdaHealthy()) +} + +func Test_remoteWD_NewSession(t *testing.T) { + setup(t) + + // sessionInfo, err := driver.NewSession(nil) + sessionInfo, err := driver.NewSession( + NewCapabilities().WithAppLaunchOption( + NewAppLaunchOption().WithBundleId(bundleId).WithArguments([]string{"-AppleLanguages", "(Russian)"}), + ), + ) + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_ActiveSession(t *testing.T) { + setup(t) + + sessionInfo, err := driver.ActiveSession() + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_DeleteSession(t *testing.T) { + setup(t) + + err := driver.DeleteSession() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_HealthCheck(t *testing.T) { + setup(t) + + err := driver.HealthCheck() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetAppiumSettings(t *testing.T) { + setup(t) + + settings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + t.Log(settings) +} + +func Test_remoteWD_SetAppiumSettings(t *testing.T) { + setup(t) + + const _acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','暂不'}`]" + const _dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" + + key := "acceptAlertButtonSelector" + value := _acceptAlertButtonSelector + + // settings, err := driver.SetAppiumSettings(map[string]interface{}{"dismissAlertButtonSelector": "暂不"}) + settings, err := driver.SetAppiumSettings(map[string]interface{}{key: value}) + if err != nil { + t.Fatal(err) + } + if settings[key] != value { + t.Fatal(settings[key]) + } +} + +func Test_remoteWD_IsWdaHealthy(t *testing.T) { + setup(t) + + healthy, err := driver.IsHealthy() + if err != nil { + t.Fatal(err) + } + if healthy == false { + t.Fatal("healthy =", healthy) + } +} + +// func Test_remoteWD_WdaShutdown(t *testing.T) { +// setup(t) +// +// if err := driver.WdaShutdown(); err != nil { +// t.Fatal(err) +// } +// } + +func Test_remoteWD_Status(t *testing.T) { + setup(t) + + status, err := driver.Status() + if err != nil { + t.Fatal(err) + } + if status.Ready == false { + t.Fatal("deviceStatus =", status) + } +} + +func Test_remoteWD_DeviceInfo(t *testing.T) { + setup(t) + + info, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + if len(info.Model) == 0 { + t.Fatal(info) + } +} + +func Test_remoteWD_BatteryInfo(t *testing.T) { + setup(t) + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal() + } + t.Log(batteryInfo) +} + +func Test_remoteWD_WindowSize(t *testing.T) { + setup(t) + + size, err := driver.WindowSize() + if err != nil { + t.Fatal() + } + t.Log(size) +} + +func Test_remoteWD_Screen(t *testing.T) { + setup(t) + + screen, err := driver.Screen() + if err != nil { + t.Fatal(err) + } + t.Log(screen) +} + +func Test_remoteWD_ActiveAppInfo(t *testing.T) { + setup(t) + + appInfo, err := driver.ActiveAppInfo() + if err != nil { + t.Fatal(err) + } + if len(appInfo.BundleId) == 0 { + t.Fatal(appInfo) + } + t.Log(appInfo) +} + +func Test_remoteWD_ActiveAppsList(t *testing.T) { + setup(t) + + appsList, err := driver.ActiveAppsList() + if err != nil { + t.Fatal(err) + } + if len(appsList) == 0 { + t.Fatal(appsList) + } + t.Log(appsList) +} + +func Test_remoteWD_AppState(t *testing.T) { + setup(t) + + runState, err := driver.AppState(bundleId) + if err != nil { + t.Fatal(err) + } + t.Log(runState) +} + +func Test_remoteWD_IsLocked(t *testing.T) { + setup(t) + + locked, err := driver.IsLocked() + if err != nil { + t.Fatal(err) + } + t.Log(locked) +} + +func Test_remoteWD_Unlock(t *testing.T) { + setup(t) + + err := driver.Unlock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Lock(t *testing.T) { + setup(t) + + err := driver.Lock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertText(t *testing.T) { + setup(t) + + text, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + _ = text + t.Log(text) +} + +func Test_remoteWD_AlertButtons(t *testing.T) { + setup(t) + + btnLabels, err := driver.AlertButtons() + if err != nil { + t.Fatal(err) + } + t.Log(btnLabels) +} + +func Test_remoteWD_AlertAccept(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertAccept() + // err := driver.AlertAccept("") + // err := driver.AlertAccept("好") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertDismiss(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertDismiss() + // err := driver.AlertDismiss("") + // err := driver.AlertDismiss("不允许") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertSendKeys(t *testing.T) { + setup(t) + + err := driver.AlertSendKeys("todo") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Homescreen(t *testing.T) { + setup(t) + + err := driver.Homescreen() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunch(t *testing.T) { + setup(t) + + // bundleId = "com.hustlzp.xcz" + // bundleId = "com.github.stormbreaker.prod" + // bundleId = "com.360buy.jdmobile" + // bundleId = "com.zhihu.ios" + // bundleId = "com.tencent.xin" + // bundleId = "com.jsmcc.ZP7267A6ES" + err := driver.AppLaunch(bundleId) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithShouldWaitForQuiescence(true)) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithArguments([]string{"-AppleLanguages", "(Russian)"})) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunchUnattached(t *testing.T) { + setup(t) + + err := driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppTerminate(t *testing.T) { + setup(t) + + _, err := driver.AppTerminate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppActivate(t *testing.T) { + setup(t) + + err := driver.AppActivate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppDeactivate(t *testing.T) { + setup(t) + + err := driver.AppDeactivate(2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppAuthReset(t *testing.T) { + setup(t) + + err := driver.AppAuthReset(ProtectedResourceCamera) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Tap(t *testing.T) { + setup(t) + + err := driver.Tap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_DoubleTap(t *testing.T) { + setup(t) + + err := driver.DoubleTap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_TouchAndHold(t *testing.T) { + setup(t) + + // err := driver.TouchAndHold(200, 300) + err := driver.TouchAndHold(200, 300, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Drag(t *testing.T) { + setup(t) + + // err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5)) + err := driver.Swipe(200, 300, 200, 500) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ForceTouch(t *testing.T) { + setup(t) + + err := driver.ForceTouch(256, 400, 0.8, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SetPasteboard(t *testing.T) { + setup(t) + + // err := driver.SetPasteboard(PasteboardTypePlaintext, "gwda") + err := driver.SetPasteboard(PasteboardTypeUrl, "Clock-stopwatch://") + // userHomeDir, _ := os.UserHomeDir() + // bytesImg, _ := ioutil.ReadFile(userHomeDir + "/Pictures/IMG_0806.jpg") + // err := driver.SetPasteboard(PasteboardTypeImage, string(bytesImg)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetPasteboard(t *testing.T) { + setup(t) + + var buffer *bytes.Buffer + var err error + + buffer, err = driver.GetPasteboard(PasteboardTypePlaintext) + // buffer, err = driver.GetPasteboard(PasteboardTypeUrl) + if err != nil { + t.Fatal(err) + } + t.Log(buffer.String()) + + // buffer, err = driver.GetPasteboard(PasteboardTypeImage) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // if err = ioutil.WriteFile(userHomeDir+"/Desktop/p1.png", buffer.Bytes(), 0600); err != nil { + // t.Error(err) + // } +} + +func Test_remoteWD_SendKeys(t *testing.T) { + setup(t) + + err := driver.SendKeys("App Store") + // err := driver.SendKeys("App Store", WithFrequency(3)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PressButton(t *testing.T) { + setup(t) + + err := driver.PressButton(DeviceButtonVolumeUp) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonVolumeDown) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonHome) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriActivate(t *testing.T) { + setup(t) + + err := driver.SiriActivate("What's the weather like today") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriOpenUrl(t *testing.T) { + setup(t) + + err := driver.SiriOpenUrl("Prefs:root=Bluetooth") + // err := driver.SiriOpenUrl("Prefs:root=WIFI![]()") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Orientation(t *testing.T) { + setup(t) + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + if orientation == "" { + t.Fatal(orientation) + } +} + +func Test_remoteWD_SetOrientation(t *testing.T) { + setup(t) + + var err error + err = driver.SetOrientation(OrientationLandscapeLeft) + err = driver.SetOrientation(OrientationLandscapeRight) + err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Rotation(t *testing.T) { + setup(t) + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal() + } + t.Log(rotation) +} + +func Test_remoteWD_SetRotation(t *testing.T) { + setup(t) + + err := driver.SetRotation(Rotation{X: 0, Y: 0, Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformW3CActions(t *testing.T) { + // setup(t) + // actions := NewW3CActions().SendKeys("App Store") + + element := setupElement(t, BySelector{Name: "touchableView"}) + actions := NewW3CActions().FingerAction( + NewFingerAction(). + Move(NewFingerMove().WithXY(-15, -85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + NewFingerAction(). + Move(NewFingerMove().WithXY(15, 85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + ) + err := driver.PerformW3CActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformAppiumTouchActions(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + actions := NewTouchActions(). + Press(NewTouchActionPress().WithElement(element).WithXY(100, 150).WithPressure(0.2)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithXY(300, 150)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element).WithXY(300, 400)). + Release() + + err := driver.PerformAppiumTouchActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ActiveElement(t *testing.T) { + setup(t) + + element, err := driver.ActiveElement() + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElement(t *testing.T) { + setup(t) + + element, err := driver.FindElement(BySelector{Name: "设置"}) + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElements(t *testing.T) { + setup(t) + + elements, err := driver.FindElements(BySelector{ClassName: ElementType{Icon: true}}) + if err != nil { + t.Fatal(err) + } + _ = elements + t.Log(elements) +} + +func Test_remoteWD_Screenshot(t *testing.T) { + setup(t) + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} + +func Test_remoteWD_Source(t *testing.T) { + setup(t) + + var source string + var err error + + // source, err = driver.Source() + // if err != nil { + // t.Fatal(err) + // } + + source, err = driver.Source(NewSourceOption().WithScope("AppiumAUT")) + if err != nil { + t.Fatal(err) + } + + // source, err = driver.Source(NewSourceOption().WithFormatAsJson()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsDescription()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsXml().WithExcludedAttributes([]string{"label", "type", "index"})) + // if err != nil { + // t.Fatal(err) + // } + + _ = source + fmt.Println(source) +} + +func Test_remoteWD_AccessibleSource(t *testing.T) { + setup(t) + + source, err := driver.AccessibleSource() + if err != nil { + t.Fatal(err) + } + _ = source + fmt.Println(source) +} + +func Test_remoteWD_Wait(t *testing.T) { + setup(t) + + var element WebElement + var err error + + by := BySelector{Name: "通知"} + // driver.AppLaunch() + exists := func(d WebDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + _ = exists + _ = element + + err = driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } + // element, err = driver.FindElement(by) + err = driver.WaitWithTimeoutAndInterval(exists, time.Second*10, time.Millisecond*10) + if err != nil { + t.Fatal(err) + } + + // t.Log(element.Rect()) +} + +func Test_remoteWD_Location(t *testing.T) { + setup(t) + + location, err := driver.Location() + if err != nil { + t.Fatal(err) + } + t.Log(location) +} + +func Test_remoteWD_KeyboardDismiss(t *testing.T) { + setup(t) + + err := driver.KeyboardDismiss() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ExpectNotification(t *testing.T) { + setup(t) + + // bundleId = "com.apple.shortcuts" + // err := driver.ExpectNotification("shortcuts", NotificationTypePlain, 10) + // if err != nil { + // t.Fatal(err) + // } +} + +func Test_remoteWD_IOHIDEvent(t *testing.T) { + setup(t) + + err := driver.IOHIDEvent(EventPageIDConsumer, EventUsageIDCsmrVolumeDown) + if err != nil { + t.Fatal(err) + } +} + +func setupElement(t *testing.T, by BySelector) WebElement { + setup(t) + element, err := driver.FindElement(by) + if err != nil { + t.Fatal(err) + } + return element +} + +func Test_remoteWE_Click(t *testing.T) { + element := setupElement(t, BySelector{LinkText: NewElementAttribute().WithLabel("设置")}) + + err := element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SendKeys(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.SendKeys("App Store") + // err := element.SendKeys("App Store", 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Clear(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.Clear() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Tap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.Tap(10, 20) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_DoubleTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.DoubleTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TouchAndHold(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TouchAndHold(-1) + // err := element.TouchAndHold(5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TwoFingerTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TwoFingerTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TapWithNumberOfTaps(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TapWithNumberOfTaps(3, 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_ForceTouch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.ForceTouch(1, -1) + err := element.ForceTouchFloat(10, 20, 1, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Drag(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.Drag(10, 20, 10, 300, -1) + err := element.Swipe(10, 20, 10, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SwipeDirection(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.SwipeDirection(DirectionUp, -1) + err := element.SwipeDirection(DirectionDown, 120) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Pinch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // zoom in + // err := element.Pinch(2,10) + // zoom out + err := element.Pinch(0.9, -4.5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PinchToZoomOutByW3CAction(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.PinchToZoomOutByW3CAction(15) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rotate(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // 90 CW + // err := element.Rotate(math.Pi / 2) + // 180 CCW + err := element.Rotate(math.Pi * -2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PickerWheelSelect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{PickerWheel: true}}) + + err := element.PickerWheelSelect(PickerWheelOrderNext, 3) + if err != nil { + t.Fatal(err) + } + err = element.PickerWheelSelect(PickerWheelOrderPrevious) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_scroll(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + // err = element.ScrollElementByName("电池") + // err = element.ScrollElementByPredicate("type == 'XCUIElementTypeCell' AND name LIKE 'Safari*'") + err = element.ScrollDirection(DirectionDown, 0.8) + + // element, err = driver.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + // if err != nil { + // t.Fatal(err) + // } + // err = element.ScrollToVisible() + + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElement(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + element, err = element.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + if err != nil { + t.Fatal(err) + } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElements(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + elements, err := element.FindElements(BySelector{ClassName: ElementType{Cell: true}}) + if err != nil { + t.Fatal(err) + } + + err = elements[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindVisibleCells(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + cells, err := element.FindVisibleCells() + if err != nil { + t.Fatal(err) + } + + err = cells[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + rect, err := element.Rect() + if err != nil { + t.Fatal(err) + } + location, err := element.Location() + if err != nil { + t.Fatal(err) + } + size, err := element.Size() + if err != nil { + t.Fatal(err) + } + _, _, _ = rect, location, size + t.Log(rect, location, size) +} + +func Test_remoteWE_Text(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + text, err := element.Text() + if err != nil { + t.Fatal(err) + } + _ = text + // t.Log(text) +} + +func Test_remoteWE_Type(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + elemType, err := element.Type() + if err != nil { + t.Fatal(err) + } + _ = elemType + // t.Log(elemType) +} + +func Test_remoteWE_IsEnabled(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + enabled, err := element.IsEnabled() + if err != nil { + t.Fatal(err) + } + _ = enabled + // t.Log(enabled) +} + +func Test_remoteWE_IsDisplayed(t *testing.T) { + element := setupElement(t, BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + + displayed, err := element.IsDisplayed() + if err != nil { + t.Fatal(err) + } + _ = displayed + // t.Log(displayed) +} + +func Test_remoteWE_IsSelected(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + // element := setupElement(t, BySelector{Name: "添加到主屏幕"}) + // element := setupElement(t, BySelector{Name: "仅App资源库"}) + + selected, err := element.IsSelected() + if err != nil { + t.Fatal(err) + } + _ = selected + // t.Log(selected) +} + +func Test_remoteWE_IsAccessible(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + accessible, err := element.IsAccessible() + if err != nil { + t.Fatal(err) + } + _ = accessible + // t.Log(accessible) +} + +func Test_remoteWE_IsAccessibilityContainer(t *testing.T) { + // element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + isAccessibilityContainer, err := element.IsAccessibilityContainer() + if err != nil { + t.Fatal(err) + } + _ = isAccessibilityContainer + // t.Log(isAccessibilityContainer) +} + +func Test_remoteWE_GetAttribute(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{StaticText: true}}) + + value, err := element.GetAttribute(NewElementAttribute().WithValue("")) + if err != nil { + t.Fatal(err) + } + _ = value + // t.Log(value) +} + +func Test_remoteWE_Screenshot(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{TextView: true}}) + + screenshot, err := element.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/e1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} diff --git a/hrp/pkg/uixt/ocr_test.go b/hrp/pkg/uixt/ocr_test.go new file mode 100644 index 00000000..da868c27 --- /dev/null +++ b/hrp/pkg/uixt/ocr_test.go @@ -0,0 +1,18 @@ +//go:build ocr + +package uixt + +import ( + "testing" +) + +func TestDriverExtOCR(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + x, y, width, height, err := driverExt.FindTextByOCR("抖音") + 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) +} diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go new file mode 100644 index 00000000..1bae01ad --- /dev/null +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -0,0 +1,324 @@ +package uixt + +import ( + "bytes" + "fmt" + "image" + "io/ioutil" + "mime/multipart" + "net/http" + "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" + "github.com/httprunner/httprunner/v4/hrp/internal/env" + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +var client = &http.Client{ + Timeout: time.Second * 10, +} + +type OCRResult struct { + Text string `json:"text"` + Points []PointF `json:"points"` +} + +type ResponseOCR struct { + Code int `json:"code"` + Message string `json:"message"` + OCRResult []OCRResult `json:"ocrResult"` +} + +type veDEMOCRService struct{} + +func newVEDEMOCRService() (*veDEMOCRService, error) { + if err := checkEnv(); err != nil { + return nil, err + } + return &veDEMOCRService{}, 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 +} + +func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + bodyWriter.WriteField("withDet", "true") + // bodyWriter.WriteField("timestampOnly", "true") + + formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png") + if err != nil { + return nil, errors.Wrap(code.OCRRequestError, + fmt.Sprintf("create form file error: %v", err)) + } + size, err := formWriter.Write(imageBuf) + if err != nil { + return nil, errors.Wrap(code.OCRRequestError, + fmt.Sprintf("write form error: %v", err)) + } + + err = bodyWriter.Close() + if err != nil { + return nil, errors.Wrap(code.OCRRequestError, + fmt.Sprintf("close body writer error: %v", err)) + } + + req, err := http.NewRequest("POST", env.VEDEM_OCR_URL, bodyBuf) + if err != nil { + return nil, errors.Wrap(code.OCRRequestError, + fmt.Sprintf("construct request error: %v", err)) + } + + token := builtin.Sign("auth-v2", env.VEDEM_OCR_AK, env.VEDEM_OCR_SK, bodyBuf.Bytes()) + req.Header.Add("Agw-Auth", token) + req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) + + var resp *http.Response + // retry 3 times + for i := 1; i <= 3; i++ { + resp, err = client.Do(req) + if err == nil { + break + } + + var logID string + if resp != nil { + logID = getLogID(resp.Header) + } + log.Error().Err(err). + Str("logID", logID). + Int("imageBufSize", size). + Msgf("request OCR service failed, retry %d", i) + time.Sleep(1 * time.Second) + } + if resp == nil { + return nil, code.OCRServiceConnectionError + } + + defer resp.Body.Close() + + results, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(code.OCRResponseError, + fmt.Sprintf("read response body error: %v", err)) + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.Wrap(code.OCRResponseError, + fmt.Sprintf("unexpected response status code: %d, results: %v", + resp.StatusCode, string(results))) + } + + var ocrResult ResponseOCR + err = json.Unmarshal(results, &ocrResult) + if err != nil { + return nil, errors.Wrap(code.OCRResponseError, + fmt.Sprintf("json unmarshal response body error: %v", err)) + } + + return ocrResult.OCRResult, nil +} + +func getLogID(header http.Header) string { + if len(header) == 0 { + return "" + } + + logID, ok := header["X-Tt-Logid"] + if !ok || len(logID) == 0 { + return "" + } + return logID[0] +} + +func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...DataOption) (rect image.Rectangle, err error) { + data := NewData(map[string]interface{}{}, options...) + + ocrResults, err := s.getOCRResult(imageBuf) + if err != nil { + log.Error().Err(err).Msg("getOCRResult failed") + return + } + + var rects []image.Rectangle + var ocrTexts []string + 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), + }, + } + if rect.Min.X > data.Scope[0] && rect.Max.X < data.Scope[2] && rect.Min.Y > data.Scope[1] && rect.Max.Y < data.Scope[3] { + ocrTexts = append(ocrTexts, ocrResult.Text) + + // not contains text + if !strings.Contains(ocrResult.Text, text) { + continue + } + + rects = append(rects, rect) + } + + // contains text while not match exactly + if ocrResult.Text != text { + continue + } + + // match exactly, and not specify index, return the first one + if data.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)) + } + + // get index + idx := data.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 []byte, options ...DataOption) (rects []image.Rectangle, err error) { + ocrResults, err := s.getOCRResult(imageBuf) + if err != nil { + log.Error().Err(err).Msg("getOCRResult failed") + return + } + + data := NewData(map[string]interface{}{}, options...) + ocrTexts := map[string]bool{} + + var success bool + var rect image.Rectangle + for _, text := range texts { + var found bool + 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), + }, + } + + if rect.Min.X >= data.Scope[0] && rect.Max.X <= data.Scope[2] && rect.Min.Y >= data.Scope[1] && rect.Max.Y <= data.Scope[3] { + ocrTexts[ocrResult.Text] = true + + // not contains text + if !strings.Contains(ocrResult.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)) + } + + return rects, nil +} + +type OCRService interface { + FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) +} + +func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, y, width, height float64, err error) { + var bufSource *bytes.Buffer + if bufSource, err = dExt.takeScreenShot(); err != nil { + err = fmt.Errorf("takeScreenShot error: %v", err) + return + } + + service, err := newVEDEMOCRService() + if err != nil { + return + } + rect, err := service.FindText(ocrText, bufSource.Bytes(), options...) + if err != nil { + log.Warn().Msgf("FindText failed: %s", err.Error()) + return + } + + log.Info().Str("ocrText", ocrText). + Interface("rect", rect).Msgf("FindTextByOCR success") + x, y, width, height = dExt.MappingToRectInUIKit(rect) + return +} + +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption) (points [][]float64, err error) { + var bufSource *bytes.Buffer + if bufSource, err = dExt.takeScreenShot(); err != nil { + err = fmt.Errorf("takeScreenShot error: %v", err) + return + } + + service, err := newVEDEMOCRService() + if err != nil { + return + } + rects, err := service.FindTexts(ocrTexts, bufSource.Bytes(), 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 +} diff --git a/hrp/pkg/uixt/opencv_off.go b/hrp/pkg/uixt/opencv_off.go new file mode 100644 index 00000000..ade2c4cd --- /dev/null +++ b/hrp/pkg/uixt/opencv_off.go @@ -0,0 +1,23 @@ +//go:build !opencv + +package uixt + +import ( + "image" + + "github.com/rs/zerolog/log" +) + +func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) { + return extend(driver) +} + +func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { + log.Fatal().Msg("opencv is not supported") + return +} + +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, err error) { + log.Fatal().Msg("opencv is not supported") + return +} diff --git a/hrp/pkg/uixt/opencv_on.go b/hrp/pkg/uixt/opencv_on.go new file mode 100644 index 00000000..e8b3d407 --- /dev/null +++ b/hrp/pkg/uixt/opencv_on.go @@ -0,0 +1,146 @@ +//go:build opencv + +package uixt + +import ( + "bytes" + "image" + "io/ioutil" + "os" + + cvHelper "github.com/electricbubble/opencv-helper" +) + +const ( + // TmCcoeffNormed maps to TM_CCOEFF_NORMED + TmCcoeffNormed TemplateMatchMode = iota + // TmSqdiff maps to TM_SQDIFF + TmSqdiff + // TmSqdiffNormed maps to TM_SQDIFF_NORMED + TmSqdiffNormed + // TmCcorr maps to TM_CCORR + TmCcorr + // TmCcorrNormed maps to TM_CCORR_NORMED + TmCcorrNormed + // TmCcoeff maps to TM_CCOEFF + TmCcoeff +) + +type DebugMode int + +const ( + // DmOff no output + DmOff DebugMode = iota + // DmEachMatch output matched and mismatched values + DmEachMatch + // DmNotMatch output only values that do not match + DmNotMatch +) + +// Extend 获得扩展后的 Driver, +// 并指定匹配阀值, +// 获取当前设备的 Scale, +// 默认匹配模式为 TmCcoeffNormed, +// 默认关闭 OpenCV 匹配值计算后的输出 +func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) { + dExt, err = extend(driver) + if err != nil { + return nil, err + } + + for _, option := range options { + option(&dExt.CVArgs) + } + + if dExt.threshold == 0 { + dExt.threshold = 0.95 // default threshold + } + if dExt.matchMode == 0 { + dExt.matchMode = TmCcoeffNormed // default match mode + } + cvHelper.Debug(cvHelper.DebugMode(DmOff)) + return +} + +func (dExt *DriverExt) Debug(dm DebugMode) { + cvHelper.Debug(cvHelper.DebugMode(dm)) +} + +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 +} + +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 +} + +// func (sExt *DriverExt) findImgRect(search string) (rect image.Rectangle, err error) { +// pathSource := filepath.Join(sExt.pathname, cvHelper.GenFilename()) +// if err = sExt.driver.ScreenshotToDisk(pathSource); err != nil { +// return image.Rectangle{}, err +// } +// +// if rect, err = cvHelper.FindImageRectFromDisk(pathSource, search, float32(sExt.Threshold), cvHelper.TemplateMatchMode(sExt.MatchMode)); err != nil { +// return image.Rectangle{}, err +// } +// return +// } + +func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { + var bufSource, bufSearch *bytes.Buffer + if bufSearch, err = getBufFromDisk(search); err != nil { + return nil, err + } + if bufSource, err = dExt.takeScreenShot(); err != nil { + return nil, err + } + + if rects, err = cvHelper.FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil { + return nil, err + } + return +} + +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, 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(); err != nil { + return 0, 0, 0, 0, err + } + + var rect image.Rectangle + if rect, err = cvHelper.FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil { + return 0, 0, 0, 0, err + } + + // if rect, err = dExt.findImgRect(search); err != nil { + // return 0, 0, 0, 0, err + // } + x, y, width, height = dExt.MappingToRectInUIKit(rect) + return +} + +func getBufFromDisk(name string) (*bytes.Buffer, error) { + var f *os.File + var err error + if f, err = os.Open(name); err != nil { + return nil, err + } + var all []byte + if all, err = ioutil.ReadAll(f); err != nil { + return nil, err + } + return bytes.NewBuffer(all), nil +} diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go new file mode 100644 index 00000000..9d0089ff --- /dev/null +++ b/hrp/pkg/uixt/swipe.go @@ -0,0 +1,170 @@ +package uixt + +import ( + "fmt" + "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" +) + +func assertRelative(p float64) bool { + return p >= 0 && p <= 1 +} + +// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY] +func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...DataOption) error { + width := dExt.windowSize.Width + height := dExt.windowSize.Height + + if !assertRelative(fromX) || !assertRelative(fromY) || + !assertRelative(toX) || !assertRelative(toY) { + return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1", + fromX, fromY, toX, toY) + } + + fromX = float64(width) * fromX + fromY = float64(height) * fromY + toX = float64(width) * toX + toY = float64(height) * toY + + return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...) +} + +func (dExt *DriverExt) SwipeTo(direction string, options ...DataOption) (err error) { + switch direction { + case "up": + return dExt.SwipeUp(options...) + case "down": + return dExt.SwipeDown(options...) + case "left": + return dExt.SwipeLeft(options...) + case "right": + return dExt.SwipeRight(options...) + } + return fmt.Errorf("unexpected direction: %s", direction) +} + +func (dExt *DriverExt) SwipeUp(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, options...) +} + +func (dExt *DriverExt) SwipeDown(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, options...) +} + +func (dExt *DriverExt) SwipeLeft(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, options...) +} + +func (dExt *DriverExt) SwipeRight(options ...DataOption) (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 { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.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 %s %d times, match condition failed", direction, maxRetryTimes)) +} + +func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.Interval + + for i := 0; i < maxRetryTimes; i++ { + if err := findCondition(dExt); err == nil { + // do action after found + return foundAction(dExt) + } + + if err := findAction(dExt); err != nil { + log.Error().Err(err).Msgf("find action failed") + } + + // wait interval between each findAction + time.Sleep(time.Duration(1000*interval) * time.Millisecond) + } + + return errors.Wrap(code.OCRTextNotFoundError, + 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} + } + + identifierOption := WithDataIdentifier(action.Identifier) + indexOption := WithDataIndex(action.Index) + 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 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) + + var point PointF + findAppAction := func(d *DriverExt) error { + return dExt.SwipeLeft() + } + findAppCondition := func(d *DriverExt) error { + var err error + point, err = d.GetTextXY(appName, scopeOption, indexOption) + return err + } + foundAppAction := func(d *DriverExt) error { + // click app to launch + return d.TapAbsXY(point.X, point.Y-25, identifierOption) + } + + // go to home screen + if err := dExt.Driver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + dExt.SwipeRight() + } + + // swipe next screen until app found + return dExt.LoopUntil(findAppAction, findAppCondition, foundAppAction, maxRetryOption, waitTimeOption) +} diff --git a/hrp/pkg/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go new file mode 100644 index 00000000..22950c44 --- /dev/null +++ b/hrp/pkg/uixt/swipe_test.go @@ -0,0 +1,48 @@ +//go:build localtest + +package uixt + +import ( + "testing" +) + +func TestSwipeUntil(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + 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)) + checkErr(t, err) +} diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go new file mode 100644 index 00000000..1629f137 --- /dev/null +++ b/hrp/pkg/uixt/tap.go @@ -0,0 +1,168 @@ +package uixt + +import ( + "fmt" +) + +func (dExt *DriverExt) TapAbsXY(x, y float64, options ...DataOption) error { + // tap on absolute coordinate [x, y] + return dExt.Driver.TapFloat(x, y, options...) +} + +func (dExt *DriverExt) TapXY(x, y float64, options ...DataOption) 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) + } + + x = x * float64(dExt.windowSize.Width) + y = y * float64(dExt.windowSize.Height) + + 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...) + 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 { + data := NewData(map[string]interface{}{}, options...) + + point, err := dExt.GetTextXY(ocrText, options...) + if err != nil { + if data.IgnoreNotFoundError { + return nil + } + return err + } + + return dExt.TapAbsXY(point.X, point.Y, options...) +} + +func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error { + data := NewData(map[string]interface{}{}, options...) + + point, err := dExt.GetImageXY(imagePath, options...) + if err != nil { + if data.IgnoreNotFoundError { + return nil + } + return err + } + + 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) TapOffset(param string, xOffset, yOffset float64, options ...DataOption) (err error) { + // click on element, find by name attribute + ele, err := dExt.FindUIElement(param) + if err == nil { + return ele.Click() + } + + data := NewData(map[string]interface{}{}, options...) + + x, y, width, height, err := dExt.FindUIRectInUIKit(param, options...) + if err != nil { + if data.IgnoreNotFoundError { + return nil + } + return err + } + + return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, options...) +} + +func (dExt *DriverExt) DoubleTapXY(x, y float64) error { + // double tap on coordinate: [x, y] should be relative + if x > 1 || y > 1 { + return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) + } + + x = x * float64(dExt.windowSize.Width) + y = y * float64(dExt.windowSize.Height) + return dExt.Driver.DoubleTapFloat(x, y) +} + +func (dExt *DriverExt) DoubleTap(param string) (err error) { + return dExt.DoubleTapOffset(param, 0.5, 0.5) +} + +func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64) (err error) { + // click on element, find by name attribute + ele, err := dExt.FindUIElement(param) + if err == nil { + return ele.DoubleTap() + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + return err + } + + return dExt.Driver.DoubleTapFloat(x+width*xOffset, y+height*yOffset) +} + +// TapWithNumber sends one or more taps +func (dExt *DriverExt) TapWithNumber(param string, numberOfTaps int) (err error) { + return dExt.TapWithNumberOffset(param, numberOfTaps, 0.5, 0.5) +} + +func (dExt *DriverExt) TapWithNumberOffset(param string, numberOfTaps int, xOffset, yOffset float64) (err error) { + if numberOfTaps <= 0 { + numberOfTaps = 1 + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + return err + } + + x = x + width*xOffset + y = y + height*yOffset + + touchActions := NewTouchActions().Tap(NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps)) + return dExt.PerformTouchActions(touchActions) +} diff --git a/hrp/pkg/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go new file mode 100644 index 00000000..c5365998 --- /dev/null +++ b/hrp/pkg/uixt/tap_test.go @@ -0,0 +1,51 @@ +//go:build localtest + +package uixt + +import ( + "testing" +) + +var iosDevice *IOSDevice + +func init() { + iosDevice, _ = NewIOSDevice() +} + +func TestDriverExt_TapWithNumber(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" + + err = driverExt.TapWithNumber(pathSearch, 3) + checkErr(t, err) + + err = driverExt.TapWithNumberOffset(pathSearch, 3, 0.5, 0.75) + checkErr(t, err) +} + +func TestDriverExt_TapXY(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + err = driverExt.TapXY(0.4, 0.5) + checkErr(t, err) +} + +func TestDriverExt_TapAbsXY(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + err = driverExt.TapAbsXY(100, 300) + checkErr(t, err) +} + +func TestDriverExt_TapWithOCR(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + // 需要点击文字上方的图标 + err = driverExt.TapOffset("抖音", 0.5, -1) + checkErr(t, err) +} diff --git a/hrp/pkg/uixt/touch.go b/hrp/pkg/uixt/touch.go new file mode 100644 index 00000000..fe455507 --- /dev/null +++ b/hrp/pkg/uixt/touch.go @@ -0,0 +1,33 @@ +package uixt + +func (dExt *DriverExt) ForceTouch(pathname string, pressure float64, duration ...float64) (err error) { + return dExt.ForceTouchOffset(pathname, pressure, 0.5, 0.5, duration...) +} + +func (dExt *DriverExt) ForceTouchOffset(pathname string, pressure, xOffset, yOffset float64, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + return dExt.Driver.ForceTouchFloat(x+width*xOffset, y+height*yOffset, pressure, duration[0]) +} + +func (dExt *DriverExt) TouchAndHold(pathname string, duration ...float64) (err error) { + return dExt.TouchAndHoldOffset(pathname, 0.5, 0.5, duration...) +} + +func (dExt *DriverExt) TouchAndHoldOffset(pathname string, xOffset, yOffset float64, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + return dExt.Driver.TouchAndHoldFloat(x+width*xOffset, y+height*yOffset, duration[0]) +} diff --git a/hrp/pkg/uixt/touch_test.go b/hrp/pkg/uixt/touch_test.go new file mode 100644 index 00000000..aa5515a8 --- /dev/null +++ b/hrp/pkg/uixt/touch_test.go @@ -0,0 +1,39 @@ +//go:build localtest + +package uixt + +import ( + "testing" +) + +func TestDriverExt_ForceTouch(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" + + err = driverExt.ForceTouch(pathSearch, 0.5, 3) + checkErr(t, err) + + // err = driverExt.ForceTouchOffset(pathSearch, 0.5, 0.1, 0.9) + // checkErr(t, err) + + // err = driverExt.ForceTouchOffset(pathSearch, 0.2, 1.1, -1) + // checkErr(t, err) +} + +func TestDriverExt_TouchAndHold(t *testing.T) { + driverExt, err := iosDevice.NewDriver(nil) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" + + // err = driverExt.TouchAndHold(pathSearch) + // checkErr(t, err) + + // err = driverExt.TouchAndHold(pathSearch, 3) + // checkErr(t, err) + + err = driverExt.TouchAndHoldOffset(pathSearch, 0.8, 0.1) + checkErr(t, err) +} diff --git a/hrp/plugin.go b/hrp/plugin.go index c762b6c8..8e8fc875 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -9,9 +9,12 @@ import ( "github.com/httprunner/funplugin" "github.com/httprunner/funplugin/fungo" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) const ( @@ -34,6 +37,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er } pluginPath, err := locatePlugin(path) if err != nil { + log.Warn().Err(err).Str("path", path).Msg("locate plugin failed") return nil, nil } @@ -50,14 +54,14 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er err = BuildPlugin(pluginPath, genPyPluginPath) if err != nil { log.Error().Err(err).Str("path", pluginPath).Msg("build plugin failed") - return nil, nil + return nil, err } pluginPath = genPyPluginPath packages := []string{ fmt.Sprintf("funppy==%s", fungo.Version), } - python3, err := builtin.EnsurePython3Venv(venv, packages...) + python3, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err). Interface("packages", packages). @@ -71,6 +75,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er plugin, err = funplugin.Init(pluginPath, pluginOptions...) if err != nil { log.Error().Err(err).Msgf("init plugin failed: %s", pluginPath) + err = errors.Wrap(code.InitPluginFailed, err.Error()) return } @@ -109,7 +114,6 @@ func locatePlugin(path string) (pluginPath string, err error) { return } - log.Warn().Err(err).Str("path", path).Msg("plugin file not found") return "", fmt.Errorf("plugin file not found") } diff --git a/hrp/response.go b/hrp/response.go index 42c7e9e1..5f28b269 100644 --- a/hrp/response.go +++ b/hrp/response.go @@ -16,6 +16,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) var fieldTags = []string{"proto", "status_code", "headers", "cookies", "body", textExtractorSubRegexp} @@ -272,3 +273,38 @@ func (v *responseObject) searchRegexp(expr string) interface{} { log.Error().Str("expr", expr).Msg("search regexp failed") return expr } + +func validateUI(ud *uixt.DriverExt, iValidators []interface{}) (validateResults []*ValidationResult, err error) { + for _, iValidator := range iValidators { + validator, ok := iValidator.(Validator) + if !ok { + return nil, errors.New("validator type error") + } + + validataResult := &ValidationResult{ + Validator: validator, + CheckResult: "fail", + } + + // parse check value + if !strings.HasPrefix(validator.Check, "ui_") { + validataResult.CheckResult = "skip" + log.Warn().Interface("validator", validator).Msg("skip validator") + validateResults = append(validateResults, validataResult) + continue + } + + expected, ok := validator.Expect.(string) + if !ok { + return nil, errors.New("validator expect should be string") + } + + if !ud.DoValidation(validator.Check, validator.Assert, expected, validator.Message) { + return validateResults, errors.New("step validation failed") + } + + validataResult.CheckResult = "pass" + validateResults = append(validateResults, validataResult) + } + return validateResults, nil +} diff --git a/hrp/runner.go b/hrp/runner.go index cff1091f..63e5b407 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -2,6 +2,7 @@ package hrp import ( "crypto/tls" + _ "embed" "net" "net/http" "net/http/cookiejar" @@ -12,14 +13,16 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/httprunner/funplugin" "github.com/jinzhu/copier" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/net/http2" - "github.com/httprunner/funplugin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/internal/version" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) // Run starts to run API test with default configs. @@ -49,6 +52,7 @@ func NewRunner(t *testing.T) *HRPRunner { Transport: &http2.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, + Jar: jar, // insert response cookies into request Timeout: 120 * time.Second, }, // use default handshake timeout (no timeout limit) here, enable timeout at step level @@ -70,6 +74,7 @@ type HRPRunner struct { 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 } // SetClientTransport configures transport of http client for high concurrency load testing @@ -169,6 +174,8 @@ func (r *HRPRunner) GenHTMLReport() *HRPRunner { // Run starts to execute one or multiple testcases. func (r *HRPRunner) Run(testcases ...ITestCase) error { + log.Info().Str("hrp_version", version.VERSION). + Interface("testcases", testcases).Msg("start running") event := sdk.EventTracking{ Category: "RunAPITests", Action: "hrp run", @@ -200,19 +207,30 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { var runErr error // run testcase one by one for _, testcase := range testCases { - sessionRunner, err := r.NewSessionRunner(testcase) + // each testcase has its own case runner + caseRunner, err := r.NewCaseRunner(testcase) if err != nil { - log.Error().Err(err).Msg("[Run] init session runner failed") + log.Error().Err(err).Msg("[Run] init case runner failed") return err } - for it := sessionRunner.parametersIterator; it.HasNext(); { - err = sessionRunner.Start(it.Next()) - caseSummary := sessionRunner.GetSummary() + // release UI driver session + defer func() { + for _, client := range r.uiClients { + client.Driver.DeleteSession() + } + }() + + for it := caseRunner.parametersIterator; it.HasNext(); { + // case runner can run multiple times with different parameters + // each run has its own session runner + sessionRunner := caseRunner.NewSession() + err1 := sessionRunner.Start(it.Next()) + caseSummary, err2 := sessionRunner.GetSummary() s.appendCaseSummary(caseSummary) - if err != nil { - log.Error().Err(err).Msg("[Run] run testcase failed") - runErr = err + if err1 != nil || err2 != nil { + log.Error().Err(err1).Msg("[Run] run testcase failed") + runErr = err1 break } } @@ -238,23 +256,10 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { return runErr } -// NewSessionRunner creates a new session runner for testcase. -// each testcase has its own session runner -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) (*SessionRunner, error) { - runner, err := r.newCaseRunner(testcase) - if err != nil { - return nil, err - } - - sessionRunner := &SessionRunner{ - testCaseRunner: runner, - } - sessionRunner.resetSession() - return sessionRunner, nil -} - -func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { - runner := &testCaseRunner{ +// NewCaseRunner creates a new case runner for testcase. +// each testcase has its own case runner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) { + caseRunner := &CaseRunner{ testCase: testcase, hrpRunner: r, parser: newParser(), @@ -266,34 +271,31 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { return nil, errors.Wrap(err, "init plugin failed") } if plugin != nil { - runner.parser.plugin = plugin - runner.rootDir = filepath.Dir(plugin.Path()) + caseRunner.parser.plugin = plugin + caseRunner.rootDir = filepath.Dir(plugin.Path()) } // parse testcase config - if err := runner.parseConfig(); err != nil { + if err := caseRunner.parseConfig(); err != nil { return nil, errors.Wrap(err, "parse testcase config failed") } - // init websocket params - initWebSocket(testcase) - // set testcase timeout in seconds - if runner.testCase.Config.Timeout != 0 { - timeout := time.Duration(runner.testCase.Config.Timeout*1000) * time.Millisecond - runner.hrpRunner.SetTimeout(timeout) + if testcase.Config.Timeout != 0 { + timeout := time.Duration(testcase.Config.Timeout*1000) * time.Millisecond + r.SetTimeout(timeout) } // load plugin info to testcase config if plugin != nil { pluginPath, _ := locatePlugin(testcase.Config.Path) - if runner.parsedConfig.PluginSetting == nil { + if caseRunner.parsedConfig.PluginSetting == nil { pluginContent, err := builtin.ReadFile(pluginPath) if err != nil { return nil, err } tp := strings.Split(plugin.Path(), ".") - runner.parsedConfig.PluginSetting = &PluginConfig{ + caseRunner.parsedConfig.PluginSetting = &PluginConfig{ Path: pluginPath, Content: pluginContent, Type: tp[len(tp)-1], @@ -301,20 +303,21 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { } } - return runner, nil + return caseRunner, nil } -type testCaseRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser +type CaseRunner struct { + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + parsedConfig *TConfig parametersIterator *ParametersIterator rootDir string // project root dir } // parseConfig parses testcase config, stores to parsedConfig. -func (r *testCaseRunner) parseConfig() error { +func (r *CaseRunner) parseConfig() error { cfg := r.testCase.Config r.parsedConfig = &TConfig{} @@ -382,15 +385,227 @@ func (r *testCaseRunner) parseConfig() error { } r.parametersIterator = parametersIterator + // init iOS/Android clients + if r.hrpRunner.uiClients == nil { + r.hrpRunner.uiClients = make(map[string]*uixt.DriverExt) + } + for _, iosDeviceConfig := range r.parsedConfig.IOS { + if iosDeviceConfig.UDID != "" { + udid, err := r.parser.ParseString(iosDeviceConfig.UDID, parsedVariables) + if err != nil { + return errors.Wrap(err, "failed to parse ios device udid") + } + iosDeviceConfig.UDID = udid.(string) + } + device, err := uixt.NewIOSDevice(uixt.GetIOSDeviceOptions(iosDeviceConfig)...) + if err != nil { + return errors.Wrap(err, "init iOS device failed") + } + client, err := device.NewDriver(nil) + if err != nil { + return errors.Wrap(err, "init iOS WDA client failed") + } + r.hrpRunner.uiClients[device.UDID] = client + } + for _, androidDeviceConfig := range r.parsedConfig.Android { + if androidDeviceConfig.SerialNumber != "" { + sn, err := r.parser.ParseString(androidDeviceConfig.SerialNumber, parsedVariables) + if err != nil { + return errors.Wrap(err, "failed to parse android device serial") + } + androidDeviceConfig.SerialNumber = sn.(string) + } + device, err := uixt.NewAndroidDevice(uixt.GetAndroidDeviceOptions(androidDeviceConfig)...) + if err != nil { + return errors.Wrap(err, "init iOS device failed") + } + client, err := device.NewDriver(nil) + if err != nil { + return errors.Wrap(err, "init Android UIAutomator client failed") + } + r.hrpRunner.uiClients[device.SerialNumber] = client + } + return nil } // each boomer task initiates a new session // in order to avoid data racing -func (r *testCaseRunner) newSession() *SessionRunner { +func (r *CaseRunner) NewSession() *SessionRunner { sessionRunner := &SessionRunner{ - testCaseRunner: r, + caseRunner: r, } sessionRunner.resetSession() return sessionRunner } + +// SessionRunner is used to run testcase and its steps. +// each testcase has its own SessionRunner instance and share session variables. +type SessionRunner struct { + caseRunner *CaseRunner + sessionVariables map[string]interface{} + // transactions stores transaction timing info. + // key is transaction name, value is map of transaction type and time, e.g. start time and end time. + transactions map[string]map[transactionType]time.Time + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary + wsConnMap map[string]*websocket.Conn // save all websocket connections + pongResponseChan chan string // channel used to receive pong response message + closeResponseChan chan *wsCloseRespObject // channel used to receive close response message +} + +func (r *SessionRunner) resetSession() { + log.Info().Msg("reset session runner") + r.sessionVariables = make(map[string]interface{}) + r.transactions = make(map[string]map[transactionType]time.Time) + r.startTime = time.Now() + r.summary = newSummary() + r.wsConnMap = make(map[string]*websocket.Conn) + r.pongResponseChan = make(chan string, 1) + r.closeResponseChan = make(chan *wsCloseRespObject, 1) +} + +// Start runs the test steps in sequential order. +// givenVars is used for data driven +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { + config := r.caseRunner.testCase.Config + log.Info().Str("testcase", config.Name).Msg("run testcase start") + + // reset session runner + r.resetSession() + + // update config variables with given variables + r.InitWithParameters(givenVars) + + // 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") + + // run step + stepResult, err := step.Run(r) + stepResult.Name = stepName + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + r.summary.Stat.Total += 1 + if stepResult.Success { + r.summary.Stat.Successes += 1 + } else { + r.summary.Stat.Failures += 1 + // update summary result to failed + r.summary.Success = false + } + + // 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", false). + Msg("run step end") + + // check if failfast + if r.caseRunner.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") + } + } + + // close websocket connection after all steps done + defer func() { + for _, wsConn := range r.wsConnMap { + if wsConn != nil { + log.Info().Str("testcase", config.Name).Msg("websocket disconnected") + err := wsConn.Close() + if err != nil { + log.Error().Err(err).Msg("websocket disconnection failed") + } + } + } + }() + + log.Info().Str("testcase", config.Name).Msg("run testcase end") + return nil +} + +// ParseStepVariables merges step variables with config variables and session variables +func (r *SessionRunner) ParseStepVariables(stepVariables map[string]interface{}) (map[string]interface{}, error) { + // override variables + // step variables > session variables (extracted variables from previous steps) + overrideVars := mergeVariables(stepVariables, r.sessionVariables) + // step variables > testcase config variables + overrideVars = mergeVariables(overrideVars, r.caseRunner.parsedConfig.Variables) + + // parse step variables + parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars) + if err != nil { + log.Error().Interface("variables", r.caseRunner.parsedConfig.Variables). + Err(err).Msg("parse step variables failed") + return nil, errors.Wrap(err, "parse step variables failed") + } + return parsedVariables, nil +} + +// InitWithParameters updates session variables with given parameters. +// this is used for data driven +func (r *SessionRunner) InitWithParameters(parameters map[string]interface{}) { + if len(parameters) == 0 { + return + } + + log.Info().Interface("parameters", parameters).Msg("update session variables") + for k, v := range parameters { + r.sessionVariables[k] = v + } +} + +func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { + caseSummary := r.summary + caseSummary.Name = r.caseRunner.parsedConfig.Name + caseSummary.Time.StartAt = r.startTime + caseSummary.Time.Duration = time.Since(r.startTime).Seconds() + exportVars := make(map[string]interface{}) + for _, value := range r.caseRunner.parsedConfig.Export { + exportVars[value] = r.sessionVariables[value] + } + caseSummary.InOut.ExportVars = exportVars + caseSummary.InOut.ConfigVars = r.caseRunner.parsedConfig.Variables + + for uuid, client := range r.caseRunner.hrpRunner.uiClients { + // add WDA/UIA logs to summary + log, err := client.Driver.StopCaptureLog() + if err != nil { + return caseSummary, err + } + logs := map[string]interface{}{ + "uuid": uuid, + "content": log, + } + + // stop performance monitor + logs["performance"] = client.GetPerfData() + + caseSummary.Logs = append(caseSummary.Logs, logs) + } + + return caseSummary, nil +} diff --git a/hrp/runner_test.go b/hrp/runner_test.go index 383cae09..c07cf1f1 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func buildHashicorpGoPlugin() { err := BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin")) if err != nil { log.Error().Err(err).Msg("build hashicorp go plugin failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } } @@ -33,7 +34,7 @@ func buildHashicorpPyPlugin() { err := ioutil.WriteFile(tmpl("debugtalk.py"), src, 0o644) if err != nil { log.Error().Err(err).Msg("copy hashicorp python plugin failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } } diff --git a/hrp/server.go b/hrp/server.go index ceada927..ab2417a6 100644 --- a/hrp/server.go +++ b/hrp/server.go @@ -9,9 +9,10 @@ import ( "net/http" "strings" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" - "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/mitchellh/mapstructure" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) const jsonContentType = "application/json; encoding=utf-8" @@ -134,16 +135,14 @@ type CommonResponseBody struct { ServerStatus } -type APIGetWorkersRequestBody struct { -} +type APIGetWorkersRequestBody struct{} type APIGetWorkersResponseBody struct { ServerStatus Data []boomer.WorkerNode `json:"data"` } -type APIGetMasterRequestBody struct { -} +type APIGetMasterRequestBody struct{} type APIGetMasterResponseBody struct { ServerStatus @@ -204,7 +203,7 @@ func (api *apiHandler) Start(w http.ResponseWriter, r *http.Request) { for k := range req.Other { keys = append(keys, k) } - err = errors.New(fmt.Sprintf("failed to recognize params: %v", keys)) + err = fmt.Errorf("failed to recognize params: %v", keys) return } @@ -258,7 +257,7 @@ func (api *apiHandler) ReBalance(w http.ResponseWriter, r *http.Request) { for k := range req.Other { keys = append(keys, k) } - err = errors.New(fmt.Sprintf("failed to recognize params: %v", keys)) + err = fmt.Errorf("failed to recognize params: %v", keys) return } @@ -370,7 +369,7 @@ func (b *HRPBoomer) StartServer(ctx context.Context, addr string) { } }() - log.Println(fmt.Sprintf("starting HTTP server (%v), please use the API to control master", server.Addr)) + log.Printf("starting HTTP server (%v), please use the API to control master", server.Addr) err := server.ListenAndServe() if err != nil { if err == http.ErrServerClosed { diff --git a/hrp/session.go b/hrp/session.go deleted file mode 100644 index 35192bf4..00000000 --- a/hrp/session.go +++ /dev/null @@ -1,164 +0,0 @@ -package hrp - -import ( - _ "embed" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" -) - -// SessionRunner is used to run testcase and its steps. -// each testcase has its own SessionRunner instance and share session variables. -type SessionRunner struct { - *testCaseRunner - sessionVariables map[string]interface{} - // transactions stores transaction timing info. - // key is transaction name, value is map of transaction type and time, e.g. start time and end time. - transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary - wsConnMap map[string]*websocket.Conn // save all websocket connections - pongResponseChan chan string // channel used to receive pong response message - closeResponseChan chan *wsCloseRespObject // channel used to receive close response message -} - -func (r *SessionRunner) resetSession() { - log.Info().Msg("reset session runner") - r.sessionVariables = make(map[string]interface{}) - r.transactions = make(map[string]map[transactionType]time.Time) - r.startTime = time.Now() - r.summary = newSummary() - r.wsConnMap = make(map[string]*websocket.Conn) - r.pongResponseChan = make(chan string, 1) - r.closeResponseChan = make(chan *wsCloseRespObject, 1) -} - -func (r *SessionRunner) GetParser() *Parser { - return r.parser -} - -func (r *SessionRunner) GetConfig() *TConfig { - return r.parsedConfig -} - -func (r *SessionRunner) HTTPStatOn() bool { - return r.hrpRunner.httpStatOn -} - -func (r *SessionRunner) LogOn() bool { - return r.hrpRunner.requestsLogOn -} - -// Start runs the test steps in sequential order. -// givenVars is used for data driven -func (r *SessionRunner) Start(givenVars map[string]interface{}) error { - config := r.testCase.Config - log.Info().Str("testcase", config.Name).Msg("run testcase start") - - // reset session runner - r.resetSession() - - // update config variables with given variables - r.updateSessionVariables(givenVars) - - // run step in sequential order - for _, step := range r.testCase.TestSteps { - // parse step name - parsedName, err := r.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") - - stepResult, err := step.Run(r) - stepResult.Name = stepName - if err != nil { - log.Error(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", false). - Msg("run step end") - - if r.hrpRunner.failfast { - return errors.Wrap(err, "abort running due to failfast setting") - } - } - - // update extracted variables - for k, v := range stepResult.ExportVars { - r.sessionVariables[k] = v - } - - log.Info(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", stepResult.Success). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") - } - - // close websocket connection after all steps done - defer func() { - for _, wsConn := range r.wsConnMap { - if wsConn != nil { - log.Info().Str("testcase", config.Name).Msg("websocket disconnected") - err := wsConn.Close() - if err != nil { - log.Error().Err(err).Msg("websocket disconnection failed") - } - } - } - }() - - log.Info().Str("testcase", config.Name).Msg("run testcase end") - return nil -} - -// MergeStepVariables merges step variables with config variables and session variables -func (r *SessionRunner) MergeStepVariables(vars map[string]interface{}) (map[string]interface{}, error) { - // override variables - // step variables > session variables (extracted variables from previous steps) - overrideVars := mergeVariables(vars, r.sessionVariables) - // step variables > testcase config variables - overrideVars = mergeVariables(overrideVars, r.parsedConfig.Variables) - - // parse step variables - parsedVariables, err := r.parser.ParseVariables(overrideVars) - if err != nil { - log.Error().Interface("variables", r.parsedConfig.Variables). - Err(err).Msg("parse step variables failed") - return nil, err - } - return parsedVariables, nil -} - -// updateSessionVariables updates session variables with given variables. -// this is used for data driven -func (r *SessionRunner) updateSessionVariables(parameters map[string]interface{}) { - if len(parameters) == 0 { - return - } - - log.Info().Interface("parameters", parameters).Msg("update session variables") - for k, v := range parameters { - r.sessionVariables[k] = v - } -} - -func (r *SessionRunner) GetSummary() *TestCaseSummary { - caseSummary := r.summary - caseSummary.Name = r.parsedConfig.Name - caseSummary.Time.StartAt = r.startTime - caseSummary.Time.Duration = time.Since(r.startTime).Seconds() - exportVars := make(map[string]interface{}) - for _, value := range r.parsedConfig.Export { - exportVars[value] = r.sessionVariables[value] - } - caseSummary.InOut.ExportVars = exportVars - caseSummary.InOut.ConfigVars = r.parsedConfig.Variables - return caseSummary -} diff --git a/hrp/step.go b/hrp/step.go index b4583852..b721faea 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -1,5 +1,11 @@ package hrp +import ( + giDevice "github.com/electricbubble/gidevice" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + type StepType string const ( @@ -10,6 +16,38 @@ const ( stepTypeRendezvous StepType = "rendezvous" stepTypeThinkTime StepType = "thinktime" stepTypeWebSocket StepType = "websocket" + stepTypeAndroid StepType = "android" + stepTypeIOS StepType = "ios" +) + +var ( + WithIdentifier = uixt.WithIdentifier + WithMaxRetryTimes = uixt.WithMaxRetryTimes + WithWaitTime = uixt.WithWaitTime + WithIndex = uixt.WithIndex + WithTimeout = uixt.WithTimeout + WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError + WithText = uixt.WithText + WithID = uixt.WithID + WithDescription = uixt.WithDescription + WithDirection = uixt.WithDirection + WithCustomDirection = uixt.WithCustomDirection + WithScope = uixt.WithScope +) + +var ( + WithPerfSystemCPU = giDevice.WithPerfSystemCPU + WithPerfSystemMem = giDevice.WithPerfSystemMem + WithPerfSystemDisk = giDevice.WithPerfSystemDisk + WithPerfSystemNetwork = giDevice.WithPerfSystemNetwork + WithPerfGPU = giDevice.WithPerfGPU + WithPerfFPS = giDevice.WithPerfFPS + WithPerfNetwork = giDevice.WithPerfNetwork + WithPerfBundleID = giDevice.WithPerfBundleID + WithPerfPID = giDevice.WithPerfPID + WithPerfOutputInterval = giDevice.WithPerfOutputInterval + WithPerfProcessAttributes = giDevice.WithPerfProcessAttributes + WithPerfSystemAttributes = giDevice.WithPerfSystemAttributes ) type StepResult struct { @@ -21,7 +59,7 @@ type StepResult struct { Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables - Attachment string `json:"attachment,omitempty" yaml:"attachment,omitempty"` // step error information + Attachments interface{} `json:"attachments,omitempty" yaml:"attachments,omitempty"` // store extra step information, such as error message or screenshots } // TStep represents teststep data structure. @@ -35,6 +73,8 @@ type TStep struct { Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"` ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocket *WebSocketAction `json:"websocket,omitempty" yaml:"websocket,omitempty"` + Android *MobileStep `json:"android,omitempty" yaml:"android,omitempty"` + IOS *MobileStep `json:"ios,omitempty" yaml:"ios,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` diff --git a/hrp/step_api.go b/hrp/step_api.go index f31e96bd..f6640332 100644 --- a/hrp/step_api.go +++ b/hrp/step_api.go @@ -100,9 +100,6 @@ func (s *StepAPIWithOptionalArgs) Struct() *TStep { func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepResult, err error) { defer func() { - if err != nil { - r.summary.Success = false - } stepResult.StepType = stepTypeAPI }() // extend request with referenced API diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go new file mode 100644 index 00000000..3a0cd143 --- /dev/null +++ b/hrp/step_mobile_ui.go @@ -0,0 +1,647 @@ +package hrp + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +// ios setting options +var ( + WithUDID = uixt.WithUDID + WithWDAPort = uixt.WithWDAPort + WithWDAMjpegPort = uixt.WithWDAMjpegPort + WithLogOn = uixt.WithLogOn + WithResetHomeOnStartup = uixt.WithResetHomeOnStartup + WithSnapshotMaxDepth = uixt.WithSnapshotMaxDepth + WithAcceptAlertButtonSelector = uixt.WithAcceptAlertButtonSelector + WithDismissAlertButtonSelector = uixt.WithDismissAlertButtonSelector + WithPerfOptions = uixt.WithPerfOptions +) + +// android setting options +var ( + WithSerialNumber = uixt.WithSerialNumber + WithAdbIP = uixt.WithAdbIP + WithAdbPort = uixt.WithAdbPort + WithAdbLogOn = uixt.WithAdbLogOn +) + +type MobileStep struct { + Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` + uixt.MobileAction `yaml:",inline"` + Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` +} + +// StepMobile implements IStep interface. +type StepMobile struct { + step *TStep +} + +func (s *StepMobile) mobileStep() *MobileStep { + if s.step.IOS != nil { + return s.step.IOS + } + return s.step.Android +} + +func (s *StepMobile) Serial(serial string) *StepMobile { + s.mobileStep().Serial = serial + return &StepMobile{step: s.step} +} + +func (s *StepMobile) InstallApp(path string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.AppInstall, + Params: path, + }) + return s +} + +func (s *StepMobile) AppLaunch(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.AppLaunch, + Params: bundleId, + }) + return s +} + +func (s *StepMobile) AppLaunchUnattached(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.AppLaunchUnattached, + Params: bundleId, + }) + return s +} + +func (s *StepMobile) AppTerminate(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.AppTerminate, + Params: bundleId, + }) + return s +} + +func (s *StepMobile) Home() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.ACTION_Home, + Params: nil, + }) + return &StepMobile{step: s.step} +} + +// 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) + } + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} +} + +// 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) + } + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} +} + +// 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) + } + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} +} + +// 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) + } + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} +} + +// 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) + } + 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 { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.ACTION_DoubleTapXY, + Params: []float64{x, y}, + }) + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + 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) + } + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} +} + +// Times specify running times for run last action +func (s *StepMobile) Times(n int) *StepMobile { + if n <= 0 { + log.Warn().Int("n", n).Msg("times should be positive, set to 1") + n = 1 + } + + mobileStep := s.mobileStep() + actionsTotal := len(mobileStep.Actions) + if actionsTotal == 0 { + return s + } + + // actionsTotal >=1 && n >= 1 + lastAction := mobileStep.Actions[actionsTotal-1 : actionsTotal][0] + for i := 0; i < n-1; i++ { + // duplicate last action n-1 times + mobileStep.Actions = append(mobileStep.Actions, lastAction) + } + return &StepMobile{step: s.step} +} + +// 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, + }) + return &StepMobile{step: s.step} +} + +func (s *StepMobile) ScreenShot() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.CtlScreenShot, + Params: 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, + }) + return &StepMobile{step: s.step} +} + +func (s *StepMobile) StopCamera() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.CtlStopCamera, + Params: nil, + }) + return &StepMobile{step: s.step} +} + +// Validate switches to step validation. +func (s *StepMobile) Validate() *StepMobileUIValidation { + return &StepMobileUIValidation{ + step: s.step, + } +} + +func (s *StepMobile) Name() string { + return s.step.Name +} + +func (s *StepMobile) Type() StepType { + return stepTypeIOS +} + +func (s *StepMobile) Struct() *TStep { + return s.step +} + +func (s *StepMobile) Run(r *SessionRunner) (*StepResult, error) { + return runStepMobileUI(r, s.step) +} + +// StepMobileUIValidation implements IStep interface. +type StepMobileUIValidation struct { + step *TStep +} + +func (s *StepMobileUIValidation) AssertNameExists(expectedName string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorName, + Assert: uixt.AssertionExists, + Expect: expectedName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute name [%s] not found", expectedName) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertNameNotExists(expectedName string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorName, + Assert: uixt.AssertionNotExists, + Expect: expectedName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute name [%s] should not exist", expectedName) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionNotExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertOCRExists(expectedText string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionNotExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionNotExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) Name() string { + return s.step.Name +} + +func (s *StepMobileUIValidation) Type() StepType { + return stepTypeIOS +} + +func (s *StepMobileUIValidation) Struct() *TStep { + return s.step +} + +func (s *StepMobileUIValidation) Run(r *SessionRunner) (*StepResult, error) { + return runStepMobileUI(r, s.step) +} + +func (r *HRPRunner) initUIClient(uuid string, osType string) (client *uixt.DriverExt, err error) { + // avoid duplicate init + if uuid == "" && len(r.uiClients) > 0 { + for _, v := range r.uiClients { + return v, nil + } + } + + // avoid duplicate init + if uuid != "" { + if client, ok := r.uiClients[uuid]; ok { + return client, nil + } + } + + var device uixt.Device + if osType == "ios" { + device, err = uixt.NewIOSDevice(uixt.WithUDID(uuid)) + } else { + device, err = uixt.NewAndroidDevice(uixt.WithSerialNumber(uuid)) + } + if err != nil { + return nil, errors.Wrapf(err, "init %s device failed", osType) + } + + client, err = device.NewDriver(nil) + if err != nil { + return nil, err + } + + // cache wda client + if r.uiClients == nil { + r.uiClients = make(map[string]*uixt.DriverExt) + } + r.uiClients[client.UUID] = client + + return client, nil +} + +func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { + stepResult = &StepResult{ + Name: step.Name, + StepType: stepTypeIOS, + Success: false, + ContentSize: 0, + } + screenshots := make([]string, 0) + + // merge step variables with session variables + stepVariables, err := s.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } + + var osType string + var mobileStep *MobileStep + if step.IOS != nil { + // ios step + osType = "ios" + mobileStep = step.IOS + } else { + // android step + osType = "android" + mobileStep = step.Android + } + + // init wda/uia driver + uiDriver, err := s.caseRunner.hrpRunner.initUIClient(mobileStep.Serial, osType) + if err != nil { + return + } + uiDriver.StartTime = s.startTime + + defer func() { + attachments := make(map[string]interface{}) + if err != nil { + attachments["error"] = err.Error() + } + + // save attachments + screenshots = append(screenshots, uiDriver.ScreenShots...) + attachments["screenshots"] = screenshots + stepResult.Attachments = attachments + }() + + // prepare actions + var actions []uixt.MobileAction + if mobileStep.Actions == nil { + actions = []uixt.MobileAction{ + { + Method: mobileStep.Method, + Params: mobileStep.Params, + }, + } + } else { + actions = mobileStep.Actions + } + + // run actions + for _, action := range actions { + if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil { + return stepResult, errors.Wrap(code.ParseError, + fmt.Sprintf("parse action params failed: %v", err)) + } + if err := uiDriver.DoAction(action); err != nil { + if !code.IsErrorPredefined(err) { + err = errors.Wrap(code.MobileUIDriverError, err.Error()) + } + return stepResult, err + } + } + + // take snapshot + screenshotPath, err := uiDriver.ScreenShot( + fmt.Sprintf("%d_validate_%d", uiDriver.StartTime.Unix(), time.Now().Unix())) + if err != nil { + log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") + } else { + log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") + screenshots = append(screenshots, screenshotPath) + } + + // validate + validateResults, err := validateUI(uiDriver, step.Validators) + if err != nil { + if !code.IsErrorPredefined(err) { + err = errors.Wrap(code.MobileUIValidationError, err.Error()) + } + return + } + sessionData := newSessionData() + sessionData.Validators = validateResults + stepResult.Data = sessionData + stepResult.Success = true + return stepResult, nil +} diff --git a/hrp/step_mobile_ui_test.go b/hrp/step_mobile_ui_test.go new file mode 100644 index 00000000..c9c7dae3 --- /dev/null +++ b/hrp/step_mobile_ui_test.go @@ -0,0 +1,183 @@ +//go:build localtest + +package hrp + +import ( + "testing" +) + +func TestIOSSettingsAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on Settings"). + SetIOS(WithWDAPort(8700), WithWDAMjpegPort(8800)), + TestSteps: []IStep{ + NewStep("launch Settings"). + IOS().Home().Tap("设置"). + Validate(). + AssertNameExists("飞行模式"). + AssertLabelExists("蓝牙"). + AssertOCRExists("个人热点"), + NewStep("swipe up and down"). + IOS().SwipeUp().SwipeUp().SwipeDown(), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSSearchApp(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on Search App 资源库"), + TestSteps: []IStep{ + NewStep("进入 App 资源库 搜索框"). + IOS().Home().SwipeLeft().Times(2).Tap("dewey-search-field"). + Validate(). + AssertLabelExists("取消"), + NewStep("搜索抖音"). + IOS().Input("抖音\n"), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSAppLaunch(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("启动 & 关闭 App"). + SetIOS(WithWDAPort(8700), WithWDAMjpegPort(8800)), + TestSteps: []IStep{ + NewStep("终止今日头条"). + IOS().AppTerminate("com.ss.iphone.article.News"), + NewStep("启动今日头条"). + IOS().AppLaunch("com.ss.iphone.article.News"), + NewStep("终止今日头条"). + IOS().AppTerminate("com.ss.iphone.article.News"), + NewStep("启动今日头条"). + IOS().AppLaunchUnattached("com.ss.iphone.article.News"), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSWeixinLive(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on 微信直播"). + SetIOS(WithLogOn(true), WithWDAPort(8100), WithWDAMjpegPort(9100)), + TestSteps: []IStep{ + NewStep("启动微信"). + IOS(). + Home(). + AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 + Tap("微信"). + Validate(). + AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), + NewStep("进入直播页"). + IOS(). + Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 + TapByOCR("直播"). // 通过 OCR 识别「直播」 + Validate(). + AssertLabelExists("直播"), + NewStep("向上滑动 5 次"). + IOS(). + SwipeUp().Times(3).ScreenShot(). // 上划 3 次,截图保存 + SwipeUp().Times(2).ScreenShot(), // 再上划 2 次,截图保存 + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSCameraPhotoCapture(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios camera photo capture"), + TestSteps: []IStep{ + NewStep("launch camera"). + IOS().Home(). + StopCamera(). + StartCamera(). + Validate(). + AssertLabelExists("PhotoCapture", "拍照按钮不存在"), + NewStep("start recording"). + IOS().Tap("PhotoCapture"), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSCameraVideoCapture(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios camera video capture"), + TestSteps: []IStep{ + NewStep("launch camera"). + IOS().Home(). + StopCamera(). + StartCamera(). + Validate(). + AssertLabelExists("PhotoCapture", "录像按钮不存在"), + NewStep("switch to video capture"). + IOS(). + SwipeRight(). + Validate(). + AssertLabelExists("VideoCapture", "拍摄按钮不存在"), + NewStep("start recording"). + IOS(). + Tap("VideoCapture"). // 开始录像 + Sleep(5). + Tap("VideoCapture"), // 停止录像 + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSDouyinAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on 抖音"), + TestSteps: []IStep{ + NewStep("launch douyin"). + IOS().Home().Tap("//*[@label='抖音']"). + Validate(). + AssertLabelExists("首页", "首页 tab 不存在"). + AssertLabelExists("消息", "消息 tab 不存在"), + NewStep("swipe up and down"). + IOS().SwipeUp().Times(3).SwipeDown(), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestAndroidAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("android ui action"), + TestSteps: []IStep{ + NewStep("launch douyin"). + Android().Serial("xxx").Tap("抖音"). + Validate(). + AssertNameExists("首页", "首页 tab 不存在"). + AssertNameExists("消息", "消息 tab 不存在"), + NewStep("swipe up and down"). + Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/step_rendezvous.go b/hrp/step_rendezvous.go index edd9cf84..a9e5f0e0 100644 --- a/hrp/step_rendezvous.go +++ b/hrp/step_rendezvous.go @@ -44,7 +44,7 @@ func (s *StepRendezvous) Run(r *SessionRunner) (*StepResult, error) { } // pass current rendezvous if already released, activate rendezvous sequentially after spawn done - if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.testCase.ToTCase()) || !rendezvous.isSpawnDone() { + if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.caseRunner.testCase.ToTCase()) || !rendezvous.isSpawnDone() { return stepResult, nil } diff --git a/hrp/step_request.go b/hrp/step_request.go index 8abd7cdc..4a25d02e 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -21,8 +21,9 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/httpstat" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/httpstat" ) type HTTPMethod string @@ -292,37 +293,28 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err ContentSize: 0, } - defer func() { - // update testcase summary - if err != nil { - stepResult.Attachment = err.Error() - } - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - } - }() - - // override step variables - stepVariables, err := r.MergeStepVariables(step.Variables) + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) if err != nil { + err = errors.Wrap(err, "parse step variables failed") return } - err = prepareUpload(r.parser, step, stepVariables) + defer func() { + // update testcase summary + if err != nil { + stepResult.Attachments = err.Error() + } + }() + + err = prepareUpload(r.caseRunner.parser, step, stepVariables) if err != nil { return } sessionData := newSessionData() - parser := r.GetParser() - config := r.GetConfig() + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig rb := newRequestBuilder(parser, config, step.Request) rb.req.Method = string(step.Request.Method) @@ -355,7 +347,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } // log & print request - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printRequest(rb.req); err != nil { return stepResult, err } @@ -363,7 +355,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // stat HTTP request var httpStat httpstat.Stat - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { ctx := httpstat.WithHTTPStat(rb.req, &httpStat) rb.req = rb.req.WithContext(ctx) } @@ -371,9 +363,9 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // select HTTP client var client *http.Client if step.Request.HTTP2 { - client = r.hrpRunner.http2Client + client = r.caseRunner.hrpRunner.http2Client } else { - client = r.hrpRunner.httpClient + client = r.caseRunner.hrpRunner.httpClient } // set step timeout @@ -399,21 +391,21 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err defer resp.Body.Close() // log & print response - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printResponse(resp); err != nil { return stepResult, err } } // new response object - respObj, err := newHttpResponseObject(r.hrpRunner.t, parser, resp) + respObj, err := newHttpResponseObject(r.caseRunner.hrpRunner.t, parser, resp) if err != nil { err = errors.Wrap(err, "init ResponseObject error") return } stepResult.Elapsed = time.Since(start).Milliseconds() - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { // resp.Body has been ReadAll httpStat.Finish() stepResult.HttpStat = httpStat.Durations() @@ -692,7 +684,7 @@ func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs { s.step.TestCase, err = tc.ToTestCase() if err != nil { log.Error().Err(err).Msg("failed to load testcase") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return &StepTestCaseWithOptionalArgs{ step: s.step, @@ -705,7 +697,7 @@ func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs { s.step.API, err = api.ToAPI() if err != nil { log.Error().Err(err).Msg("failed to load api") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return &StepAPIWithOptionalArgs{ step: s.step, @@ -762,6 +754,22 @@ func (s *StepRequest) WebSocket() *StepWebSocket { } } +// Android creates a new android action +func (s *StepRequest) Android() *StepMobile { + s.step.Android = &MobileStep{} + return &StepMobile{ + step: s.step, + } +} + +// IOS creates a new ios action +func (s *StepRequest) IOS() *StepMobile { + s.step.IOS = &MobileStep{} + return &StepMobile{ + step: s.step, + } +} + // StepRequestWithOptionalArgs implements IStep interface. type StepRequestWithOptionalArgs struct { step *TStep diff --git a/hrp/step_request_test.go b/hrp/step_request_test.go index 338d7e7e..3f7e63c5 100644 --- a/hrp/step_request_test.go +++ b/hrp/step_request_test.go @@ -77,33 +77,17 @@ func TestRunRequestPostDataToStruct(t *testing.T) { } } -func TestRunRequestRun(t *testing.T) { - testcase := &TestCase{ - Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{stepGET, stepPOSTData}, - } - runner := NewRunner(t).SetRequestsLogOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) - - if _, err := stepGET.Run(sessionRunner); err != nil { - t.Fatalf("stepGET.Run() error: %v", err) - } - if _, err := stepPOSTData.Run(sessionRunner); err != nil { - t.Fatalf("stepPOSTData.Run() error: %v", err) - } -} - func TestRunRequestStatOn(t *testing.T) { testcase := &TestCase{ Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), TestSteps: []IStep{stepGET, stepPOSTData}, } - runner := NewRunner(t).SetHTTPStatOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) + caseRunner, _ := NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase) + sessionRunner := caseRunner.NewSession() if err := sessionRunner.Start(nil); err != nil { t.Fatal() } - summary := sessionRunner.GetSummary() + summary, _ := sessionRunner.GetSummary() stat := summary.Records[0].HttpStat if !assert.GreaterOrEqual(t, stat["DNSLookup"], int64(0)) { diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index 09440e5a..1464735e 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -5,6 +5,7 @@ import ( "time" "github.com/jinzhu/copier" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -51,18 +52,20 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe Success: false, } + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(s.step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } + defer func() { // update testcase summary if err != nil { - stepResult.Attachment = err.Error() + stepResult.Attachments = err.Error() } }() - stepVariables, err := r.MergeStepVariables(s.step.Variables) - if err != nil { - return stepResult, err - } - stepTestCase := s.step.TestCase.(*TestCase) // copy testcase to avoid data racing @@ -80,20 +83,27 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe // merge & override extractors copiedTestCase.Config.Export = mergeSlices(s.step.Export, copiedTestCase.Config.Export) - sessionRunner, err := r.hrpRunner.NewSessionRunner(copiedTestCase) + caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(copiedTestCase) if err != nil { - log.Error().Err(err).Msg("create session runner failed") + log.Error().Err(err).Msg("create case runner failed") return stepResult, err } + sessionRunner := caseRunner.NewSession() start := time.Now() // run referenced testcase with step variables err = sessionRunner.Start(stepVariables) - if err == nil { - stepResult.Success = true - } stepResult.Elapsed = time.Since(start).Milliseconds() - summary := sessionRunner.GetSummary() + + summary, err2 := sessionRunner.GetSummary() + if err2 != nil { + log.Error().Err(err).Msg("get summary failed") + if err != nil { + err = errors.Wrap(err, err2.Error()) + } else { + err = err2 + } + } // update step names for _, record := range summary.Records { record.Name = fmt.Sprintf("%s - %s", stepResult.Name, record.Name) @@ -102,11 +112,8 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe // export testcase export variables stepResult.ExportVars = summary.InOut.ExportVars - // merge testcase summary - r.summary.Records = append(r.summary.Records, summary.Records...) - r.summary.Stat.Total += summary.Stat.Total - r.summary.Stat.Successes += summary.Stat.Successes - r.summary.Stat.Failures += summary.Stat.Failures - + if err == nil { + stepResult.Success = true + } return stepResult, err } diff --git a/hrp/step_thinktime.go b/hrp/step_thinktime.go index 6b14b462..7a505cf8 100644 --- a/hrp/step_thinktime.go +++ b/hrp/step_thinktime.go @@ -39,7 +39,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { Success: true, } - cfg := r.parsedConfig.ThinkTimeSetting + cfg := r.caseRunner.parsedConfig.ThinkTimeSetting if cfg == nil { cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} } diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 90feec2d..fbb5cfd5 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -231,20 +231,19 @@ type WebSocketAction struct { Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` } -func initWebSocket(testcase *TestCase) { - for _, step := range testcase.TestSteps { - if step.Struct().WebSocket == nil { - continue - } - // init websocket action parameters - if step.Struct().WebSocket.Timeout <= 0 { - step.Struct().WebSocket.Timeout = defaultTimeout - } - // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 - if step.Struct().WebSocket.CloseStatusCode < 1000 || step.Struct().WebSocket.CloseStatusCode > 4999 { - step.Struct().WebSocket.CloseStatusCode = defaultCloseStatus - } +func (w *WebSocketAction) GetTimeout() int64 { + if w.Timeout <= 0 { + return defaultTimeout } + return w.Timeout +} + +func (w *WebSocketAction) GetCloseStatusCode() int64 { + // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 + if w.CloseStatusCode < 1000 || w.CloseStatusCode > 4999 { + return defaultCloseStatus + } + return w.CloseStatusCode } func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { @@ -255,32 +254,23 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er ContentSize: 0, } - defer func() { - // update testcase summary - if err != nil { - stepResult.Attachment = err.Error() - } - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - } - }() - - // override step variables - stepVariables, err := r.MergeStepVariables(step.Variables) + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) if err != nil { + err = errors.Wrap(err, "parse step variables failed") return } + defer func() { + // update testcase summary + if err != nil { + stepResult.Attachments = err.Error() + } + }() + sessionData := newSessionData() - parser := r.GetParser() - config := r.GetConfig() + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig dummyReq := &Request{ URL: step.WebSocket.URL, @@ -317,12 +307,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er start := time.Now() // do websocket action - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { fmt.Printf("-------------------- websocket action: %v --------------------\n", step.WebSocket.Type.toString()) } switch step.WebSocket.Type { case wsOpen: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("open websocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("open websocket connection") // use the current websocket connection if existed if r.wsConnMap[parsedURL] != nil { break @@ -332,12 +322,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "open connection failed") } case wsPing: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("send ping and expect pong") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("send ping and expect pong") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "send ping message failed") } - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) // asynchronous receiving pong message with timeout go func() { select { @@ -350,7 +340,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } }() case wsWriteAndRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("write a message and read response") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("write a message and read response") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "write message failed") @@ -360,7 +350,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "read message failed") } case wsRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("read only") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("read only") resp, err = readMessageWithTimeout(parsedURL, r, step) if err != nil { return stepResult, errors.Wrap(err, "read message failed") @@ -372,7 +362,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "write message failed") } case wsClose: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("close webSocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("close webSocket connection") resp, err = closeWithTimeout(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "close connection failed") @@ -380,7 +370,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er default: return stepResult, errors.Errorf("unexpected websocket frame type: %v", step.WebSocket.Type) } - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { err = printWebSocketResponse(resp) if err != nil { return stepResult, errors.Wrap(err, "print response failed") @@ -388,7 +378,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } stepResult.Elapsed = time.Since(start).Milliseconds() - respObj, err := getResponseObject(r.hrpRunner.t, r.parser, resp) + respObj, err := getResponseObject(r.caseRunner.hrpRunner.t, r.caseRunner.parser, resp) if err != nil { err = errors.Wrap(err, "get response object error") return @@ -470,7 +460,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan := make(chan *http.Response) errorChan := make(chan error) go func() { - conn, resp, err := r.hrpRunner.wsDialer.Dial(urlStr, requestHeader) + conn, resp, err := r.caseRunner.hrpRunner.wsDialer.Dial(urlStr, requestHeader) if err != nil { errorChan <- errors.Wrap(err, "dial tcp failed") return @@ -496,7 +486,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan <- resp }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -526,7 +516,7 @@ func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*w } } }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -545,7 +535,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl } // check priority: text message > binary message if step.WebSocket.TextMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.TextMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.TextMessage, stepVariables) if parseErr != nil { return parseErr } @@ -554,7 +544,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl return writeErr } } else if step.WebSocket.BinaryMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) if parseErr != nil { return parseErr } @@ -597,7 +587,7 @@ func writeWithAction(c *websocket.Conn, step *TStep, messageType int, message [] case wsPing: return c.WriteControl(websocket.PingMessage, message, time.Now().Add(defaultWriteWait)) case wsClose: - closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.CloseStatusCode), string(message)) + closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.GetCloseStatusCode()), string(message)) return c.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(defaultWriteWait)) default: return c.WriteMessage(messageType, message) @@ -637,7 +627,7 @@ func closeWithTimeout(urlString string, r *SessionRunner, step *TStep, stepVaria // r.wsConn.Close() will be called at the end of current session, so no need to Close here log.Info().Str("msg", readErr.Error()).Msg("connection closed") }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() diff --git a/hrp/summary.go b/hrp/summary.go index b73522cc..0883a3d8 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO + Logs []interface{} `json:"logs,omitempty" yaml:"logs,omitempty"` Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` } diff --git a/hrp/summary_test.go b/hrp/summary_test.go index 32343e0e..034ded16 100644 --- a/hrp/summary_test.go +++ b/hrp/summary_test.go @@ -12,7 +12,7 @@ func TestGenHTMLReport(t *testing.T) { StepType: stepTypeRequest, Success: false, ContentSize: 0, - Attachment: "err", + Attachments: "err", } caseSummary1.Records = []*StepResult{stepResult1, stepResult2, nil} summary.appendCaseSummary(caseSummary1) diff --git a/hrp/testcase.go b/hrp/testcase.go index e1cd4533..67d0d715 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -2,16 +2,15 @@ package hrp import ( "fmt" - "io/fs" - "os" "path/filepath" "strings" + "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/mitchellh/mapstructure" + "github.com/httprunner/httprunner/v4/hrp/internal/code" ) // ITestCase represents interface for testcases, @@ -51,6 +50,24 @@ func (tc *TestCase) ToTCase() *TCase { return tCase } +func (tc *TestCase) Dump2JSON(targetPath string) error { + tCase := tc.ToTCase() + err := builtin.Dump2JSON(tCase, targetPath) + if err != nil { + return errors.Wrap(err, "dump testcase to json failed") + } + return nil +} + +func (tc *TestCase) Dump2YAML(targetPath string) error { + tCase := tc.ToTCase() + err := builtin.Dump2YAML(tCase, targetPath) + if err != nil { + return errors.Wrap(err, "dump testcase to yaml failed") + } + return nil +} + // TestCasePath implements ITestCase interface. type TestCasePath string @@ -101,7 +118,8 @@ func (tc *TCase) MakeCompat() (err error) { func (tc *TCase) ToTestCase(casePath string) (*TestCase, error) { if tc.TestSteps == nil { - return nil, errors.New("invalid testcase format, missing teststeps!") + return nil, errors.Wrap(code.InvalidCaseFormat, + "invalid testcase format, missing teststeps!") } if tc.Config == nil { @@ -153,7 +171,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { if ok { path := filepath.Join(projectRootDir, apiPath) if !builtin.IsFilePathExists(path) { - return nil, errors.New("referenced api file not found: " + path) + return nil, errors.Wrap(code.ReferencedFileNotFound, + fmt.Sprintf("referenced api file not found: %s", path)) } refAPI := APIPath(path) @@ -165,7 +184,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } else { apiMap, ok := step.API.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("referenced api should be map or path(string), got %v", step.API) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("referenced api should be map or path(string), got %v", step.API)) } api := &API{} err = mapstructure.Decode(apiMap, api) @@ -176,7 +196,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } _, ok = step.API.(*API) if !ok { - return nil, fmt.Errorf("failed to handle referenced API, got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("failed to handle referenced API, got %v", step.TestCase)) } testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{ step: step, @@ -186,7 +207,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { if ok { path := filepath.Join(projectRootDir, casePath) if !builtin.IsFilePathExists(path) { - return nil, errors.New("referenced testcase file not found: " + path) + return nil, errors.Wrap(code.ReferencedFileNotFound, + fmt.Sprintf("referenced testcase file not found: %s", path)) } refTestCase := TestCasePath(path) @@ -198,7 +220,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } else { testCaseMap, ok := step.TestCase.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("referenced testcase should be map or path(string), got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("referenced testcase should be map or path(string), got %v", step.TestCase)) } tCase := &TCase{} err = mapstructure.Decode(testCaseMap, tCase) @@ -213,7 +236,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } _, ok = step.TestCase.(*TestCase) if !ok { - return nil, fmt.Errorf("failed to handle referenced testcase, got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("failed to handle referenced testcase, got %v", step.TestCase)) } testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{ step: step, @@ -242,6 +266,14 @@ func (tc *TCase) toTestCase() (*TestCase, error) { testCase.TestSteps = append(testCase.TestSteps, &StepWebSocket{ step: step, }) + } else if step.IOS != nil { + testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ + step: step, + }) + } else if step.Android != nil { + testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ + step: step, + }) } else { log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step") } @@ -293,7 +325,8 @@ func convertCompatValidator(Validators []interface{}) (err error) { for assertMethod, iValidatorContent := range validatorMap { validatorContent := iValidatorContent.([]interface{}) if len(validatorContent) > 3 { - return fmt.Errorf("unexpected validator format: %v", validatorMap) + return errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("unexpected validator format: %v", validatorMap)) } validator.Check = validatorContent[0].(string) validator.Assert = assertMethod @@ -306,7 +339,8 @@ func convertCompatValidator(Validators []interface{}) (err error) { Validators[i] = validator continue } - return fmt.Errorf("unexpected validator format: %v", validatorMap) + return errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("unexpected validator format: %v", validatorMap)) } return nil } @@ -333,61 +367,3 @@ func convertJmespathExpr(checkExpr string) string { } return strings.Join(checkItems, ".") } - -func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { - testCases := make([]*TestCase, 0) - - for _, iTestCase := range iTestCases { - if _, ok := iTestCase.(*TestCase); ok { - testcase, err := iTestCase.ToTestCase() - if err != nil { - log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct") - return nil, err - } - testCases = append(testCases, testcase) - continue - } - - // iTestCase should be a TestCasePath, file path or folder path - tcPath, ok := iTestCase.(*TestCasePath) - if !ok { - return nil, errors.New("invalid iTestCase type") - } - - casePath := tcPath.GetPath() - err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error { - if dir == nil { - // casePath is a file other than a dir - path = casePath - } else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") { - // skip hidden folders - return fs.SkipDir - } else { - // casePath is a dir - path = filepath.Join(casePath, path) - } - - // ignore non-testcase files - ext := filepath.Ext(path) - if ext != ".yml" && ext != ".yaml" && ext != ".json" { - return nil - } - - // filtered testcases - testCasePath := TestCasePath(path) - tc, err := testCasePath.ToTestCase() - if err != nil { - log.Warn().Err(err).Str("path", path).Msg("load testcase failed") - return nil - } - testCases = append(testCases, tc) - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "read dir failed") - } - } - - log.Info().Int("count", len(testCases)).Msg("load testcases successfully") - return testCases, nil -} diff --git a/httprunner/__init__.py b/httprunner/__init__.py index fe4e9b1c..1f96dea8 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.2.0" +__version__ = "v4.3.0" __description__ = "One-stop solution for HTTP(S) testing." @@ -19,6 +19,7 @@ from httprunner.step_thrift_request import ( StepThriftRequestValidation, ) + __all__ = [ "__version__", "__description__", diff --git a/httprunner/database/engine.py b/httprunner/database/engine.py index aa0c0cec..8a99deda 100644 --- a/httprunner/database/engine.py +++ b/httprunner/database/engine.py @@ -79,8 +79,8 @@ class DBEngine(object): if __name__ == "__main__": - # db = DBEngine(f"mysql+pymysql://xxxxx:xxxxx@10.0.0.1:3306/dbname?charset=utf8mb4") - db = DBEngine(f"sqlite:////Users/bytedance/HttpRunner/examples/data/sqlite.db") + # db = DBEngine("mysql+pymysql://xxxxx:xxxxx@10.0.0.1:3306/dbname?charset=utf8mb4") + db = DBEngine("sqlite:////Users/xxx/HttpRunner/examples/data/sqlite.db") print(db.fetchmany(""" select* from student""", 5)) print(db.fetchmany("select* from student", 5)) diff --git a/httprunner/utils.py b/httprunner/utils.py index b95d3ecb..cbcc8f8e 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -243,7 +243,8 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List): class ExtendJSONEncoder(json.JSONEncoder): - """especially used to safely dump json data with python object, such as MultipartEncoder""" + """especially used to safely dump json data with python object, + such as MultipartEncoder""" def default(self, obj): try: @@ -275,7 +276,8 @@ def is_support_multiprocessing() -> bool: Queue() return True except (ImportError, OSError): - # system that does not support semaphores(dependency of multiprocessing), like Android termux + # system that does not support semaphores + # (dependency of multiprocessing), like Android termux return False @@ -320,7 +322,10 @@ def gen_cartesian_product(*args: List[Dict]) -> List[Dict]: return product_list -LOGGER_FORMAT = "